Skip to content

Latest commit

 

History

History
147 lines (112 loc) · 4.49 KB

advanced_task.md

File metadata and controls

147 lines (112 loc) · 4.49 KB

発展課題

課題

Croma.Struct を利用してリクエストボディのバリデーション処理をリファクタリングしてください。

期待結果

  • リクエストボディが有効な場合

    $ curl -X POST "http://iot-intern.localhost:8080/api/v1/alert" -H "Content-Type: application/json" -d '{"type": "dead_battery"}' -w '\n%{http_code}\n'
    {"sent_at":"2021-06-18T06:26:53Z"}
    200
  • リクエストボディが無効な場合

    $ curl -X POST "http://iot-intern.localhost:8080/api/v1/alert" -H "Content-Type: application/json" -d '{"type": "hello"}' -w '\n%{http_code}\n'
    {"message":"Unable to understand the request","type":"BadRequest"}
    400

補足

  • Croma は、elixir で型ベースのプログラミングを補助するためのマクロユーティリティ
  • サンプル実装
    • web/controller/hello_croma.ex
  • Elixir講義の7章にも説明があります

Croma.Structの使用例

Croma.Structを用いることでStructの各fieldの制約などを宣言的に定義し、その制約に応じて値をvalidationすることができる関数(new/1)を自動生成することができる。 具体的な使用例は下記である。

defmodule Hoge do
  use Croma.Struct, fields: [
    foo: Croma.String,  # foo fieldがbinary型として宣言
    bar: Croma.Integer, # bar fieldがinteger型として宣言
  ]
end

# 上記で宣言した制約を満たすmap
valid_map = %{
 "foo" => "string",
 "bar" => 5
}

> Hoge.new(valid_map)
{:ok, %Hoge{bar: 5, foo: "string"}}

# 上記で宣言した制約を満たさないmap
invalid_map = %{
 "foo" => "string",
 "bar" => "not integer"
}

> Hoge.new(invalid_map)
{:error, {:invalid_value, [Hoge, {Croma.Integer, :bar}]}}

fieldの型として宣言できるもの(Croma.Stringなど)はここを参照。

fieldの型として独自の制約を作ることもできる。 例えば、field fooは数字3桁で文字列でなければならないといった制約はこれを利用して下記のようにかける。

defmodule Hoge do
  defmodule ThreeNumberStr do
    use Croma.SubtypeOfString, pattern: ~r/^[1-9]{3}$/ # binary型かつ正規表現で数字3桁のフォーマットであると宣言
  end
  use Croma.Struct, fields: [
    foo: ThreeNumberStr, # foo fieldとしてThreeNumberStrであることを宣言
    bar: Croma.Integer,  # bar fieldがinteger型として宣言
  ]
end

# 上記で宣言した制約を満たすmap
valid_map = %{
 "foo" => "123",
 "bar" => 5
}

> Hoge.new(valid_map)
{:ok, %Hoge{bar: 5, foo: "123"}}

# 上記で宣言した制約を満たさないmap
valid_map = %{
 "foo" => "1234",
 "bar" => "string"
}

> Hoge.new(invalid_map)
{:error, {:invalid_value, [Hoge, {Hoge.ThreeNumberStr, :foo}]}}

fieldとして値が必須であるかどうかはCroma.TypeGenによって制御できる。 例えば、fooが必須でbarはオプションであるstructは下記のように書けば良い。

use Croma
defmodule Hoge do
  use Croma.Struct, fields: [
    foo: Croma.String,                        # 必須
    bar: Croma.TypeGen.nilable(Croma.String), # オプショナル
  ]
end

# 上記で宣言した制約を満たすmap
valid_map = %{
 "foo" => "string"
}

> Hoge.new(valid_map)
{:ok, %Hoge{bar: nil, foo: "string"}}

# 上記で宣言した制約を満たさないmap
invalid_map = %{}

> Hoge.new(invalid_map)
{:error, {:value_missing, [Hoge, {Croma.String, :foo}]}}

defunの使用例

defunを使うことで、関数の定義とそのtypespec定義を同時に行なうことができます。 通常、関数とそのtypespecの定義は下記のように行います。

@spec f(integer, String.t) :: String.t
def f(a, b) do
  "\#{a} \#{b}"
end

上記では、最初の行で関数f/2のtypespecを定義し、続く2~4行目で関数の本体を定義しています。 defunを利用することで、この1行目と2行目をまとめて宣言できることで、簡潔に記述できます。

defun f(a :: integer, b :: String.t) :: String.t do
  "\#{a} \#{b}"
end

なお、defpに関しても同様のことがdefunpを用いて実現できます。