-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, 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", 32). ?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", 53). ?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", 69). ?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", 80). ?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", 92). ?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", 114). ?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", 143). ?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", 160). ?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", 173). ?DOC(" Value must equal the expected value\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", 183). ?DOC(" Escape hatch for user-defined checks\n"). -spec custom(fun((DNM) -> {ok, DNM} | {error, binary()})) -> fun((DNM) -> {ok, DNM} | {error, binary()}). custom(F) -> F.