JediHelpers.ChangesetHelpers (jedi_helpers v0.3.1)

Copy Markdown

Normalization and validation helpers for Ecto changesets.

These helpers are intended for schema changeset/2 pipelines that receive browser form input and need consistent values before persistence.

Summary

Functions

Trims string changes and converts whitespace-only strings to nil by default.

Trims leading and trailing whitespace from one or more string fields.

Validates that at least one of the given fields has a non-blank value.

Functions

normalize_strings(changeset, fields, opts \\ [])

@spec normalize_strings(Ecto.Changeset.t(), atom() | [atom()], keyword()) ::
  Ecto.Changeset.t()

Trims string changes and converts whitespace-only strings to nil by default.

This is useful for optional form fields where whitespace-only input should be stored as nil. Non-string fields and fields without a change are left alone.

Set :empty_to_nil to false to retain an empty string after trimming.

Use case and result

An optional profile form can store meaningful names while normalizing an empty email field to nil:

types = %{name: :string, email: :string}

result =
  {%{}, types}
  |> Ecto.Changeset.cast(%{"name" => "  Leia  ", "email" => "   "},
    [:name, :email]
  )
  |> JediHelpers.ChangesetHelpers.normalize_strings([:name, :email])

result.changes
# => %{email: nil, name: "Leia"}

With empty_to_nil: false, whitespace-only input becomes "" instead.

trim_whitespace(changeset, keys, opts \\ [])

@spec trim_whitespace(Ecto.Changeset.t(), atom() | [atom()], keyword()) ::
  Ecto.Changeset.t()

Trims leading and trailing whitespace from one or more string fields.

The helper also validates the post-trim length and can attach an Ecto unique constraint. Non-string fields are ignored.

Options

  • :max - maximum allowed length after trimming; defaults to 255.
  • :enforce_unique - adds unique_constraint/3; defaults to false.

Use case and result

A registration changeset can normalize a username before validating or inserting it:

types = %{username: :string}

result =
  {%{}, types}
  |> Ecto.Changeset.cast(%{"username" => "  leia  "}, [:username])
  |> JediHelpers.ChangesetHelpers.trim_whitespace(:username, max: 50)

Ecto.Changeset.get_change(result, :username)
# => "leia"

In a schema-backed changeset, pass enforce_unique: true to attach the database unique constraint after trimming.

When the trimmed value exceeds :max, the result is invalid rather than truncated:

result =
  {%{}, types}
  |> Ecto.Changeset.cast(%{"username" => "  too-long  "}, [:username])
  |> JediHelpers.ChangesetHelpers.trim_whitespace(:username, max: 4)

result.valid?
# => false

validate_any_required(changeset, fields, opts \\ [])

@spec validate_any_required(Ecto.Changeset.t(), [atom()], keyword()) ::
  Ecto.Changeset.t()

Validates that at least one of the given fields has a non-blank value.

By default, the error is attached to the first field. Use :error_field and :message to customize the resulting changeset error.

Use case and result

A contact form can accept either an email address or a phone number:

types = %{email: :string, phone: :string}

result =
  {%{}, types}
  |> Ecto.Changeset.cast(%{}, [:email, :phone])
  |> JediHelpers.ChangesetHelpers.validate_any_required([:email, :phone],
    error_field: :email,
    message: "email or phone is required"
  )

result.valid?
# => false

result.errors[:email]
# => {"email or phone is required", [validation: :required]}

If either field contains a value, the changeset remains valid.