-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, check_each/4, check2/5, refine/3, 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" "\n" " Build a validator for a struct by chaining `check` calls with `use`,\n" " then finish with `ok` and convert to a `Result` with `validate`. Every\n" " field runs, so a single call returns every error at once — not just the\n" " first.\n" "\n" " ```gleam\n" " import sift\n" " import sift/int as i\n" " import sift/string as s\n" "\n" " pub type User { User(name: String, email: String, age: Int) }\n" "\n" " pub fn validate_user(input: User) -> Result(User, List(sift.FieldError)) {\n" " use name <- sift.check(\"name\", input.name, s.non_empty(\"required\"))\n" " use email <- sift.check(\"email\", input.email, s.email(\"invalid\"))\n" " use age <- sift.check(\"age\", input.age, i.between(0, 150, \"out of range\"))\n" " sift.ok(User(name:, email:, age:))\n" " |> sift.validate\n" " }\n" " ```\n" "\n" " For nested structs use `nested`, for lists use `each`, for multiple\n" " constraints on one field use `check_all`, and for conditional\n" " constraints use `when`. See `example/contacts/` for a full walkthrough.\n" ). -type field_error() :: {field_error, list(binary()), binary()}. -file("src/sift.gleam", 58). ?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(), DLS, fun((DLS) -> {ok, DLS} | {error, binary()}), fun((DLS) -> {DLU, list(field_error())}) ) -> {DLU, 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", 79). ?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(), DLX, fun((DLX) -> {DLY, list(field_error())}), fun((DLY) -> {DMA, list(field_error())}) ) -> {DMA, 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", 95). ?DOC(" Wrap a final value into a Validated tuple with no errors\n"). -spec ok(DMD) -> {DMD, list(field_error())}. ok(Value) -> {Value, []}. -file("src/sift.gleam", 106). ?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({DMF, list(field_error())}) -> {ok, DMF} | {error, list(field_error())}. validate(Validated) -> case Validated of {Value, []} -> {ok, Value}; {_, Errors} -> {error, Errors} end. -file("src/sift.gleam", 118). ?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((DMK) -> {ok, DMK} | {error, binary()}), fun((DMK) -> {ok, DMK} | {error, binary()}) ) -> fun((DMK) -> {ok, DMK} | {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", 140). ?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(DMO), fun((DMO) -> {ok, DMO} | {error, binary()}), fun((list(DMO)) -> {DMS, list(field_error())}) ) -> {DMS, 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", 169). ?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((DMV) -> {ok, DMV} | {error, binary()}), fun((DMV) -> {ok, DMV} | {error, binary()}) ) -> fun((DMV) -> {ok, DMV} | {error, binary()}). 'or'(V1, V2) -> fun(Value) -> case V1(Value) of {ok, V} -> {ok, V}; {error, _} -> V2(Value) end end. -file("src/sift.gleam", 186). ?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((DMZ) -> {ok, DMZ} | {error, binary()}), binary()) -> fun((DMZ) -> {ok, DMZ} | {error, binary()}). 'not'(Validator, Msg) -> fun(Value) -> case Validator(Value) of {ok, _} -> {error, Msg}; {error, _} -> {ok, Value} end end. -file("src/sift.gleam", 205). ?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(DNC, binary()) -> fun((DNC) -> {ok, DNC} | {error, binary()}). equals(Expected, Msg) -> fun(Value) -> case Value =:= Expected of true -> {ok, Value}; false -> {error, Msg} end end. -file("src/sift.gleam", 224). ?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(), DNE, list(fun((DNE) -> {ok, DNE} | {error, binary()})), fun((DNE) -> {DNH, list(field_error())}) ) -> {DNH, 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", 248). ?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((DNK) -> {ok, DNK} | {error, binary()})) -> fun((DNK) -> {ok, DNK} | {error, binary()}). 'when'(Condition, Validator) -> fun(Value) -> case Condition of true -> Validator(Value); false -> {ok, Value} end end. -file("src/sift.gleam", 264). ?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(DNN), fun((DNN) -> {ok, DNN} | {error, binary()}), fun((gleam@option:option(DNN)) -> {DNR, list(field_error())}) ) -> {DNR, 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", 293). ?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(), DNU, fun((DNU) -> {ok, DNV} | {error, any()}), DNV, binary(), fun((DNV) -> {DNZ, list(field_error())}) ) -> {DNZ, 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", 321). ?DOC( " Validate every item in a list with a sub-validator function, prefixing\n" " error paths with the field name and the item's index.\n" " Produces paths like `[\"tags\", \"0\", \"name\"]`.\n" "\n" " Use this when each item is itself a struct to validate. For a single\n" " `Validator(a)` per item, use `each` instead.\n" "\n" " ```gleam\n" " use tags <- sift.check_each(\"tags\", input.tags, validate_tag)\n" " // errors get paths like [\"tags\", \"2\", \"name\"]\n" " ```\n" ). -spec check_each( binary(), list(DOC), fun((DOC) -> {DOE, list(field_error())}), fun((list(DOE)) -> {DOH, list(field_error())}) ) -> {DOH, list(field_error())}. check_each(Field, Values, Validator_fn, Next) -> {Validated_values, Item_errors} = begin _pipe = Values, _pipe@1 = gleam@list:index_map( _pipe, fun(Item, Idx) -> {Value, Errors} = Validator_fn(Item), Prefixed = gleam@list:map( Errors, fun(E) -> {field_error, [Field, erlang:integer_to_binary(Idx) | erlang:element(2, E)], erlang:element(3, E)} end ), {Value, Prefixed} end ), gleam@list:fold( _pipe@1, {[], []}, fun(Acc, Pair) -> {Values_acc, Errors_acc} = Acc, {Value@1, Errors@1} = Pair, {[Value@1 | Values_acc], lists:append(Errors_acc, Errors@1)} end ) end, Validated_values@1 = lists:reverse(Validated_values), {Result, Outer_errors} = Next(Validated_values@1), {Result, lists:append(Item_errors, Outer_errors)}. -file("src/sift.gleam", 363). ?DOC( " Cross-field validator comparing two already-validated values.\n" " On success, passes the (possibly transformed) first value to next.\n" " On failure, records a FieldError under `field` and passes `a` through\n" " so subsequent checks still run.\n" "\n" " ```gleam\n" " use name <- sift.check(\"name\", input.name, s.non_empty(\"required\"))\n" " use confirm <- sift.check(\"confirm\", input.confirm, s.non_empty(\"required\"))\n" " use name <- sift.check2(\"confirm\", name, confirm, fn(a, b) {\n" " case a == b { True -> Ok(a) False -> Error(\"must match name\") }\n" " })\n" " sift.ok(name)\n" " ```\n" ). -spec check2( binary(), DOK, DOL, fun((DOK, DOL) -> {ok, DOK} | {error, binary()}), fun((DOK) -> {DOO, list(field_error())}) ) -> {DOO, list(field_error())}. check2(Field, A, B, Validator, Next) -> case Validator(A, B) of {ok, V} -> Next(V); {error, Msg} -> {Result, Errors} = Next(A), {Result, [{field_error, [Field], Msg} | Errors]} end. -file("src/sift.gleam", 393). ?DOC( " Post-assembly whole-object check. Runs on a `Validated(a)` produced by\n" " `ok(...)`, useful for cross-field constraints expressed in terms of the\n" " final struct.\n" "\n" " ```gleam\n" " sift.ok(Registration(role:, mfa:))\n" " |> sift.refine(\"mfa\", fn(r) {\n" " case r.role == \"admin\" && r.mfa == None {\n" " True -> Error(\"required for admins\")\n" " False -> Ok(r)\n" " }\n" " })\n" " |> sift.validate\n" " ```\n" ). -spec refine( {DOR, list(field_error())}, binary(), fun((DOR) -> {ok, DOR} | {error, binary()}) ) -> {DOR, list(field_error())}. refine(Validated, Field, Check) -> {Value, Errors} = Validated, case Check(Value) of {ok, V} -> {V, Errors}; {error, Msg} -> {Value, lists:append(Errors, [{field_error, [Field], Msg}])} end. -file("src/sift.gleam", 420). ?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((DOW) -> {ok, DOW} | {error, binary()})) -> fun((DOW) -> {ok, DOW} | {error, binary()}). custom(F) -> F.