# `Koyomi.Changeset`
[🔗](https://github.com/hattoriai/koyomi/blob/v0.2.1/lib/koyomi/changeset.ex#L1)

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 `t: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.

# `cast_datetime`

```elixir
@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`

```elixir
@spec messages() :: %{required(atom()) =&gt; String.t()}
```

Returns the default error messages, by kind.

## Examples

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

# `validate_date`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
