Custom operator: skip operation on invalid changeset

I think it would be useful to have custom operator in Ecto.Changeset itself to have an ability to skip some operations.

It allows to have basic validation on the beginning and perform computation/io-heavy validations at the end only if entire changeset is valid.
Using ~> instead of ~>> or >>> allows quick switch between default pipe operator just by replacing one symbol.

Usage example

  defmacro changeset ~> function do
    quote do
      case unquote(changeset) do
        %{valid?: true} = changeset -> changeset |> unquote(function)
        changeset -> changeset
      end
    end
  end
card
|> cast(params, [:github_repo, :board_id, :text, :name])
|> validate_requried([:github_repo, :board_id, :text, :name])
~> validate_repo_existence()
~> validate_board()

Happy to read your feedback :slight_smile:

The Haskeller in me recognizes the piping of Either / Maybe datatypes.

Perhaps should you read also about Validation, which is basically akin to Either but accumulates the errors so you can have a full trace of what goes wrong.

You should take a look at https://hex.pm/packages/witchcraft which has the plumbing to make that possible.

If you have a validation that should only run on a valid-so-far changeset, it seems clearer to just write that:

card
|> cast(params, [:github_repo, :board_id, :text, :name])
|> validate_requried([:github_repo, :board_id, :text, :name])
|> validate_repo_existence()
|> validate_board()

def validate_repo_existence(%{valid?: false} = changeset), do: changeset
def validate_repo_existence(changeset) do
  # etc
end
1 Like

For one simple case - yes :slight_smile: When you have a lot of validations and some cast_assoc calls you’ll see a lot of boilerplate.

I’d guess that this is unlikely to be accepted in Ecto. This type of feature is regularly rejected for Elixir core. The reason there is that you can implement it yourself and that defining a function that does what you want is often clearer. I’d expect similar arguments for adding it to Ecto.

2 Likes

+1.

In this case, I’d probably go for something like this:

defp maybe_validate(%{valid?: true} = changeset, validator), do: validator.(changeset)
defp maybe_validate(changeset, _validator), do: changeset

changeset
|> # ...
|> maybe_validate(&validate_repo_existence/1)
|> maybe_validate(&validate_board/1)
3 Likes