Packages

Schema validation for Gleam — constraints, error accumulation, and field paths

Current section

Files

Jump to
sift src sift.erl
Raw

src/sift.erl

-module(sift).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sift.gleam").
-export([check/4, nested/4, ok/1, validate/1, 'and'/2, each/4, 'or'/2, 'not'/2, equals/2, check_all/4, 'when'/2, check_optional/4, check_parse/6, custom/1]).
-export_type([field_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" Core validation functions — check fields, accumulate errors, compose validators.\n").
-type field_error() :: {field_error, list(binary()), binary()}.
-file("src/sift.gleam", 33).
?DOC(
" Run a validator on a field value, accumulate errors, feeds into `use`.\n"
"\n"
" ```gleam\n"
" use name <- sift.check(\"name\", input.name, s.min_length(1, \"required\"))\n"
" use email <- sift.check(\"email\", input.email, s.email(\"invalid\"))\n"
" sift.ok(User(name:, email:))\n"
" ```\n"
).
-spec check(
binary(),
DMA,
fun((DMA) -> {ok, DMA} | {error, binary()}),
fun((DMA) -> {DMC, list(field_error())})
) -> {DMC, list(field_error())}.
check(Field, Value, Validator, Next) ->
case Validator(Value) of
{ok, V} ->
Next(V);
{error, Msg} ->
{Result, Errors} = Next(Value),
{Result, [{field_error, [Field], Msg} | Errors]}
end.
-file("src/sift.gleam", 54).
?DOC(
" Run a sub-validator function, prefixing error paths with the field name.\n"
"\n"
" ```gleam\n"
" use address <- sift.nested(\"address\", input.address, validate_address)\n"
" // errors get paths like [\"address\", \"zip\"]\n"
" ```\n"
).
-spec nested(
binary(),
DMF,
fun((DMF) -> {DMG, list(field_error())}),
fun((DMG) -> {DMI, list(field_error())})
) -> {DMI, list(field_error())}.
nested(Field, Value, Validator_fn, Next) ->
{Inner_value, Inner_errors} = Validator_fn(Value),
Prefixed_errors = gleam@list:map(
Inner_errors,
fun(E) ->
{field_error, [Field | erlang:element(2, E)], erlang:element(3, E)}
end
),
{Result, Outer_errors} = Next(Inner_value),
{Result, lists:append(Prefixed_errors, Outer_errors)}.
-file("src/sift.gleam", 70).
?DOC(" Wrap a final value into a Validated tuple with no errors\n").
-spec ok(DML) -> {DML, list(field_error())}.
ok(Value) ->
{Value, []}.
-file("src/sift.gleam", 81).
?DOC(
" Convert a Validated(a) to Result(a, List(FieldError)).\n"
"\n"
" ```gleam\n"
" sift.ok(User(name: \"Jo\", email: \"jo@example.com\"))\n"
" |> sift.validate\n"
" // -> Ok(User(name: \"Jo\", email: \"jo@example.com\"))\n"
" ```\n"
).
-spec validate({DMN, list(field_error())}) -> {ok, DMN} |
{error, list(field_error())}.
validate(Validated) ->
case Validated of
{Value, []} ->
{ok, Value};
{_, Errors} ->
{error, Errors}
end.
-file("src/sift.gleam", 93).
?DOC(
" Compose two validators — run both, accumulate errors from both.\n"
"\n"
" ```gleam\n"
" let validator = s.min_length(1, \"required\") |> sift.and(s.email(\"invalid\"))\n"
" ```\n"
).
-spec 'and'(
fun((DMS) -> {ok, DMS} | {error, binary()}),
fun((DMS) -> {ok, DMS} | {error, binary()})
) -> fun((DMS) -> {ok, DMS} | {error, binary()}).
'and'(V1, V2) ->
fun(Value) -> case {V1(Value), V2(Value)} of
{{ok, A}, {ok, _}} ->
{ok, A};
{{ok, _}, {error, Msg}} ->
{error, Msg};
{{error, Msg@1}, {ok, _}} ->
{error, Msg@1};
{{error, Msg@2}, _} ->
{error, Msg@2}
end end.
-file("src/sift.gleam", 115).
?DOC(
" Validate every item in a list, accumulating indexed error paths.\n"
" Produces paths like `[\"tags\", \"0\"]`, `[\"tags\", \"1\"]`, etc.\n"
"\n"
" ```gleam\n"
" use tags <- sift.each(\"tags\", input.tags, s.non_empty(\"empty tag\"))\n"
" // invalid items get paths like [\"tags\", \"2\"]\n"
" ```\n"
).
-spec each(
binary(),
list(DMW),
fun((DMW) -> {ok, DMW} | {error, binary()}),
fun((list(DMW)) -> {DNA, list(field_error())})
) -> {DNA, list(field_error())}.
each(Field, Items, Validator, Next) ->
Item_errors = begin
_pipe = Items,
_pipe@1 = gleam@list:index_map(
_pipe,
fun(Item, Idx) -> case Validator(Item) of
{ok, _} ->
[];
{error, Msg} ->
[{field_error,
[Field, erlang:integer_to_binary(Idx)],
Msg}]
end end
),
lists:append(_pipe@1)
end,
{Result, Outer_errors} = Next(Items),
{Result, lists:append(Item_errors, Outer_errors)}.
-file("src/sift.gleam", 144).
?DOC(
" Pass if either validator succeeds (try v1 first, then v2).\n"
"\n"
" ```gleam\n"
" let validator = s.email(\"invalid\") |> sift.or(s.url(\"invalid\"))\n"
" ```\n"
).
-spec 'or'(
fun((DND) -> {ok, DND} | {error, binary()}),
fun((DND) -> {ok, DND} | {error, binary()})
) -> fun((DND) -> {ok, DND} | {error, binary()}).
'or'(V1, V2) ->
fun(Value) -> case V1(Value) of
{ok, V} ->
{ok, V};
{error, _} ->
V2(Value)
end end.
-file("src/sift.gleam", 161).
?DOC(
" Invert a validator — fail if it passes, pass if it fails.\n"
"\n"
" ```gleam\n"
" let not_admin = sift.not(s.one_of([\"admin\"], \"\"), \"cannot be admin\")\n"
" ```\n"
).
-spec 'not'(fun((DNH) -> {ok, DNH} | {error, binary()}), binary()) -> fun((DNH) -> {ok,
DNH} |
{error, binary()}).
'not'(Validator, Msg) ->
fun(Value) -> case Validator(Value) of
{ok, _} ->
{error, Msg};
{error, _} ->
{ok, Value}
end end.
-file("src/sift.gleam", 180).
?DOC(
" Value must equal the expected value.\n"
"\n"
" ```gleam\n"
" let validator = sift.equals(\"yes\", \"must accept terms\")\n"
" validator(\"yes\") // -> Ok(\"yes\")\n"
" validator(\"no\") // -> Error(\"must accept terms\")\n"
" ```\n"
).
-spec equals(DNK, binary()) -> fun((DNK) -> {ok, DNK} | {error, binary()}).
equals(Expected, Msg) ->
fun(Value) -> case Value =:= Expected of
true ->
{ok, Value};
false ->
{error, Msg}
end end.
-file("src/sift.gleam", 199).
?DOC(
" Run multiple validators on a field, accumulate all errors.\n"
"\n"
" ```gleam\n"
" use name <- sift.check_all(\"name\", input.name, [\n"
" s.non_empty(\"required\"),\n"
" s.min_length(3, \"too short\"),\n"
" s.max_length(100, \"too long\"),\n"
" ])\n"
" sift.ok(name)\n"
" ```\n"
).
-spec check_all(
binary(),
DNM,
list(fun((DNM) -> {ok, DNM} | {error, binary()})),
fun((DNM) -> {DNP, list(field_error())})
) -> {DNP, list(field_error())}.
check_all(Field, Value, Validators, Next) ->
Field_errors = begin
_pipe = Validators,
gleam@list:filter_map(_pipe, fun(V) -> case V(Value) of
{ok, _} ->
{error, nil};
{error, Msg} ->
{ok, {field_error, [Field], Msg}}
end end)
end,
{Result, Outer_errors} = Next(Value),
{Result, lists:append(Field_errors, Outer_errors)}.
-file("src/sift.gleam", 223).
?DOC(
" Conditional validator — runs the validator only when condition is True.\n"
"\n"
" ```gleam\n"
" use state <- sift.check(\"state\", input.state,\n"
" sift.when(country == \"US\", s.non_empty(\"required\")))\n"
" ```\n"
).
-spec 'when'(boolean(), fun((DNS) -> {ok, DNS} | {error, binary()})) -> fun((DNS) -> {ok,
DNS} |
{error, binary()}).
'when'(Condition, Validator) ->
fun(Value) -> case Condition of
true ->
Validator(Value);
false ->
{ok, Value}
end end.
-file("src/sift.gleam", 239).
?DOC(
" Validate an Option value only when Some, skip when None.\n"
"\n"
" ```gleam\n"
" use nickname <- sift.check_optional(\"nickname\", input.nickname,\n"
" s.min_length(2, \"too short\"))\n"
" sift.ok(User(nickname:))\n"
" ```\n"
).
-spec check_optional(
binary(),
gleam@option:option(DNV),
fun((DNV) -> {ok, DNV} | {error, binary()}),
fun((gleam@option:option(DNV)) -> {DNZ, list(field_error())})
) -> {DNZ, list(field_error())}.
check_optional(Field, Value, Validator, Next) ->
case Value of
none ->
Next(none);
{some, V} ->
case Validator(V) of
{ok, _} ->
Next({some, V});
{error, Msg} ->
{Result, Errors} = Next({some, V}),
{Result, [{field_error, [Field], Msg} | Errors]}
end
end.
-file("src/sift.gleam", 268).
?DOC(
" Parse a raw value and feed the result into the chain.\n"
" On success, passes the parsed value to next.\n"
" On failure, records a FieldError and passes the default to next\n"
" so that subsequent fields still validate.\n"
"\n"
" ```gleam\n"
" use age <- sift.check_parse(\"age\", \"42\", int.parse, 0, \"must be a number\")\n"
" use age <- sift.check(\"age\", age, i.min(13, \"must be at least 13\"))\n"
" sift.ok(age)\n"
" ```\n"
).
-spec check_parse(
binary(),
DOC,
fun((DOC) -> {ok, DOD} | {error, any()}),
DOD,
binary(),
fun((DOD) -> {DOH, list(field_error())})
) -> {DOH, list(field_error())}.
check_parse(Field, Value, Parser, Default, Msg, Next) ->
case Parser(Value) of
{ok, Parsed} ->
Next(Parsed);
{error, _} ->
{Result, Errors} = Next(Default),
{Result, [{field_error, [Field], Msg} | Errors]}
end.
-file("src/sift.gleam", 297).
?DOC(
" Escape hatch for user-defined checks.\n"
"\n"
" ```gleam\n"
" let even = sift.custom(fn(n: Int) {\n"
" case n % 2 == 0 {\n"
" True -> Ok(n)\n"
" False -> Error(\"must be even\")\n"
" }\n"
" })\n"
" even(4) // -> Ok(4)\n"
" even(3) // -> Error(\"must be even\")\n"
" ```\n"
).
-spec custom(fun((DOK) -> {ok, DOK} | {error, binary()})) -> fun((DOK) -> {ok,
DOK} |
{error, binary()}).
custom(F) ->
F.