Koyomi.Changeset (Koyomi v0.2.1)

Copy Markdown View Source

Changeset validations with the rules of a picker (see Koyomi.Rules).

Give the same rules to the component and to the changeset, so that the browser and the server never disagree:

@stay_rules Koyomi.Rules.new(min: :today, max_days: 14)

def changeset(booking, attrs) do
  booking
  |> cast(attrs, [:stay])
  |> validate_required([:stay])
  |> Koyomi.Changeset.validate_range(:stay, @stay_rules)
end

Each validation checks the field only when it has a value (a change or the data's own value), and adds at most one error. The error has validation: :koyomi and kind: (the reason, see Koyomi.Rules.error/0) in its keys, and the values of the message (min, max, count, date...), so that translate_error/1 of a Phoenix app can translate it. count is set for the messages with a number, for plural forms.

Options

  • :message - a message for every error, or a map of messages by kind (%{too_long: "a stay is two weeks at most"}); the kinds that the map does not name keep the default message.

Summary

Functions

Casts a date and time that a person gave in a time zone into a DateTime in UTC.

Returns the default error messages, by kind.

Validates a :date field (or a Koyomi.Month or Koyomi.Week field): its day must be allowed by the rules.

Validates a list of days (Koyomi.Dates or {:array, :date}): each day, and the count of days.

Validates a date and time field (:utc_datetime, :naive_datetime and their _usec forms): its day, its time of day in the time zone of the rules, and the bounds.

Validates a :time field against :min_time, :max_time and :step.

Functions

cast_datetime(changeset, params, field, time_zone, opts \\ [])

@spec cast_datetime(Ecto.Changeset.t(), map(), atom(), String.t(), keyword()) ::
  Ecto.Changeset.t()

Casts a date and time that a person gave in a time zone into a DateTime in UTC.

The date and time picker with a time_zone posts an ISO 8601 string with the offset of the zone ("2027-07-15T14:30:00+01:00"), which a :utc_datetime field casts on its own. This function also reads a date and time without an offset ("2027-03-15T14:30", as the browser's own input posts it when JavaScript is off) as a wall clock time in time_zone. Call it instead of listing the field in cast/4:

booking
|> cast(attrs, [:name])
|> Koyomi.Changeset.cast_datetime(attrs, :starts_at, "Europe/Lisbon")

A wall clock time that the zone skips (the hour the clocks go forward) gets the error :gap; a time that it repeats (the hour the clocks go back) takes the first of the two instants, or the second with ambiguous: :later, or gets the error :ambiguous with ambiguous: :error. A zone other than UTC needs a time zone database (see Calendar). params may have string or atom keys.

messages()

@spec messages() :: %{required(atom()) => String.t()}

Returns the default error messages, by kind.

Examples

iex> Koyomi.Changeset.messages().disabled
"is not available"

validate_date(changeset, field, rules, opts \\ [])

@spec validate_date(
  Ecto.Changeset.t(),
  atom(),
  Koyomi.Rules.t() | keyword(),
  keyword()
) ::
  Ecto.Changeset.t()

Validates a :date field (or a Koyomi.Month or Koyomi.Week field): its day must be allowed by the rules.

Examples

iex> types = %{due: :date}
iex> changeset = Ecto.Changeset.cast({%{}, types}, %{"due" => "2027-03-13"}, [:due])
iex> changeset = Koyomi.Changeset.validate_date(changeset, :due, disabled_weekdays: [6, 7])
iex> changeset.errors
[due: {"is not available", [validation: :koyomi, kind: :disabled, date: ~D[2027-03-13]]}]

validate_dates(changeset, field, rules, opts \\ [])

@spec validate_dates(
  Ecto.Changeset.t(),
  atom(),
  Koyomi.Rules.t() | keyword(),
  keyword()
) ::
  Ecto.Changeset.t()

Validates a list of days (Koyomi.Dates or {:array, :date}): each day, and the count of days.

validate_datetime(changeset, field, rules, opts \\ [])

@spec validate_datetime(
  Ecto.Changeset.t(),
  atom(),
  Koyomi.Rules.t() | keyword(),
  keyword()
) ::
  Ecto.Changeset.t()

Validates a date and time field (:utc_datetime, :naive_datetime and their _usec forms): its day, its time of day in the time zone of the rules, and the bounds.

validate_range(changeset, field, rules, opts \\ [])

@spec validate_range(
  Ecto.Changeset.t(),
  atom() | {atom(), atom()},
  Koyomi.Rules.t() | keyword(),
  keyword()
) :: Ecto.Changeset.t()

Validates a range of days.

field is a Koyomi.DateRange field (a Date.Range), or a {from, to} tuple of two :date fields; with two fields, the error goes on from for a rule of its day and on to otherwise.

Examples

iex> types = %{check_in: :date, check_out: :date}
iex> params = %{"check_in" => "2027-03-01", "check_out" => "2027-03-20"}
iex> changeset = Ecto.Changeset.cast({%{}, types}, params, [:check_in, :check_out])
iex> changeset = Koyomi.Changeset.validate_range(changeset, {:check_in, :check_out}, max_days: 14)
iex> changeset.errors
[check_out: {"must be at most %{count} days", [validation: :koyomi, kind: :too_long, count: 14, max_days: 14]}]

validate_time(changeset, field, rules, opts \\ [])

@spec validate_time(
  Ecto.Changeset.t(),
  atom(),
  Koyomi.Rules.t() | keyword(),
  keyword()
) ::
  Ecto.Changeset.t()

Validates a :time field against :min_time, :max_time and :step.