3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Phoenix.HTMLのtime_inputが直接insertできなかったんでectoの中を見てみた

Last updated at Posted at 2018-03-28

Phoenixで時間をectoに保存する際に、time_selectを使ったときに軽くはまりました。
その時に調べた内容を備忘録的に記録していきます。

環境情報

debian : version 8
Phoenix : 1.3.2
Phoenix.HTML : 2.10
ecto : 2.2.9

エラーについて

Phoenix.HTMLtime_input のデータをそのままectoの :time field に保存するときにバリデーションエラーになりました。

# User schema
defmodule Example.User do
  use ExampleWeb, :model

  schema "users" do
    field :set_time, :time 
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:set_time])
  end
end
> User.changeset(%User{}, %{"set_time" => "10:30"})
%Ecto.Changeset{
  action: null,
  changes: %{},
  errors: [
    set_time: {"is invalid", [type: :time, validation: :cast]}
  ],
  data: #Example.User<>,
  valid?: false
}

エラー内容から :time fieldへの保存でcastの際にバリデーションエラーになっていることがわかります。

では、 Ecto.Changesetcast 関数を見て原因を探っていきましょう。

Ecto.Changeset

def cast(%{__struct__: module} = data, params, permitted, opts) do
  cast(data, module.__changeset__, %{}, params, permitted, opts)
end

defp cast(%{} = data, %{} = types, %{} = changes, %{} = params, permitted, opts) when is_list(permitted) do
  {empty_values, _opts} = Keyword.pop(opts, :empty_values, @empty_values)
  params = convert_params(params)

  defaults = case data do
    %{__struct__: struct} -> struct.__struct__()
    %{} -> %{}
  end

  {changes, errors, valid?} =
      Enum.reduce(permitted, {changes, [], true},
                  &process_param(&1, params, types, data, empty_values, defaults, &2))

    %Changeset{params: params, data: data, valid?: valid?,
               errors: Enum.reverse(errors), changes: changes,
               types: types, empty_values: empty_values}
  end

初めに、上記 cast 関数でパブリック関数の cast 関数が呼ばれ、そのあとにプライベートの cast 関数が呼ばれます。

# パブリック関数の引数の値
module => Example.User
data => %Example.User{...}
params => %{"set_time" => "10:30"}
permitted => [:set_time]
opts => []
# プライベート関数の引数の値
data => %Example.User{...}
types => %{
  set_time: :time 
  ...
}
changes => %{}
params => %{"set_time" => "10:30"}
permitted => [:set_time]
opts => []

ここでparamsの値のチェックをしている部分か下記の部分になります。

{changes, errors, valid?} =
      Enum.reduce(permitted, {changes, [], true},
                  &process_param(&1, params, types, data, empty_values, defaults, &2))

Enum.reduce なんで process_param 関数を呼び、permittedの中を順番にチェック、最終的な結果を返します。

defp process_param(key, params, types, data, empty_values, defaults, {changes, errors, valid?}) do
  # param_keyにkeyのstringの値を入れる
  {key, param_key} = cast_key(key)
  type = type!(types, key)

  # changesetを引数で渡しているなら現在のchangesetを取得する
  current =
    case changes do
      %{^key => value} -> value
      _ -> Map.get(data, key)
    end

  case cast_field(key, param_key, type, params, current, empty_values, defaults, valid?) do
    {:ok, value, valid?} ->
      {Map.put(changes, key, value), errors, valid?}
    :missing ->
      {changes, errors, valid?}
    :invalid ->
      {changes, [{key, {"is invalid", [type: type, validation: :cast]}} | errors], false}
  end
end

ここで cast_field 関数の結果で invalid:の処理が実行されているようなので、 cast_field 関数の中の実装を見ていきます。

# params => %{"set_time" => "10:30"}
# param_key => "set_time"
# type => :set_time
# value => "10:30"
defp cast_field(key, param_key, type, params, current, empty_values, defaults, valid?) do
  case params do
    %{^param_key => value} ->
      value = if value in empty_values, do: Map.get(defaults, key), else: value
      case Ecto.Type.cast(type, value) do
        {:ok, ^current} ->
          :missing
        {:ok, value} ->
          {:ok, value, valid?}
        :error ->
          :invalid
      end
    _ ->
      :missing
  end
end

最終的に Ecto.Type.cast 関数で値が指定した型に cast できるかの判定を行います。

Ecto.Type

case Ecto.Type.cast(type, value) do
def cast(:time, term) do
  cast_time(term)
end

...

defp cast_time(binary) when is_binary(binary) do
   case Time.from_iso8601(binary) do
    {:ok, _} = ok -> ok
    {:error, _} -> :error
  end
end

なるほど、、Time.from_iso8601 の関数だったか。
"10:30"っという文字列だと Time.from_iso8601 のフォーマットに合わないためエラーになっています。
ちなみに、from_iso8601の関数は下記のようになっています。

def from_iso8601(<<hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>, calendar) do
  with {hour, ""} <- Integer.parse(hour),
       {min, ""} <- Integer.parse(min),
       {sec, ""} <- Integer.parse(sec),
       {microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
       {_offset, ""} <- Calendar.ISO.parse_offset(rest) do
    with {:ok, utc_time} <- new(hour, min, sec, microsec, Calendar.ISO),
         do: convert(utc_time, calendar)
  else
    _ -> {:error, :invalid_format}
  end
end

def from_iso8601(<<_::binary>>, _calendar) do
  {:error, :invalid_format}
end

確かに、引数にマッチせず、下のほうの :invalid_format の関数が呼ばれ、invalid errorになっているようです。

終わりに

現状、"HH:mm"での形式を変換できないのでアプリケーション側で制御してfrom_8601で変換できる形式にするしかないようですね。
それか、time_input ではなく、 time_select ですれば問題なく動きます。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?