-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/1]). -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(String))) {\n" " sift.validate({\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" " })\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(DLN) :: {field_error, list(binary()), DLN}. -file("src/sift.gleam", 62). ?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(), DLW, fun((DLW) -> {ok, DLW} | {error, DLX}), fun((DLW) -> {DMA, list(field_error(DLX))}) ) -> {DMA, list(field_error(DLX))}. 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", 83). ?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(DMH))}), fun((DMG) -> {DMK, list(field_error(DMH))}) ) -> {DMK, list(field_error(DMH))}. nested(Field, Value, Validator_fn, Next) -> {Inner_value, Inner_errors} = Validator_fn(Value), Prefixed_errors = gleam@list:map( Inner_errors, fun(Err) -> {field_error, [Field | erlang:element(2, Err)], erlang:element(3, Err)} end ), {Result, Outer_errors} = Next(Inner_value), {Result, lists:append(Prefixed_errors, Outer_errors)}. -file("src/sift.gleam", 99). ?DOC(" Wrap a final value into a Validated tuple with no errors\n"). -spec ok(DMP) -> {DMP, list(field_error(any()))}. ok(Value) -> {Value, []}. -file("src/sift.gleam", 110). ?DOC( " Convert a Validated(a, e) to Result(a, List(FieldError(e))).\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({DMT, list(field_error(DMU))}) -> {ok, DMT} | {error, list(field_error(DMU))}. validate(Validated) -> case Validated of {Value, []} -> {ok, Value}; {_, Errors} -> {error, Errors} end. -file("src/sift.gleam", 122). ?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((DNB) -> {ok, DNB} | {error, DNC}), fun((DNB) -> {ok, DNB} | {error, DNC}) ) -> fun((DNB) -> {ok, DNB} | {error, DNC}). '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", 141). ?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(DNJ), fun((DNJ) -> {ok, DNJ} | {error, DNL}), fun((list(DNJ)) -> {DNP, list(field_error(DNL))}) ) -> {DNP, list(field_error(DNL))}. 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", 167). ?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((DNU) -> {ok, DNU} | {error, DNV}), fun((DNU) -> {ok, DNU} | {error, DNV}) ) -> fun((DNU) -> {ok, DNU} | {error, DNV}). 'or'(V1, V2) -> fun(Value) -> case V1(Value) of {ok, V} -> {ok, V}; {error, _} -> V2(Value) end end. -file("src/sift.gleam", 184). ?DOC( " Invert a validator — fail if it passes, pass if it fails.\n" "\n" " The inverted validator's own error is discarded, so its error type is\n" " independent of `msg`'s — pass any placeholder you like.\n" "\n" " ```gleam\n" " let not_admin = sift.not(s.one_of([\"admin\"], Nil), \"cannot be admin\")\n" " ```\n" ). -spec 'not'(fun((DOC) -> {ok, DOC} | {error, any()}), DOG) -> fun((DOC) -> {ok, DOC} | {error, DOG}). 'not'(Validator, Msg) -> fun(Value) -> case Validator(Value) of {ok, _} -> {error, Msg}; {error, _} -> {ok, Value} end end. -file("src/sift.gleam", 200). ?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(DOJ, DOK) -> fun((DOJ) -> {ok, DOJ} | {error, DOK}). equals(Expected, Msg) -> fun(Value) -> case Value =:= Expected of true -> {ok, Value}; false -> {error, Msg} end end. -file("src/sift.gleam", 219). ?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(), DON, list(fun((DON) -> {ok, DON} | {error, DOO})), fun((DON) -> {DOS, list(field_error(DOO))}) ) -> {DOS, list(field_error(DOO))}. 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", 243). ?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((DOX) -> {ok, DOX} | {error, DOY})) -> fun((DOX) -> {ok, DOX} | {error, DOY}). 'when'(Condition, Validator) -> fun(Value) -> case Condition of true -> Validator(Value); false -> {ok, Value} end end. -file("src/sift.gleam", 259). ?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(DPD), fun((DPD) -> {ok, DPD} | {error, DPF}), fun((gleam@option:option(DPD)) -> {DPJ, list(field_error(DPF))}) ) -> {DPJ, list(field_error(DPF))}. 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", 288). ?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(), DPO, fun((DPO) -> {ok, DPP} | {error, any()}), DPP, DPT, fun((DPP) -> {DPU, list(field_error(DPT))}) ) -> {DPU, list(field_error(DPT))}. 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", 316). ?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, e)` 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(DPZ), fun((DPZ) -> {DQB, list(field_error(DQC))}), fun((list(DQB)) -> {DQG, list(field_error(DQC))}) ) -> {DQG, list(field_error(DQC))}. 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(Err) -> {field_error, [Field, erlang:integer_to_binary(Idx) | erlang:element(2, Err)], erlang:element(3, Err)} 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", 358). ?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(), DQL, DQM, fun((DQL, DQM) -> {ok, DQL} | {error, DQN}), fun((DQL) -> {DQQ, list(field_error(DQN))}) ) -> {DQQ, list(field_error(DQN))}. 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", 388). ?DOC( " Post-assembly whole-object check. Runs on a `Validated(a, e)` 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( {DQV, list(field_error(DQW))}, binary(), fun((DQV) -> {ok, DQV} | {error, DQW}) ) -> {DQV, list(field_error(DQW))}. 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", 415). ?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((DRD) -> {ok, DRD} | {error, DRE})) -> fun((DRD) -> {ok, DRD} | {error, DRE}). custom(F) -> F.