defmodule Mv.Membership.Email do @moduledoc """ Custom Ash type for validated email addresses. ## Overview This type extends `:string` with email-specific validation constraints. It ensures that email values stored in CustomFieldValue resources are valid email addresses according to a standard regex pattern. ## Validation Rules - **Optional**: `nil` and empty strings are allowed (custom fields are optional) - Minimum length: 5 characters (for non-empty values) - Maximum length: 254 characters (RFC 5321 maximum) - Pattern: Standard email format (username@domain.tld) - Automatic trimming of leading/trailing whitespace (empty strings become `nil`) ## Usage This type is used in the CustomFieldValue union type for custom fields with `value_type: :email` in CustomField definitions. ## Example # In a custom field definition CustomField.create!(%{ name: "work_email", value_type: :email }) # Valid values "user@example.com" "first.last@company.co.uk" # Invalid values "not-an-email" # Missing @ and domain "a@b" # Too short """ @match_pattern ~S/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ @match_regex Regex.compile!(@match_pattern) @min_length 5 @max_length 254 use Ash.Type.NewType, subtype_of: :string, constraints: [ match: @match_pattern, trim?: true, min_length: @min_length, max_length: @max_length ] @impl true def cast_input(nil, _), do: {:ok, nil} @impl true def cast_input(value, _) when is_binary(value) do value = String.trim(value) cond do # Empty string after trim becomes nil (optional field) value == "" -> {:ok, nil} String.length(value) < @min_length -> :error String.length(value) > @max_length -> :error !Regex.match?(@match_regex, value) -> :error true -> {:ok, value} end end @impl true def cast_input(_, _), do: :error end