Current section

Files

Jump to
funx lib validate behaviour.ex
Raw

lib/validate/behaviour.ex

defmodule Funx.Validate.Behaviour do
@moduledoc """
Behaviour for validation functions.
All validators follow a consistent arity-3 signature, matching other DSL behaviours in Funx.
## Contract
```elixir
@callback validate(value :: any(), opts :: keyword(), env :: map()) ::
Funx.Monad.Either.t(any(), Funx.Errors.ValidationError.t())
| :ok
| {:ok, any()}
| {:error, Funx.Errors.ValidationError.t()}
```
## Arguments
- `value` - The value to validate (may be transformed by previous validators)
- `opts` - Keyword list of options (validator-specific configuration)
- `env` - Environment map (runtime context like database connections, session data)
## Return Values
**Canonical (preferred)**:
- `Either.right(value)` - Validation passed, return original or transformed value
- `Either.left(ValidationError.t())` - Validation failed with error
**Legacy (supported via normalization)**:
- `:ok` - Validation passed, return original value
- `{:ok, value}` - Validation passed with transformation
- `{:error, ValidationError.t()}` - Validation failed
## Semantic Rules
1. **Arguments strictly ordered**: value, opts, env
2. **Either is canonical** (tagged tuples normalized by DSL)
3. **Value transformation allowed** (sequential within focus)
4. **Never raise for validation failure** (use Left/error tuple)
5. **Return ValidationError** for errors (not raw strings)
6. **Concurrency-safe** by contract
## Message Option
All validators should support a `:message` option for custom error messages:
- **String**: `[message: "custom error"]`
- **Function**: `[message: fn value -> "got \#{inspect(value)}" end]`
When a function is provided, it receives the current value being validated.
## Example
```elixir
defmodule MyValidator do
@behaviour Funx.Validate.Behaviour
alias Funx.Monad.Either
alias Funx.Errors.ValidationError
@impl true
def validate(value, opts, _env) do
if valid?(value) do
Either.right(value)
else
message = get_message(opts, value, "default error message")
Either.left(ValidationError.new(message))
end
end
defp get_message(opts, value, default) do
case Keyword.get(opts, :message) do
nil -> default
msg when is_binary(msg) -> msg
msg_fn when is_function(msg_fn, 1) -> msg_fn.(value)
end
end
end
```
"""
alias Funx.Errors.ValidationError
alias Funx.Monad.Either
@callback validate(value :: any(), opts :: keyword(), env :: map()) ::
Either.t(any(), ValidationError.t())
| :ok
| {:ok, any()}
| {:error, ValidationError.t()}
end