Shorthand for custom transformations (on_success)

The previous section was about transformations that come for free with Ecto.Schema. Others are performed by other code.

Consider the code used on previous pages, which looks roughly like this:

  embedded_schema do
    field :date, :date
  end
  
  def changeset(struct, params) do
    struct
    |> cast(params, [:date])

The problem with that code is that casting an incorrect ISO8601 string will produce a changeset with:

  • an error value, but

  • no change associated with the date field.

As a result, the incorrect form field will be wiped out (set to blank). That's not much of a problem, given that you're probably using a date-picker on the front end (that only allows valid strings) and if someone's circumventing the web page to generate bad POST values, well, they deserve what they get. Nevertheless, let's replace it with a schema like this:

  embedded_schema do
    field :date_string, :string, virtual: true
    field :date, :date
  end

Here, the date_stringcan be anything. It's not up to the cast to translate the string into a Date; it's the responsibility of another part of the changeset function, something like the last line below:

  def changeset(struct, params) do
    struct
    |> cast(params, [:date_string])
    |> validate_required([:date_string])
    |> cast_date

This pair of fields is like the as_cast values on the previous page in that the result is easily predictable from the input. On the previous page, :date was the result of Changeset.cast. Here, it's the result of Date.from_iso8601applied to the :date_string field. In both cases, the result is a trivial function call...

... at least in the non-error case. Let's focus on the success case here because, as is often the case, the result is calculated by a function we don't have to write. (We'll look at the error case later.)

Here is how to indicate date's transformation:

    |> field_transformations(
      as_cast: [:date_string],
      date: on_success(
        Date.from_iso8601!(:date_string)),

Notice that the on_success argument looks like a function call. Any is_atom argument is taken to refer to other values in the changeset.

Given this field_transformation, we don't have to write a changeset check for date. That is, this:

    |> category(                                         :success,
      ok: [
        params(      name: "Bossie", date_string:   "2001-01-01"),
      ]
    )

... will automatically check that the value of date is ~D[2001-01-01].

If you don't like that on_success notation (which uses a macro), you can use a function form:

date: on_success(&Date.from_iso8601!, applied_to: :date_string)

That form is required if you're using an anonymous function rather than one from a module:

date: on_success(&(&1 + 1), applied_to: :age) 

Both forms can take more than one argument. Here, for example, are the two ways of describing the result of a days_since_2000 field that's calculated from date:

days_since_2000: on_success(
  Date.diff(:date, ~D[2000-01-01])
  
days_since_2000: on_success(
  &Date.diff/2, applied_to: [:date, ~D[2000-01-01]])

Note that the non-atom value is taken as-is; it's not used for a changeset lookup like :date is.

Last updated