-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.