/home/runner/work/klsn/klsn/_build/test/cover/eunit/klsn_rule.html

1 -module(klsn_rule).
2
3 %% public functions
4 -export([
5 validate/2
6 , normalize/2
7 , eval/3
8 , lookup_alias/1
9 ]).
10
11 %% builtin rules
12 -export([
13 term_rule/3
14 , exact_rule/3
15 , default_rule/3
16 , boolean_rule/3
17 , integer_rule/3
18 , float_rule/3
19 , number_rule/3
20 , range_rule/3
21 , alias_rule/3
22 , with_defs_rule/3
23 , ref_rule/3
24 , timeout_rule/3
25 , binstr_rule/3
26 , atom_rule/3
27 , enum_rule/3
28 , any_of_rule/3
29 , all_of_rule/3
30 , foldl_rule/3
31 , optnl_rule/3
32 , nullable_rule/3
33 , strict_rule/3
34 , list_rule/3
35 , tuple_rule/3
36 , map_rule/3
37 , struct_rule/3
38 ]).
39
40 -export_type([
41 name/0
42 , input/0
43 , output/0
44 , state/0
45 , alias/0
46 , alias/1
47 , alias/2
48 , alias_ref/0
49 , custom/0
50 , rule_param/0
51 , rule/0
52 , reason/0
53 , result/0
54 , strict_result/0
55 ]).
56
57 -type name() :: atom().
58
59 -type input() :: term().
60
61 -type output() :: term().
62
63 -type state() :: #{
64 klsn_rule => state_()
65 , module() => term()
66 }.
67 -type state_() :: #{
68 definitions => definitions()
69 }.
70
71 -type ref_key() :: klsn:binstr().
72
73 -type definitions() :: #{ref_key() => rule()}.
74
75 -type alias() :: atom().
76
77 %% Just to define a type by using an rule alias.
78 -type alias(_AliasName) :: term().
79 -type alias(_Module, _AliasName) :: term().
80
81 -type alias_ref() :: {module(), alias()}.
82
83 -type custom() :: fun( (input(), rule_param(), state()) -> result() ).
84
85 -type rule_param() :: term().
86
87 -type rule() :: {custom, name(), custom(), rule_param()}
88 | term
89 | {exact, term()}
90 | {default, {output(), rule()}}
91 | boolean
92 | integer
93 | float
94 | number
95 | {range, range_(rule())}
96 | {alias, alias_ref()}
97 | {with_defs, {definitions(), rule()}}
98 | {ref, ref_key()}
99 | timeout
100 | binstr
101 | atom
102 | {enum, [atom()]}
103 | {any_of, [rule()]}
104 | {all_of, [rule()]}
105 | {foldl, [rule()]}
106 | {optnl, rule()}
107 | {nullable, rule()}
108 | {strict, rule()}
109 | {list, rule()}
110 | {tuple, [rule()] | tuple()} % {rule(), rule(), ...}
111 | {map, {KeyRule::rule(), ValueRule::rule()}}
112 | {struct, #{atom() => {required | optional, rule()}}}
113 .
114
115 -type reason() :: {custom, term()}
116 | {unknown_rule, rule()}
117 | {invalid, name(), input()}
118 | {invalid_exact, term(), input()}
119 | {invalid_enum, [atom()], input()}
120 | {invalid_list_element, pos_integer(), reason()}
121 | {invalid_tuple_size, non_neg_integer(), input()}
122 | {invalid_tuple_element, pos_integer(), reason()}
123 | {invalid_optnl_value, reason()}
124 | {invalid_nullable_value, reason()}
125 | {invalid_map_key, reason()}
126 | {invalid_map_value, Key::term(), reason()}
127 | {map_key_conflict, Key::term()}
128 | {invalid_alias, alias_ref(), reason()}
129 | {undefined_alias, alias_ref(), input()}
130 | {invalid_ref, ref_key(), reason()}
131 | {undefined_ref, ref_key(), input()}
132 | {invalid_struct_field, term()}
133 | {invalid_struct_value, atom(), reason()}
134 | {missing_required_field, atom()}
135 | {struct_field_conflict, atom()}
136 | {any_of, [reason()]}
137 | {all_of, [reason()]}
138 | {strict, reason()}
139 | {invalid_range, range_(input())}
140 .
141
142 -type result() :: valid
143 | {valid, output()}
144 | {normalized, output()}
145 | {normalized, output(), reason()}
146 | reject
147 | {reject, reason()}
148 .
149
150 -type strict_result() :: {valid, output()}
151 | {normalized, output(), reason()}
152 | {reject, reason()}
153 .
154
155 -type range_(Subject) :: {number(), '<' | '=<', Subject}
156 | {Subject, '<' | '=<', number()}
157 | {number(), '<' | '=<', Subject, '<' | '=<', number()}
158 .
159
160 %% @doc
161 %% Lookup a named rule alias declared via -klsn_rule_alias in a module.
162 %%
163 %% Returns {value, Rule} when found, otherwise none.
164 %%
165 %% Examples:
166 %% ```
167 %% 1> klsn_rule:lookup_alias({my_mod, my_alias}).
168 %% {value, integer}
169 %% 2> klsn_rule:lookup_alias({my_mod, missing}).
170 %% none
171 %% '''
172 -spec lookup_alias(alias_ref()) -> klsn:optnl(rule()).
173 lookup_alias({Module, Alias}) when is_atom(Module), is_atom(Alias) ->
174 982 try Module:module_info(attributes) of
175 Attrs ->
176 981 AliasRules0 = proplists:lookup_all(klsn_rule_alias, Attrs),
177 981 AliasRules1 = lists:map(fun
178 ({_, RuleEntries}) when is_list(RuleEntries) ->
179 1977 RuleEntries;
180 ({_, RuleEntry}) ->
181
:-(
[RuleEntry]
182 end, AliasRules0),
183 981 AliasRules = lists:concat(AliasRules1),
184 981 case lists:search(fun
185 ({RuleName0, _Rule0}) when RuleName0 =:= Alias ->
186 980 true;
187 (_) ->
188 10258 false
189 end, AliasRules) of
190 {value, {_, RuleValue}} ->
191 980 {value, RuleValue};
192 false ->
193 1 none
194 end
195 catch _:_ ->
196 1 none
197 end;
198 lookup_alias(_Arg1) ->
199 1 none.
200
201 %% @doc
202 %% Validate an input against a rule.
203 %%
204 %% This calls {@link eval/3} and only accepts a `{valid, Output}'
205 %% result. Any normalized or reject result raises
206 %% `error({klsn_rule, Reason})'.
207 %%
208 %% Examples:
209 %% ```
210 %% 1> klsn_rule:validate(10, integer).
211 %% ok
212 %% 2> klsn_rule:validate(<<"10">>, integer).
213 %% ** exception error: {klsn_rule,{invalid,integer,<<"10">>}}
214 %% '''
215 %% @see normalize/2
216 %% @see eval/3
217 -spec validate(input(), rule()) -> ok.
218 validate(Input, Rule) ->
219 3 case eval(Input, Rule, #{}) of
220 {valid, _} ->
221 1 ok;
222 {normalized, _, Reason} ->
223 1 error({?MODULE, Reason});
224 {reject, Reason} ->
225 1 error({?MODULE, Reason})
226 end.
227
228 %% @doc
229 %% Normalize an input according to a rule.
230 %%
231 %% This calls {@link eval/3}. Valid and normalized results return the output,
232 %% while reject results raise `error({klsn_rule, Reason})'.
233 %% Normalization reasons are dropped; use {@link eval/3} if you need them.
234 %%
235 %% Examples:
236 %% ```
237 %% 1> klsn_rule:normalize(<<"10">>, integer).
238 %% 10
239 %% 2> klsn_rule:normalize(5, {range, {0, '=<', integer, '<', 10}}).
240 %% 5
241 %% 3> klsn_rule:normalize(99, {range, {0, '=<', integer, '<', 10}}).
242 %% ** exception error: {klsn_rule,{invalid_range,{0,'=<',99,'<',10}}}
243 %% '''
244 %% @see validate/2
245 %% @see eval/3
246 -spec normalize(input(), rule()) -> output().
247 normalize(Input, Rule) ->
248 6 case eval(Input, Rule, #{}) of
249 {valid, Output} ->
250 2 Output;
251 {normalized, Output, _Reason} ->
252 3 Output;
253 {reject, Reason} ->
254 1 error({?MODULE, Reason})
255 end.
256
257 %% @doc
258 %% Accept any input and always return valid for validate/2, normalize/2, and eval/3.
259 %% Rule form: term (rule()).
260 %%
261 %% Result (eval/3):
262 %% - valid with the input unchanged.
263 %% - never normalized (no reason()).
264 %% - never rejected (no reason()).
265 %%
266 %% Examples:
267 %% ```
268 %% 1> klsn_rule:validate({any, value}, term).
269 %% ok
270 %% 2> klsn_rule:normalize(123, term).
271 %% 123
272 %% 3> klsn_rule:eval([a, b], term, #{}).
273 %% {valid, [a, b]}
274 %% '''
275 %% @see validate/2
276 %% @see normalize/2
277 %% @see eval/3
278 -spec term_rule(input(), rule_param(), state()) -> result().
279 term_rule(_Input, _Param, _State) ->
280 80 valid.
281
282 %% @doc
283 %% Match an input against the exact rule parameter value.
284 %%
285 %% Rule form: {exact, Exact}.
286 %%
287 %% When evaluated via {@link eval/3}:
288 %% - valid: {valid, Input} when Input =:= Exact
289 %% - reject: {reject, {invalid_exact, Exact, Input}}
290 %%
291 %% This rule never produces a normalized result. {@link normalize/2} either
292 %% returns the original input or raises with {klsn_rule, {invalid_exact, Exact, Input}}.
293 %%
294 %% Examples:
295 %% ```
296 %% 1> klsn_rule:eval(42, {exact, 42}, #{}).
297 %% {valid, 42}
298 %% 2> klsn_rule:eval(7, {exact, 42}, #{}).
299 %% {reject, {invalid_exact, 42, 7}}
300 %% 3> klsn_rule:normalize(ok, {exact, ok}).
301 %% ok
302 %% 4> klsn_rule:normalize(error, {exact, ok}).
303 %% ** exception error: {klsn_rule,{invalid_exact,ok,error}}
304 %% 5> klsn_rule:validate(ok, {exact, ok}).
305 %% ok
306 %% '''
307 %% @see validate/2
308 %% @see normalize/2
309 %% @see eval/3
310 -spec exact_rule(input(), rule_param(), state()) -> result().
311 exact_rule(Input, Exact, _State) ->
312 129 case Input =:= Exact of
313 true ->
314 6 valid;
315 false ->
316 123 {reject, {invalid_exact, Exact, Input}}
317 end.
318
319 %% @doc
320 %% Apply Rule and fall back to Default when it rejects.
321 %%
322 %% Rule form: {default, {Default, Rule}} where Default is output() and
323 %% Rule is rule().
324 %%
325 %% When evaluated via {@link eval/3}:
326 %% - valid: {valid, Output} when Rule returns {valid, Output}.
327 %% - normalized: {normalized, Output, Reason} when Rule returns
328 %% {normalized, Output, Reason}.
329 %% - normalized: {normalized, Default, Reason} when Rule returns
330 %% {reject, Reason}.
331 %% - reject: {reject, {invalid, default, Input}} when the rule parameter is not
332 %% {Default, Rule}.
333 %%
334 %% Reason handling:
335 %% - Reasons are passed through from Rule (including {unknown_rule, Rule}).
336 %% - If Rule normalizes without a reason, {@link eval/3} uses
337 %% {invalid, RuleName, Input} where RuleName is the inner rule name.
338 %% - {@link normalize/2} returns Output or Default and drops the reason.
339 %% - {@link validate/2} raises error({klsn_rule, Reason}) on normalized or
340 %% reject results.
341 %%
342 %% Examples:
343 %% ```
344 %% 1> klsn_rule:eval(ok, {default, {ok, {exact, ok}}}, #{}).
345 %% {valid, ok}
346 %% 2> klsn_rule:eval(error, {default, {ok, {exact, ok}}}, #{}).
347 %% {normalized, ok, {invalid_exact, ok, error}}
348 %% 3> klsn_rule:normalize(1, {default, {0, {exact, 0}}}).
349 %% 0
350 %% 4> klsn_rule:validate(1, {default, {0, {exact, 0}}}).
351 %% ** exception error: {klsn_rule,{invalid_exact,0,1}}
352 %% 5> klsn_rule:eval(1, {default, []}, #{}).
353 %% {reject, {invalid, default, 1}}
354 %% '''
355 %% @see validate/2
356 %% @see normalize/2
357 %% @see eval/3
358 -spec default_rule(input(), rule_param(), state()) -> result().
359 default_rule(Input, {Default, Rule}, State) ->
360 8 case eval(Input, Rule, State) of
361 {valid, Output} ->
362 1 {valid, Output};
363 {normalized, Output, Reason} ->
364 2 {normalized, Output, Reason};
365 {reject, Reason} ->
366 5 {normalized, Default, Reason}
367 end;
368 default_rule(_, _, _State) ->
369
:-(
reject.
370
371 %% @doc
372 %% Normalize booleans from common boolean-like inputs.
373 %%
374 %% Rule form: boolean.
375 %%
376 %% Accepted as-is: true and false.
377 %% Otherwise, values are coerced by:
378 %% - returning false for numbers with `-1 < N < 1';
379 %% - converting via klsn_binstr:from_any/1 and treating
380 %% `<<>>', "false"/"False"/"FALSE", "null"/"Null"/"NULL", the
381 %% lowercase "undefined", and the Unicode fullwidth zero (U+FF10)
382 %% as false; everything else is true.
383 %%
384 %% When evaluated via {@link eval/3}:
385 %% - valid: {valid, Input} when Input is boolean
386 %% - normalized: {normalized, Bool, {invalid, boolean, Input}} when coerced
387 %% - reject: {reject, {invalid, boolean, Input}} when not coercible
388 %%
389 %% validate/2 only accepts literal booleans; any coercion or reject
390 %% yields error({klsn_rule, {invalid, boolean, Input}}). Use
391 %% normalize/2 to allow coercion.
392 %%
393 %% Examples:
394 %% ```
395 %% 1> klsn_rule:eval(true, boolean, #{}).
396 %% {valid, true}
397 %% 2> klsn_rule:eval(<<"false">>, boolean, #{}).
398 %% {normalized, false, {invalid, boolean, <<"false">>}}
399 %% 3> klsn_rule:normalize(0, boolean).
400 %% false
401 %% 4> klsn_rule:eval(<<"0">>, boolean, #{}).
402 %% {normalized, true, {invalid, boolean, <<"0">>}}
403 %% 5> klsn_rule:eval(#{}, boolean, #{}).
404 %% {reject, {invalid, boolean, #{}}}
405 %% 6> klsn_rule:validate(<<"false">>, boolean).
406 %% ** exception error: {klsn_rule,{invalid,boolean,<<"false">>}}
407 %% '''
408 %% @see validate/2
409 %% @see normalize/2
410 %% @see eval/3
411 -spec boolean_rule(input(), rule_param(), state()) -> result().
412 boolean_rule(Input, _Param, _State) ->
413 24 do(
414 fun is_boolean/1
415 , [fun
416 (I) when -1 < I, I < 1 ->
417 1 false;
418 (I) ->
419 12 case klsn_binstr:from_any(I) of
420 1 <<>> -> false;
421 1 <<"false">> -> false;
422 1 <<"False">> -> false;
423 1 <<"FALSE">> -> false;
424 1 <<"0"/utf8>> -> false;
425 1 <<"null">> -> false;
426 1 <<"Null">> -> false;
427 1 <<"NULL">> -> false;
428 1 <<"undefined">> -> false;
429 2 _ -> true
430 end
431 end]
432 , Input
433 ) .
434
435 %% @doc
436 %% Validate integer input for use with validate/2, normalize/2, and eval/3.
437 %% Rule form: integer.
438 %%
439 %% Result (eval/3):
440 %% - valid when Input is an integer.
441 %% - normalized when Input is a list or binary that parses via
442 %% binary_to_integer/1 or list_to_integer/1.
443 %% - reject when Input is neither an integer nor a parseable list/binary.
444 %%
445 %% Reason (eval/3):
446 %% - {invalid, integer, Input} on normalize and reject.
447 %% validate/2 raises error({klsn_rule, {invalid, integer, Input}}) when
448 %% normalization or rejection occurs.
449 %%
450 %% Examples:
451 %% ```
452 %% 1> klsn_rule:eval(10, integer, #{}).
453 %% 2> klsn_rule:eval("10", integer, #{}).
454 %% 3> klsn_rule:normalize("10", integer).
455 %% 4> klsn_rule:eval("nope", integer, #{}).
456 %% '''
457 %% @see validate/2
458 %% @see normalize/2
459 %% @see eval/3
460 -spec integer_rule(input(), rule_param(), state()) -> result().
461 integer_rule(Input, _Param, _State) ->
462 128 do(
463 fun is_integer/1
464 , [fun binary_to_integer/1, fun list_to_integer/1]
465 , Input
466 ) .
467
468 %% @doc
469 %% Validate float input for validate/2, normalize/2, and eval/3.
470 %% Rule form: float.
471 %%
472 %% Result (eval/3):
473 %% - valid when Input is a float.
474 %% - normalized when Input is a binary or list that parses via binary_to_float/1
475 %% or list_to_float/1; reason is {invalid, float, Input}.
476 %% - reject when Input cannot be converted; reason is {invalid, float, Input}.
477 %%
478 %% normalize/2 returns the float output (original or parsed) and drops the reason.
479 %% validate/2 accepts only valid results; normalized or reject raise
480 %% error({klsn_rule, {invalid, float, Input}}).
481 %%
482 %% Examples:
483 %% ```
484 %% 1> klsn_rule:eval(1.25, float, #{}).
485 %% {valid, 1.25}
486 %% 2> klsn_rule:eval(<<"1.25">>, float, #{}).
487 %% {normalized, 1.25, {invalid, float, <<"1.25">>}}
488 %% 3> klsn_rule:normalize("1.25", float).
489 %% 1.25
490 %% 4> klsn_rule:eval(<<"nope">>, float, #{}).
491 %% {reject, {invalid, float, <<"nope">>}}
492 %% '''
493 %% @see validate/2
494 %% @see normalize/2
495 %% @see eval/3
496 -spec float_rule(input(), rule_param(), state()) -> result().
497 float_rule(Input, _Param, _State) ->
498 15 do(
499 fun is_float/1
500 , [fun binary_to_float/1, fun list_to_float/1]
501 , Input
502 ) .
503
504 %% @doc
505 %% Validate numeric input for validate/2, normalize/2, and eval/3.
506 %% Rule form: number (or {number, Param}; Param is ignored).
507 %%
508 %% Result (eval/3):
509 %% - valid when Input is a number (integer or float).
510 %% - normalized when Input is a binary or list that parses as an integer/float;
511 %% parsing order is binary_to_integer/1, binary_to_float/1,
512 %% list_to_integer/1, list_to_float/1.
513 %% - reject when Input is not numeric and cannot be parsed.
514 %%
515 %% Reason (eval/3):
516 %% - {invalid, number, Input} on normalize or reject.
517 %%
518 %% normalize/2 returns the parsed number (or original) or raises with
519 %% {klsn_rule, {invalid, number, Input}}. validate/2 returns ok only for
520 %% already-numeric input and raises for normalized/reject results.
521 %%
522 %% Examples:
523 %% ```
524 %% 1> klsn_rule:eval(10, number, #{}).
525 %% {valid, 10}
526 %% 2> klsn_rule:eval(<<"10">>, number, #{}).
527 %% {normalized, 10, {invalid, number, <<"10">>}}
528 %% 3> klsn_rule:normalize("1.5", number).
529 %% 1.5
530 %% 4> klsn_rule:eval(<<"nope">>, number, #{}).
531 %% {reject, {invalid, number, <<"nope">>}}
532 %% '''
533 %% @see validate/2
534 %% @see normalize/2
535 %% @see eval/3
536 -spec number_rule(input(), rule_param(), state()) -> result().
537 number_rule(Input, _Param, _State) ->
538 24 do(
539 fun is_number/1
540 , [
541 fun binary_to_integer/1
542 , fun binary_to_float/1
543 , fun list_to_integer/1
544 , fun list_to_float/1
545 ]
546 , Input
547 ) .
548
549 %% @doc
550 %% Validate a range rule used by validate/2, normalize/2, and eval/3.
551 %% Rule form: {range, range_(Rule)} where Rule is rule() and range_(Rule)
552 %% is one of:
553 %% {Rule, Op, Upper} | {Lower, Op, Rule} | {Lower, Op1, Rule, Op2, Upper}
554 %% Op/Op1/Op2 are `` '<' '' or `` '=<' ''. Lower/Upper are numbers.
555 %%
556 %% Result (eval/3):
557 %% - valid when Rule validates and the output satisfies the bound(s).
558 %% - normalized with the same Reason when Rule normalizes and the output
559 %% satisfies the bound(s).
560 %% - reject with {invalid_range, RangeOutput} when the bound check fails.
561 %% - reject with the underlying Reason when Rule rejects.
562 %%
563 %% normalize/2 returns Output for valid/normalized results and raises with
564 %% {klsn_rule, Reason} on reject. validate/2 returns ok only for valid results.
565 %%
566 %% Examples:
567 %% ```
568 %% 1> klsn_rule:eval(<<"10">>, {range, {integer, '=<', 10}}, #{}).
569 %% 2> klsn_rule:eval(5, {range, {0, '=<', integer, '<', 10}}, #{}).
570 %% '''
571 %% @see validate/2
572 %% @see normalize/2
573 %% @see eval/3
574 -spec range_rule(input(), rule_param(), state()) -> result().
575 range_rule(Input, {Subject, Op, Upper}, State)
576 when (Op =:= '<' orelse Op =:= '=<'),
577 is_number(Upper),
578 is_number(Subject) =:= false ->
579 7 case eval(Input, Subject, State) of
580 {valid, Output} ->
581 4 case Op of
582 '<' when Output < Upper ->
583
:-(
valid;
584 '=<' when Output =< Upper ->
585 2 valid;
586 _ ->
587 2 {reject, {invalid_range, {Output, Op, Upper}}}
588 end;
589 {normalized, Output, Reason} ->
590 2 case Op of
591 '<' when Output < Upper ->
592
:-(
{normalized, Output, Reason};
593 '=<' when Output =< Upper ->
594 2 {normalized, Output, Reason};
595 _ ->
596
:-(
{reject, {invalid_range, {Output, Op, Upper}}}
597 end;
598 {reject, Reason} ->
599 1 {reject, Reason}
600 end;
601 range_rule(Input, {Lower, Op, Subject}, State)
602 when (Op =:= '<' orelse Op =:= '=<'),
603 is_number(Lower),
604 is_number(Subject) =:= false ->
605 9 case eval(Input, Subject, State) of
606 {valid, Output} ->
607 4 case Op of
608 '<' when Lower < Output ->
609 1 valid;
610 '=<' when Lower =< Output ->
611 1 valid;
612 _ ->
613 2 {reject, {invalid_range, {Lower, Op, Output}}}
614 end;
615 {normalized, Output, Reason} ->
616 2 case Op of
617 '<' when Lower < Output ->
618
:-(
{normalized, Output, Reason};
619 '=<' when Lower =< Output ->
620
:-(
{normalized, Output, Reason};
621 _ ->
622 2 {reject, {invalid_range, {Lower, Op, Output}}}
623 end;
624 {reject, Reason} ->
625 3 {reject, Reason}
626 end;
627 range_rule(Input, {Lower, Op1, Subject, Op2, Upper}, State)
628 when (Op1 =:= '<' orelse Op1 =:= '=<'),
629 (Op2 =:= '<' orelse Op2 =:= '=<'),
630 is_number(Lower),
631 is_number(Upper),
632 is_number(Subject) =:= false ->
633 4 case eval(Input, Subject, State) of
634 {valid, Output} ->
635 4 LowerOk = case Op1 of
636 '<' when Lower < Output ->
637
:-(
true;
638 '=<' when Lower =< Output ->
639 3 true;
640 _ ->
641 1 false
642 end,
643 4 UpperOk = case Op2 of
644 '<' when Output < Upper ->
645
:-(
true;
646 '=<' when Output =< Upper ->
647 3 true;
648 _ ->
649 1 false
650 end,
651 4 case LowerOk andalso UpperOk of
652 true ->
653 2 valid;
654 false ->
655 2 {reject, {invalid_range, {Lower, Op1, Output, Op2, Upper}}}
656 end;
657 {normalized, Output, Reason} ->
658
:-(
LowerOk = case Op1 of
659 '<' when Lower < Output ->
660
:-(
true;
661 '=<' when Lower =< Output ->
662
:-(
true;
663 _ ->
664
:-(
false
665 end,
666
:-(
UpperOk = case Op2 of
667 '<' when Output < Upper ->
668
:-(
true;
669 '=<' when Output =< Upper ->
670
:-(
true;
671 _ ->
672
:-(
false
673 end,
674
:-(
case LowerOk andalso UpperOk of
675 true ->
676
:-(
{normalized, Output, Reason};
677 false ->
678
:-(
{reject, {invalid_range, {Lower, Op1, Output, Op2, Upper}}}
679 end;
680 {reject, Reason} ->
681
:-(
{reject, Reason}
682 end;
683 range_rule(_, _, _State) ->
684 2 reject.
685
686 %% @doc
687 %% Resolve and evaluate a named rule alias declared via -klsn_rule_alias.
688 %%
689 %% Rule form: {alias, AliasRef} where AliasRef is {Module, Alias}.
690 %%
691 %% Result (eval/3):
692 %% - valid when the named rule validates.
693 %% - normalized when the named rule normalizes; reason is
694 %% {invalid_alias, AliasRef, Reason}.
695 %% - reject when the named rule rejects; reason is
696 %% {invalid_alias, AliasRef, Reason}.
697 %% - reject with {undefined_alias, AliasRef, Input} when missing.
698 %% - reject with {invalid, alias, Input} when AliasRef is malformed.
699 %%
700 %% Examples:
701 %% ```
702 %% 1> MyAlias = {my_mod, my_alias}.
703 %% 2> klsn_rule:eval(42, {alias, MyAlias}, #{}).
704 %% {valid, 42}
705 %% 3> klsn_rule:eval(<<"42">>, {alias, MyAlias}, #{}).
706 %% {normalized, 42, {invalid_alias, MyAlias, {invalid, integer, <<"42">>}}}
707 %% '''
708 %% @see lookup_alias/1
709 %% @see eval/3
710 -spec alias_rule(input(), rule_param(), state()) -> result().
711 alias_rule(Input, {Module, Alias}=AliasRef, State)
712 when is_atom(Module), is_atom(Alias) ->
713 976 alias_rule_eval_(Input, AliasRef, State);
714 alias_rule(_, _, _State) ->
715 1 reject.
716
717 -spec alias_rule_eval_(input(), alias_ref(), state()) -> result().
718 alias_rule_eval_(Input, AliasRef, State) ->
719 976 case lookup_alias(AliasRef) of
720 {value, Rule} ->
721 975 case eval(Input, Rule, State) of
722 {valid, Output} ->
723 131 {valid, Output};
724 {normalized, Output, Reason} ->
725 110 {normalized, Output, {invalid_alias, AliasRef, Reason}};
726 {reject, Reason} ->
727 734 {reject, {invalid_alias, AliasRef, Reason}}
728 end;
729 none ->
730 1 {reject, {undefined_alias, AliasRef, Input}}
731 end.
732
733 %% @doc
734 %% Evaluate a rule with a definitions map stored in the evaluation state.
735 %%
736 %% Rule form: {with_defs, {Definitions, Rule}} where Definitions is a map of
737 %% reference name to rule().
738 %%
739 %% Result (eval/3):
740 %% - valid/normalized/reject according to Rule evaluated with the updated state.
741 %%
742 %% Examples:
743 %% ```
744 %% 1> Defs = #{<<"n">> => integer}.
745 %% 2> klsn_rule:eval(1, {with_defs, {Defs, {ref, <<"n">>}}}, #{}).
746 %% {valid, 1}
747 %% '''
748 %% @see ref_rule/3
749 %% @see eval/3
750 -spec with_defs_rule(input(), rule_param(), state()) -> result().
751 with_defs_rule(Input, {Defs, Rule}, State) when is_map(Defs) ->
752 5 Defs0 = klsn_map:get([?MODULE, definitions], State, #{}),
753 5 State1 = klsn_map:upsert([?MODULE, definitions], maps:merge(Defs0, Defs), State),
754 5 eval(Input, Rule, State1);
755 with_defs_rule(_, _, _State) ->
756
:-(
reject.
757
758 %% @doc
759 %% Resolve and evaluate a rule from the current definition set.
760 %%
761 %% Rule form: {ref, RefName} where RefName is a binary name that maps to a rule
762 %% stored by {@link with_defs_rule/3}.
763 %%
764 %% Result (eval/3):
765 %% - valid when the referenced rule validates.
766 %% - normalized when the referenced rule normalizes; reason is
767 %% {invalid_ref, RefName, Reason}.
768 %% - reject when the referenced rule rejects; reason is
769 %% {invalid_ref, RefName, Reason}.
770 %% - reject with {undefined_ref, RefName, Input} when missing.
771 %%
772 %% Examples:
773 %% ```
774 %% 1> Defs = #{<<"n">> => integer}.
775 %% 2> klsn_rule:eval(<<"1">>, {with_defs, {Defs, {ref, <<"n">>}}}, #{}).
776 %% {normalized, 1, {invalid_ref, <<"n">>, {invalid, integer, <<"1">>}}}
777 %% '''
778 %% @see with_defs_rule/3
779 %% @see eval/3
780 -spec ref_rule(input(), rule_param(), state()) -> result().
781 ref_rule(Input, Ref, State) ->
782 8 case klsn_map:lookup([?MODULE, definitions, Ref], State) of
783 {value, Rule} ->
784 7 case eval(Input, Rule, State) of
785 {valid, Output} ->
786 6 {valid, Output};
787 {normalized, Output, Reason} ->
788 1 {normalized, Output, {invalid_ref, Ref, Reason}};
789 {reject, Reason} ->
790
:-(
{reject, {invalid_ref, Ref, Reason}}
791 end;
792 none ->
793 1 {reject, {undefined_ref, Ref, Input}}
794 end.
795
796 %% @doc
797 %% Validate timeout input for validate/2, normalize/2, and eval/3.
798 %% Rule form: timeout.
799 %%
800 %% Result (eval/3):
801 %% - valid when Input is infinity or a non-negative integer.
802 %% - normalized when Input is a list/binary that parses via
803 %% binary_to_integer/1 or list_to_integer/1, or the string "infinity".
804 %% - reject when Input cannot be converted.
805 %%
806 %% Reason (eval/3):
807 %% - {invalid, timeout, Input} on normalize or reject.
808 %%
809 %% normalize/2 returns the converted timeout (or original) or raises with
810 %% {klsn_rule, {invalid, timeout, Input}}. validate/2 returns ok only when the
811 %% input is already valid and raises on normalization or reject.
812 %%
813 %% Examples:
814 %% ```
815 %% 1> klsn_rule:eval(infinity, timeout, #{}).
816 %% 2> klsn_rule:eval(0, timeout, #{}).
817 %% 3> klsn_rule:eval(<<"15">>, timeout, #{}).
818 %% 4> klsn_rule:normalize("15", timeout).
819 %% 5> klsn_rule:eval(foo, timeout, #{}).
820 %% '''
821 %% @see validate/2
822 %% @see normalize/2
823 %% @see eval/3
824 -spec timeout_rule(input(), rule_param(), state()) -> result().
825 timeout_rule(Input, _Param, State) ->
826 8 Rule = {any_of, [{enum, [infinity]}, {range, {0, '=<', integer}}]},
827 8 case eval(Input, Rule, State) of
828 {valid, _Output} ->
829 2 valid;
830 {normalized, Output, _Reason} ->
831 2 {normalized, Output};
832 {reject, _Reason} ->
833 4 reject
834 end.
835
836 %% @doc
837 %% Validate binary strings used by validate/2, normalize/2, and eval/3.
838 %% Rule form: binstr.
839 %%
840 %% Accepts binaries as-is (klsn:binstr()); otherwise converts integers,
841 %% floats, atoms, and iolists via klsn_binstr:from_any/1.
842 %%
843 %% Result:
844 %% - valid when Input is already a binary.
845 %% - normalized when Input converts to a binary; reason is {invalid, binstr, Input}.
846 %% - reject when conversion fails; reason is {invalid, binstr, Input}.
847 %%
848 %% {@link normalize/2} returns the binary (original or converted) or raises
849 %% error({klsn_rule, {invalid, binstr, Input}}). {@link validate/2} returns
850 %% ok only when the input is already a binary, otherwise it raises with
851 %% the same reason.
852 %%
853 %% Examples:
854 %% ```
855 %% 1> klsn_rule:eval(<<"ok">>, binstr, #{}).
856 %% 2> klsn_rule:eval(42, binstr, #{}).
857 %% 3> klsn_rule:normalize([<<"a">>, "b"], binstr).
858 %% 4> klsn_rule:eval(#{}, binstr, #{}).
859 %% '''
860 %% @see validate/2
861 %% @see normalize/2
862 %% @see eval/3
863 -spec binstr_rule(input(), rule_param(), state()) -> result().
864 binstr_rule(Input, _Param, _State) ->
865 27 do(
866 [fun klsn_binstr:from_any/1]
867 , Input
868 ) .
869
870 %% @doc
871 %% Validate atoms used by validate/2, normalize/2, and eval/3.
872 %% Rule form: atom.
873 %% Accepts atoms; otherwise converts input via klsn_binstr:from_any/1 and
874 %% binary_to_existing_atom/1 (existing atoms only).
875 %%
876 %% Result:
877 %% - valid when Input is an atom.
878 %% - normalized when Input converts to an existing atom;
879 %% reason is {invalid, atom, Input}.
880 %% - reject when Input cannot be converted to an existing atom;
881 %% reason is {invalid, atom, Input}.
882 %%
883 %% Examples:
884 %% ```
885 %% 1> klsn_rule:validate(ok, atom).
886 %% 2> klsn_rule:normalize(<<"ok">>, atom).
887 %% 3> klsn_rule:eval(<<"not_an_atom">>, atom, #{}).
888 %% '''
889 %% @see validate/2
890 %% @see normalize/2
891 %% @see eval/3
892 -spec atom_rule(input(), rule_param(), state()) -> result().
893 atom_rule(Input, _Param, _State) ->
894 15 do(
895 fun is_atom/1
896 , [fun(I) ->
897 6 binary_to_existing_atom(klsn_binstr:from_any(I))
898 end]
899 , Input
900 ) .
901
902 %% @doc
903 %% Validate enums used by validate/2, normalize/2, and eval/3.
904 %% Rule form: {enum, [atom()]}.
905 %% Matching compares klsn_binstr:from_any/1 for the input and each allowed enum.
906 %% Exact =:= matches are valid; otherwise the input normalizes to the matched enum.
907 %%
908 %% Result (eval/3):
909 %% - valid when Input matches an allowed enum exactly (Input =:= Enum).
910 %% - normalized when Input matches by binary conversion but is not =:=;
911 %% reason is {invalid, enum, Input}.
912 %% - reject when no allowed enum matches; reason is {invalid_enum, AllowedEnums, Input}.
913 %% - reject when AllowedEnums is not a list; reason is {invalid, enum, Input}.
914 %%
915 %% normalize/2 returns the matched enum and discards the reason; use eval/3 to
916 %% inspect the normalization reason.
917 %%
918 %% Examples:
919 %% ```
920 %% 1> klsn_rule:eval(foo, {enum, [foo, bar]}, #{}).
921 %% 2> klsn_rule:eval(<<"foo">>, {enum, [foo, bar]}, #{}).
922 %% 3> klsn_rule:eval(baz, {enum, [foo, bar]}, #{}).
923 %% 4> klsn_rule:normalize(<<"bar">>, {enum, [foo, bar]}).
924 %% 5> klsn_rule:validate(baz, {enum, [foo, bar]}).
925 %% '''
926 %% @see validate/2
927 %% @see normalize/2
928 %% @see eval/3
929 -spec enum_rule(input(), rule_param(), state()) -> result().
930 enum_rule(Input, AllowedEnums, _State) when is_list(AllowedEnums) ->
931 279 InputBinary = try klsn_binstr:from_any(Input) catch
932 _:_ ->
933 6 error
934 end,
935 279 MaybeEnum = lists:search(fun(E) ->
936 368 try
937 368 InputBinary =:= klsn_binstr:from_any(E)
938 catch _:_ ->
939 1 false
940 end
941 end, AllowedEnums),
942 279 case MaybeEnum of
943 {value, Enum} when Input =:= Enum ->
944 23 valid;
945 {value, Enum} ->
946 32 {normalized, Enum};
947 _ ->
948 224 {reject, {invalid_enum, AllowedEnums, Input}}
949 end;
950 enum_rule(_, _, _State) ->
951 1 reject.
952
953 %% @doc
954 %% Validate against a list of rules for validate/2, normalize/2, and eval/3.
955 %% Rule form: {any_of, [rule()]}.
956 %%
957 %% Result (eval/3):
958 %% - valid when any rule validates (short-circuit).
959 %% - normalized when no rule validates and at least one normalizes; output is
960 %% from the first normalized rule; reason is {any_of, [reason()]}.
961 %% - reject when no rule validates or normalizes; reason is {any_of, [reason()]}.
962 %% - valid when the rule list is empty.
963 %%
964 %% Reasons are collected from each attempted rule (normalized or reject) in
965 %% rule order.
966 %%
967 %% normalize/2 returns the first normalized output when present; validate/2
968 %% accepts only the valid case and errors otherwise.
969 %%
970 %% Examples:
971 %% ```
972 %% 1> klsn_rule:eval(3, {any_of, [integer, {exact, ok}]}, #{}).
973 %% {valid,3}
974 %% 2> klsn_rule:eval(<<"10">>, {any_of, [integer, {exact, ok}]}, #{}).
975 %% {normalized,10,{any_of,[{invalid,integer,<<"10">>},{invalid_exact,ok,<<"10">>}]}}
976 %% 3> klsn_rule:eval(<<"nope">>, {any_of, [integer, {exact, ok}]}, #{}).
977 %% {reject,{any_of,[{invalid,integer,<<"nope">>},{invalid_exact,ok,<<"nope">>}]}}
978 %% 4> klsn_rule:normalize(<<"10">>, {any_of, [integer, {exact, ok}]}).
979 %% 10
980 %% 5> klsn_rule:validate(3, {any_of, [integer, {exact, ok}]}).
981 %% ok
982 %% '''
983 %% @see validate/2
984 %% @see normalize/2
985 %% @see eval/3
986 -spec any_of_rule(input(), rule_param(), state()) -> result().
987 any_of_rule(_Input, [], _State) ->
988 1 valid;
989 any_of_rule(Input, Rules, State) when is_list(Rules) ->
990 87 any_of_rule_(Input, Rules, none, [], State);
991 any_of_rule(_, _, _State) ->
992
:-(
reject.
993
994 -spec any_of_rule_(input(), [rule()], klsn:optnl(output()), [reason()], state()) -> result().
995 any_of_rule_(_Input, [], {value, Output}, ReasonsRev, _State) ->
996 40 {normalized, Output, {any_of, lists:reverse(ReasonsRev)}};
997 any_of_rule_(_Input, [], none, ReasonsRev, _State) ->
998 5 {reject, {any_of, lists:reverse(ReasonsRev)}};
999 any_of_rule_(Input, [Rule|T], MaybeOutput0, ReasonsRev0, State) ->
1000 879 case eval(Input, Rule, State) of
1001 {valid, _} ->
1002 42 valid;
1003 {normalized, Output, Reason} ->
1004 76 MaybeOutput = case MaybeOutput0 of
1005 none ->
1006 41 {value, Output};
1007 _ ->
1008 35 MaybeOutput0
1009 end,
1010 76 any_of_rule_(Input, T, MaybeOutput, [Reason|ReasonsRev0], State);
1011 {reject, Reason} ->
1012 761 any_of_rule_(Input, T, MaybeOutput0, [Reason|ReasonsRev0], State)
1013 end.
1014
1015 %% @doc
1016 %% Apply all Rules; used by validate/2, normalize/2, and eval/3.
1017 %%
1018 %% Rule form: {all_of, [rule()]}.
1019 %% Each rule is evaluated against the same Input in list order.
1020 %%
1021 %% Result (eval/3):
1022 %% - valid when every rule returns valid (or Rules is empty).
1023 %% - normalized when no rule rejects and at least one rule normalizes; output is
1024 %% the first valid output if present, otherwise the first normalized output;
1025 %% reason is {all_of, [reason()]} from the normalizing rules in list order.
1026 %% - reject when any rule rejects; reason is {all_of, [reason()]} from the
1027 %% rejecting rules in list order (normalization reasons are discarded).
1028 %%
1029 %% normalize/2 returns the output and drops the reason; validate/2 errors on
1030 %% normalized or reject results.
1031 %%
1032 %% Examples:
1033 %% ```
1034 %% 1> klsn_rule:eval(5, {all_of, [integer, {range, {0, '=<', integer, '<', 10}}]}, #{}).
1035 %% {valid, 5}
1036 %% 2> klsn_rule:eval(<<"5">>, {all_of, [integer, {range, {0, '=<', integer, '<', 10}}]}, #{}).
1037 %% {normalized, 5, {all_of, [{invalid, integer, <<"5">>}, {invalid, integer, <<"5">>}]}}
1038 %% 3> klsn_rule:eval(20, {all_of, [integer, {range, {0, '=<', integer, '<', 10}}]}, #{}).
1039 %% {reject, {all_of, [{invalid_range, {0, '=<', 20, '<', 10}}]}}
1040 %% '''
1041 %% @see validate/2
1042 %% @see normalize/2
1043 %% @see eval/3
1044 -spec all_of_rule(input(), rule_param(), state()) -> result().
1045 all_of_rule(_Input, [], _State) ->
1046 1 valid;
1047 all_of_rule(Input, Rules, State) when is_list(Rules) ->
1048 6 {MaybeValidOutput, MaybeNormOutput, NormReasonsRev, RejectReasonsRev} =
1049 all_of_rule_(Input, Rules, none, none, [], [], State),
1050 6 case RejectReasonsRev of
1051 [] ->
1052 5 case NormReasonsRev of
1053 [] ->
1054 1 valid;
1055 _ ->
1056 4 Output = case MaybeValidOutput of
1057 {value, ValidOutput} ->
1058 2 ValidOutput;
1059 none ->
1060 2 {value, NormOutput} = MaybeNormOutput,
1061 2 NormOutput
1062 end,
1063 4 {normalized, Output, {all_of, lists:reverse(NormReasonsRev)}}
1064 end;
1065 _ ->
1066 1 {reject, {all_of, lists:reverse(RejectReasonsRev)}}
1067 end;
1068 all_of_rule(_, _, _State) ->
1069
:-(
reject.
1070
1071 -spec all_of_rule_(
1072 input()
1073 , [rule()]
1074 , klsn:optnl(output())
1075 , klsn:optnl(output())
1076 , [reason()]
1077 , [reason()]
1078 , state()
1079 ) -> {klsn:optnl(output()), klsn:optnl(output()), [reason()], [reason()]}.
1080 all_of_rule_(_Input, [], MaybeValidOutput, MaybeNormOutput, NormReasonsRev, RejectReasonsRev, _State) ->
1081 6 {MaybeValidOutput, MaybeNormOutput, NormReasonsRev, RejectReasonsRev};
1082 all_of_rule_(Input, [Rule|T], MaybeValidOutput0, MaybeNormOutput0, NormReasonsRev0, RejectReasonsRev0, State) ->
1083 12 case eval(Input, Rule, State) of
1084 {valid, Output} ->
1085 4 MaybeValidOutput = case MaybeValidOutput0 of
1086 none ->
1087 3 {value, Output};
1088 _ ->
1089 1 MaybeValidOutput0
1090 end,
1091 4 all_of_rule_(Input, T, MaybeValidOutput, MaybeNormOutput0, NormReasonsRev0, RejectReasonsRev0, State);
1092 {normalized, Output, Reason} ->
1093 7 MaybeNormOutput = case MaybeNormOutput0 of
1094 none ->
1095 5 {value, Output};
1096 _ ->
1097 2 MaybeNormOutput0
1098 end,
1099 7 all_of_rule_(Input, T, MaybeValidOutput0, MaybeNormOutput, [Reason|NormReasonsRev0], RejectReasonsRev0, State);
1100 {reject, Reason} ->
1101 1 all_of_rule_(Input, T, MaybeValidOutput0, MaybeNormOutput0, NormReasonsRev0, [Reason|RejectReasonsRev0], State)
1102 end.
1103
1104 %% @doc
1105 %% Apply rules left-to-right, feeding each output into the next rule.
1106 %% Rule form: {foldl, [rule()]}.
1107 %%
1108 %% Result (eval/3):
1109 %% - valid when every rule returns valid (output stays the original input).
1110 %% - normalized when the first rule that normalizes succeeds; output is from the
1111 %% last rule and the reason is from the first normalization. Later
1112 %% normalizations keep the original reason.
1113 %% - reject with the Reason from the first rule that rejects.
1114 %%
1115 %% normalize/2 returns the last output; validate/2 raises on normalize/reject.
1116 %%
1117 %% Examples:
1118 %% ```
1119 %% 1> klsn_rule:eval(ok, {foldl, [atom, {exact, ok}]}, #{}).
1120 %% {valid, ok}
1121 %% 2> klsn_rule:eval("ok", {foldl, [atom, {exact, ok}]}, #{}).
1122 %% {normalized, ok, {invalid, atom, "ok"}}
1123 %% 3> klsn_rule:eval("ok", {foldl, [atom, {exact, nope}]}, #{}).
1124 %% {reject, {invalid_exact, nope, ok}}
1125 %% '''
1126 %% @see validate/2
1127 %% @see normalize/2
1128 %% @see eval/3
1129 -spec foldl_rule(input(), rule_param(), state()) -> result().
1130 foldl_rule(Input, Rules, State) when is_list(Rules) ->
1131 6 lists:foldl(fun(Rule, Acc) ->
1132 9 case Acc of
1133 {reject, _} ->
1134
:-(
Acc;
1135 {valid, Value} ->
1136 6 eval(Value, Rule, State);
1137 {normalized, Value, Reason} ->
1138 3 case eval(Value, Rule, State) of
1139 {valid, Output} ->
1140 1 {normalized, Output, Reason};
1141 {normalized, Output, _Reason} ->
1142 1 {normalized, Output, Reason};
1143 {reject, RejectReason} ->
1144 1 {reject, RejectReason}
1145 end
1146 end
1147 end, {valid, Input}, Rules);
1148 foldl_rule(_, _, _State) ->
1149 1 reject.
1150
1151 %% @doc
1152 %% Optional rule wrapper; use as {optnl, Rule} in rule() with validate/2,
1153 %% normalize/2, or eval/3. Output is klsn:optnl(Output)
1154 %% ({value, Output} or none).
1155 %%
1156 %% Input handling:
1157 %% - none is valid and stays none.
1158 %% - {value, V} validates V with Rule.
1159 %% - null | nil | undefined | false | [] | error normalize to none.
1160 %% - {ok, V} | {true, V} | [V] | binary() | number() are treated as values,
1161 %% evaluated with Rule, and wrapped as {value, Output}.
1162 %%
1163 %% Result conditions (eval/3):
1164 %% - valid when Input is none or {value, V} and Rule validates V.
1165 %% - normalized when a wrapper/marker is converted or Rule normalizes V.
1166 %% - reject when Input is unrecognized or Rule rejects V.
1167 %%
1168 %% Reasons (eval/3):
1169 %% - {invalid, optnl, Input} when optnl_rule/3 normalizes without a rule reason
1170 %% (markers/wrappers that validate) or when Input is unrecognized.
1171 %% - {invalid_optnl_value, Reason} when Rule normalizes or rejects V.
1172 %%
1173 %% validate/2 only accepts valid; normalize/2 returns the output and drops
1174 %% the reason.
1175 %%
1176 %% Examples:
1177 %% ```
1178 %% 1> klsn_rule:eval(none, {optnl, integer}, #{}).
1179 %% {valid, none}
1180 %% 2> klsn_rule:eval(null, {optnl, integer}, #{}).
1181 %% {normalized, none, {invalid, optnl, null}}
1182 %% 3> klsn_rule:eval({ok, 3}, {optnl, integer}, #{}).
1183 %% {normalized, {value, 3}, {invalid, optnl, {ok, 3}}}
1184 %% 4> klsn_rule:eval({value, 3}, {optnl, integer}, #{}).
1185 %% {valid, {value, 3}}
1186 %% 5> klsn_rule:eval({value, foo}, {optnl, integer}, #{}).
1187 %% {reject, {invalid_optnl_value, {invalid, integer, foo}}}
1188 %% 6> klsn_rule:normalize({ok, 3}, {optnl, integer}).
1189 %% {value, 3}
1190 %% '''
1191 %% @see validate/2
1192 %% @see normalize/2
1193 %% @see eval/3
1194 -spec optnl_rule(input(), rule_param(), state()) -> result().
1195 optnl_rule(Input, Rule, State) ->
1196 30 case Input of
1197 {value, Value0} ->
1198 7 case eval(Value0, Rule, State) of
1199 {valid, _} ->
1200 2 valid;
1201 {normalized, Value, Reason} ->
1202 3 {normalized, {value, Value}, {invalid_optnl_value, Reason}};
1203 {reject, Reason} ->
1204 2 {reject, {invalid_optnl_value, Reason}}
1205 end;
1206 none ->
1207 1 valid;
1208 _ ->
1209 22 optnl_rule_(Input, Rule, State)
1210 end.
1211 optnl_rule_(Input, Rule, State) ->
1212 22 InputResult = eval(Input, Rule, State),
1213 22 NoneCanary = case eval(none, Rule, State) of
1214 {valid, _} ->
1215 %% We can't trust Rule to tell if it's optnl or raw value
1216 1 valid;
1217 {normalized, _, _} ->
1218 %% We can't trust normalized result, but we can trust valid as raw value.
1219 4 normalized;
1220 {reject, _} ->
1221 %% We can trust valid and normalized result as raw value
1222 17 reject
1223 end,
1224 22 NoneAlt = case eval(Input, {enum, [undefined, null, nil, error, false]}, State) of
1225 {valid, _} ->
1226 5 valid;
1227 {normalized, _, _} ->
1228 1 normalized;
1229 {reject, _} ->
1230 16 reject
1231 end,
1232 22 ValueAltResult = case Input of
1233 {_, Value0} ->
1234 5 {value, eval(Value0, Rule, State)};
1235 [Value0] ->
1236 1 {value, eval(Value0, Rule, State)};
1237 _ ->
1238 16 none
1239 end,
1240 22 case {NoneCanary, NoneAlt, InputResult, ValueAltResult, Input} of
1241 {valid, _, _, _, _} ->
1242 1 reject;
1243 {_, valid, _, _, _} ->
1244 5 {normalized, none};
1245 {reject, normalized, _, _, _} ->
1246 1 {normalized, none};
1247 {_, _, {reject, _}, {value, {valid, Value}}, _} ->
1248 4 {normalized, {value, Value}};
1249 {_, _, {reject, _}, {value, {normalized, Value, _}}, _} ->
1250 1 {normalized, {value, Value}};
1251 {_, _, {normalized, _, _}, {value, {valid, Value}}, _} ->
1252 1 {normalized, {value, Value}};
1253 {_, _, {reject, _}, _, []} ->
1254 1 {normalized, none};
1255 {_, _, {normalized, [], _}, _, _} ->
1256 1 reject;
1257 {_, _, {normalized, _, _}, _, []} ->
1258 1 {normalized, none};
1259 {reject, _, {valid, Value}, _, _} ->
1260 2 {normalized, {value, Value}};
1261 {reject, _, {normalized, Value, _}, _, _} ->
1262 1 {normalized, {value, Value}};
1263 {normalized, _, {valid, Value}, _, _} ->
1264 1 {normalized, {value, Value}};
1265 _ ->
1266 2 reject
1267 end.
1268
1269 %% @doc
1270 %% Validate a nullable rule used by validate/2, normalize/2, and eval/3.
1271 %% Rule form: {nullable, Rule} where Rule is a rule().
1272 %%
1273 %% Result (eval/3):
1274 %% - valid when Input is null.
1275 %% - normalized to null when Input is none.
1276 %% - valid when Input is Value and Rule validates.
1277 %% - normalized when Input is {value, Value} and Rule validates.
1278 %% - normalized with {invalid_nullable_value, Reason} when Rule normalizes.
1279 %% - reject with {invalid_nullable_value, Reason} when Rule rejects.
1280 %%
1281 %% Reasons (eval/3):
1282 %% - {invalid, nullable, Input} when nullable_rule/3 normalizes without a rule
1283 %% reason (none or {value, Value} when Rule validates).
1284 %% - {invalid_nullable_value, Reason} when Rule normalizes or rejects Value.
1285 %%
1286 %% Examples:
1287 %% ```
1288 %% 1> klsn_rule:eval(null, {nullable, integer}, #{}).
1289 %% 2> klsn_rule:eval(none, {nullable, integer}, #{}).
1290 %% 3> klsn_rule:eval(<<"12">>, {nullable, integer}, #{}).
1291 %% 4> klsn_rule:eval({value, <<"12">>}, {nullable, integer}, #{}).
1292 %% '''
1293 %% @see validate/2
1294 %% @see normalize/2
1295 %% @see eval/3
1296 -spec nullable_rule(input(), rule_param(), state()) -> result().
1297 nullable_rule(null, _Rule, _State) ->
1298 4 valid;
1299 nullable_rule(none, _Rule, _State) ->
1300 4 {normalized, null};
1301 nullable_rule({value, Value}, Rule, State) ->
1302 8 nullable_eval_value_(Value, Rule, normalized, State);
1303 nullable_rule(Value, Rule, State) ->
1304 5 nullable_eval_value_(Value, Rule, valid, State).
1305
1306 %% @doc
1307 %% Enforce strict evaluation when used as {strict, Rule}.
1308 %% Rule form: {strict, Rule} where Rule :: rule().
1309 %%
1310 %% Result (eval/3):
1311 %% - valid when Rule returns {valid, Output}.
1312 %% - normalized is never returned; when Rule normalizes with
1313 %% {normalized, Output, Reason}, this rule rejects with {strict, Reason}.
1314 %% - reject when Rule rejects; the Reason passes through unchanged.
1315 %%
1316 %% validate/2 and normalize/2 only succeed on valid. When strict rejects,
1317 %% they raise error({klsn_rule, Reason}) where Reason is the reject reason above.
1318 %%
1319 %% Examples:
1320 %% ```
1321 %% 1> klsn_rule:eval(42, {strict, integer}, #{}).
1322 %% 2> klsn_rule:eval(<<"42">>, {strict, integer}, #{}).
1323 %% 3> klsn_rule:normalize(42, {strict, integer}).
1324 %% '''
1325 %% @see validate/2
1326 %% @see normalize/2
1327 %% @see eval/3
1328 -spec strict_rule(input(), rule_param(), state()) -> result().
1329 strict_rule(Input, Rule, State) ->
1330 4 case eval(Input, Rule, State) of
1331 {valid, Output} ->
1332 1 {valid, Output};
1333 {normalized, _Output, Reason} ->
1334 2 {reject, {strict, Reason}};
1335 {reject, Reason} ->
1336 1 {reject, Reason}
1337 end.
1338
1339 -spec nullable_eval_value_(input(), rule(), valid | normalized, state()) -> result().
1340 nullable_eval_value_(Value, Rule, Validity, State) ->
1341 13 case eval(Value, Rule, State) of
1342 {valid, Output} ->
1343 4 case Validity of
1344 valid ->
1345
:-(
{valid, Output};
1346 normalized ->
1347 4 {normalized, Output}
1348 end;
1349 {normalized, Output, Reason} ->
1350 5 {normalized, Output, {invalid_nullable_value, Reason}};
1351 {reject, Reason} ->
1352 4 {reject, {invalid_nullable_value, Reason}}
1353 end.
1354
1355 %% @doc
1356 %% Validate list inputs by applying an element rule to each entry.
1357 %%
1358 %% Rule form: `{list, ElementRule}' where `ElementRule :: rule()'.
1359 %%
1360 %% Result (eval/3):
1361 %% - valid when Input is a list and every element validates (no normalization).
1362 %% - normalized when Input is a list, no element rejects, and the first
1363 %% normalized element is at Index (1-based); output is a list of element
1364 %% outputs; reason is {invalid_list_element, Index, Reason}.
1365 %% - reject when Input is not a list; reason is {invalid, list, Input}.
1366 %% - reject when the first rejecting element is at Index; reason is
1367 %% {invalid_list_element, Index, Reason}.
1368 %%
1369 %% normalize/2 returns the output list on valid/normalized and raises on reject.
1370 %% validate/2 returns ok only on valid; it raises on normalized/reject.
1371 %%
1372 %% Examples:
1373 %% ```
1374 %% 1> klsn_rule:eval([1, 2], {list, integer}, #{}).
1375 %% {valid, [1, 2]}
1376 %% 2> klsn_rule:eval([<<"1">>, <<"2">>], {list, integer}, #{}).
1377 %% {normalized, [1, 2], {invalid_list_element, 1, {invalid, integer, <<"1">>}}}
1378 %% 3> klsn_rule:eval([1, <<"bad">>], {list, integer}, #{}).
1379 %% {reject, {invalid_list_element, 2, {invalid, integer, <<"bad">>}}}
1380 %% 4> klsn_rule:eval(<<"1">>, {list, integer}, #{}).
1381 %% {reject, {invalid, list, <<"1">>}}
1382 %% 5> klsn_rule:normalize([<<"1">>], {list, integer}).
1383 %% [1]
1384 %% 6> klsn_rule:validate([<<"1">>], {list, integer}).
1385 %% ** exception error: {klsn_rule,{invalid_list_element,1,{invalid,integer,<<"1">>}}}
1386 %% '''
1387 %% @see validate/2
1388 %% @see normalize/2
1389 %% @see eval/3
1390 -spec list_rule(input(), rule_param(), state()) -> result().
1391 list_rule(Input, ElementRule, State) when is_list(Input) ->
1392 14 List0 = lists:map(fun(Elem) ->
1393 22 eval(Elem, ElementRule, State)
1394 end, Input),
1395 14 List = lists:zip(lists:seq(1, length(List0)), List0),
1396 14 MaybeReject = lists:search(fun
1397 ({_I, {reject, _}})->
1398 1 true;
1399 (_) ->
1400 21 false
1401 end, List),
1402 14 case MaybeReject of
1403 {value, {I, {reject, Reason}}} ->
1404 1 {reject, {invalid_list_element, I, Reason}};
1405 _ ->
1406 13 MaybeNormalized = lists:search(fun
1407 ({_I, {normalized, _, _}})->
1408 7 true;
1409 (_) ->
1410 8 false
1411 end, List),
1412 13 case MaybeNormalized of
1413 {value, {I, {normalized, _, Reason}}} ->
1414 7 Output = lists:map(fun
1415 ({_I, {valid, ElemOutput}}) ->
1416 1 ElemOutput;
1417 ({_I, {normalized, ElemOutput, _}}) ->
1418 12 ElemOutput
1419 end, List),
1420 7 {normalized, Output, {invalid_list_element, I, Reason}};
1421 _ ->
1422 6 valid
1423 end
1424 end;
1425 list_rule(_, _, _State) ->
1426 43 reject.
1427
1428 %% @doc
1429 %% Validate tuple inputs against element rules.
1430 %%
1431 %% Rule form: {tuple, Rules} where Rules is a list or tuple of rule().
1432 %%
1433 %% Result (eval/3):
1434 %% - valid when Input is a tuple, arity matches Rules, and all elements validate.
1435 %% - normalized when Input is a tuple, no element rejects, and the first
1436 %% normalized element is at Index; output is a tuple of element outputs; reason
1437 %% is {invalid_tuple_element, Index, Reason}.
1438 %% - reject when Input is not a tuple; reason is {invalid, tuple, Input}.
1439 %% - reject when arity mismatches; reason is {invalid_tuple_size, Expected, Input}.
1440 %% - reject when the first element to reject is at Index; reason is
1441 %% {invalid_tuple_element, Index, Reason}.
1442 %%
1443 %% normalize/2 returns the tuple output on valid/normalized and raises on reject.
1444 %% validate/2 returns ok only for valid results (normalized counts as invalid).
1445 %%
1446 %% Examples:
1447 %% ```
1448 %% 1> klsn_rule:eval({1, ok}, {tuple, [integer, atom]}, #{}).
1449 %% {valid, {1, ok}}
1450 %% 2> klsn_rule:eval({"1", ok}, {tuple, [integer, atom]}, #{}).
1451 %% {normalized, {1, ok}, {invalid_tuple_element, 1, {invalid, integer, "1"}}}
1452 %% 3> klsn_rule:eval({1, ok}, {tuple, {integer, atom}}, #{}).
1453 %% {valid, {1, ok}}
1454 %% 4> klsn_rule:eval({1}, {tuple, [integer, atom]}, #{}).
1455 %% {reject, {invalid_tuple_size, 2, {1}}}
1456 %% 5> klsn_rule:eval([1, ok], {tuple, [integer, atom]}, #{}).
1457 %% {reject, {invalid, tuple, [1, ok]}}
1458 %% 6> klsn_rule:normalize({ok, ok}, {tuple, [integer, atom]}).
1459 %% exception error: {klsn_rule,{invalid_tuple_element,1,{invalid,integer,ok}}}
1460 %% '''
1461 %% @see validate/2
1462 %% @see normalize/2
1463 %% @see eval/3
1464 -spec tuple_rule(input(), rule_param(), state()) -> result().
1465 tuple_rule(Input, Rules, State) when is_tuple(Input), is_tuple(Rules) ->
1466 6 tuple_rule(Input, tuple_to_list(Rules), State);
1467 tuple_rule(Input, Rules, _State) when is_tuple(Input), is_list(Rules), length(Rules) =/= tuple_size(Input) ->
1468 1 {reject, {invalid_tuple_size, length(Rules), Input}};
1469 tuple_rule(Input, Rules, State) when is_tuple(Input), is_list(Rules) ->
1470 9 InputList = tuple_to_list(Input),
1471 9 List0 = lists:map(fun({Rule, Elem}) ->
1472 16 eval(Elem, Rule, State)
1473 end, lists:zip(Rules, InputList)),
1474 9 List = lists:zip(lists:seq(1, length(List0)), List0),
1475 9 MaybeReject = lists:search(fun
1476 ({_I, {reject, _}})->
1477 1 true;
1478 (_) ->
1479 15 false
1480 end, List),
1481 9 case MaybeReject of
1482 {value, {I, {reject, Reason}}} ->
1483 1 {reject, {invalid_tuple_element, I, Reason}};
1484 _ ->
1485 8 MaybeNormalized = lists:search(fun
1486 ({_I, {normalized, _, _}})->
1487 3 true;
1488 (_) ->
1489 10 false
1490 end, List),
1491 8 case MaybeNormalized of
1492 {value, {I, {normalized, _, Reason}}} ->
1493 3 Output = lists:map(fun
1494 ({_I, {valid, ElemOutput}}) ->
1495 2 ElemOutput;
1496 ({_I, {normalized, ElemOutput, _}}) ->
1497 3 ElemOutput
1498 end, List),
1499 3 {normalized, list_to_tuple(Output), {invalid_tuple_element, I, Reason}};
1500 _ ->
1501 5 valid
1502 end
1503 end;
1504 tuple_rule(_, _, _State) ->
1505 5 reject.
1506
1507 %% @doc
1508 %% Validate map input for validate/2, normalize/2, and eval/3.
1509 %% Rule form: {map, {KeyRule, ValueRule}} where KeyRule and ValueRule are rule().
1510 %%
1511 %% Result (eval/3):
1512 %% - valid when Input is a map and all keys/values validate without normalization.
1513 %% - normalized when Input is a map, no key/value rejects, and at least one
1514 %% key or value normalizes; output is a map with normalized keys/values.
1515 %% - reject when Input is not a map, when a key or value rejects, when
1516 %% normalized keys collide, or when the rule parameter is not {KeyRule, ValueRule}.
1517 %%
1518 %% Reason (eval/3):
1519 %% - {invalid, map, Input} when Input is not a map or the rule parameter is invalid.
1520 %% - {invalid_map_key, Reason} when a key rejects or normalizes.
1521 %% - {invalid_map_value, Key, Reason} when a value rejects or normalizes
1522 %% (Key is the original input key).
1523 %% - {map_key_conflict, Key} when key normalization produces duplicates.
1524 %%
1525 %% normalize/2 returns the (possibly normalized) map and drops the reason.
1526 %% validate/2 returns ok only for valid results; it raises on normalized or reject.
1527 %%
1528 %% Examples:
1529 %% ```
1530 %% 1> klsn_rule:eval(#{1 => 2}, {map, {integer, integer}}, #{}).
1531 %% {valid, #{1 => 2}}
1532 %% 2> klsn_rule:eval(#{1 => "2"}, {map, {integer, integer}}, #{}).
1533 %% {normalized, #{1 => 2}, {invalid_map_value, 1, {invalid, integer, "2"}}}
1534 %% 3> klsn_rule:eval(#{"nope" => 1}, {map, {integer, integer}}, #{}).
1535 %% {reject, {invalid_map_key, {invalid, integer, "nope"}}}
1536 %% 4> klsn_rule:eval(#{"1" => a, 1 => b}, {map, {integer, term}}, #{}).
1537 %% {reject, {map_key_conflict, 1}}
1538 %% 5> klsn_rule:eval([1, 2], {map, {integer, integer}}, #{}).
1539 %% {reject, {invalid, map, [1, 2]}}
1540 %% 6> klsn_rule:normalize(#{"1" => "2"}, {map, {integer, integer}}).
1541 %% #{1 => 2}
1542 %% '''
1543 %% @see validate/2
1544 %% @see normalize/2
1545 %% @see eval/3
1546 -spec map_rule(input(), rule_param(), state()) -> result().
1547 map_rule(Input, {KeyRule, ValueRule}, State) when is_map(Input) ->
1548 13 List0 = lists:map(fun({Key, Value}) ->
1549 17 {Key, eval(Key, KeyRule, State), eval(Value, ValueRule, State)}
1550 end, maps:to_list(Input)),
1551 13 MaybeKeyReject = lists:search(fun
1552 ({_Key, {reject, _}, _}) ->
1553 1 true;
1554 (_) ->
1555 16 false
1556 end, List0),
1557 13 case MaybeKeyReject of
1558 {value, {_Key, {reject, Reason}, _}} ->
1559 1 {reject, {invalid_map_key, Reason}};
1560 _ ->
1561 12 MaybeValueReject = lists:search(fun
1562 ({_Key, _KeyRes, {reject, _}}) ->
1563 1 true;
1564 (_) ->
1565 15 false
1566 end, List0),
1567 12 case MaybeValueReject of
1568 {value, {Key, _KeyRes, {reject, Reason}}} ->
1569 1 {reject, {invalid_map_value, Key, Reason}};
1570 _ ->
1571 11 MaybeNormalized = lists:search(fun
1572 ({_Key, {normalized, _, _}, _}) ->
1573 4 true;
1574 ({_Key, _KeyRes, {normalized, _, _}}) ->
1575 6 true;
1576 (_) ->
1577 3 false
1578 end, List0),
1579 11 MaybeNormReason = case MaybeNormalized of
1580 {value, {_Key, {normalized, _, Reason}, _}} ->
1581 4 {value, {invalid_map_key, Reason}};
1582 {value, {Key, _KeyRes, {normalized, _, Reason}}} ->
1583 6 {value, {invalid_map_value, Key, Reason}};
1584 _ ->
1585 1 none
1586 end,
1587 11 case MaybeNormReason of
1588 {value, NormReason} ->
1589 10 OutputList = lists:map(fun({_Key0, KeyRes0, ValueRes0}) ->
1590 14 Key1 = case KeyRes0 of
1591 {valid, KeyOut} ->
1592 9 KeyOut;
1593 {normalized, KeyOut, _} ->
1594 5 KeyOut
1595 end,
1596 14 Value1 = case ValueRes0 of
1597 {valid, ValueOut} ->
1598 7 ValueOut;
1599 {normalized, ValueOut, _} ->
1600 7 ValueOut
1601 end,
1602 14 {Key1, Value1}
1603 end, List0),
1604 10 MaybeKeyConflict = lists:foldl(fun({Key1, _Value1}, Acc) ->
1605 14 case Acc of
1606 {value, _} ->
1607 1 Acc;
1608 Seen ->
1609 13 case maps:is_key(Key1, Seen) of
1610 true ->
1611 2 {value, Key1};
1612 false ->
1613 11 maps:put(Key1, true, Seen)
1614 end
1615 end
1616 end, #{}, OutputList),
1617 10 case MaybeKeyConflict of
1618 {value, Key1} ->
1619 2 {reject, {map_key_conflict, Key1}};
1620 _ ->
1621 8 {normalized, maps:from_list(OutputList), NormReason}
1622 end;
1623 _ ->
1624 1 valid
1625 end
1626 end
1627 end;
1628 map_rule(_, _, _State) ->
1629 1 reject.
1630
1631 %% @doc
1632 %% Validate struct maps against a field spec for use with validate/2, normalize/2,
1633 %% and eval/3.
1634 %%
1635 %% Rule form: {struct, #{Field => {required | optional, rule()}}}.
1636 %% Field must be an atom; Input must be a map. Keys are matched by comparing
1637 %% klsn_binstr:from_any(Key) and klsn_binstr:from_any(Field), so Field,
1638 %% `<<"field">>', and "field" are treated as the same field.
1639 %%
1640 %% Output normalization:
1641 %% - extra keys are dropped,
1642 %% - matched keys are normalized to their atom field names,
1643 %% - field values are normalized when their rule normalizes.
1644 %%
1645 %% When evaluated via {@link eval/3}:
1646 %% - valid when all required fields are present, no extra or duplicate keys
1647 %% exist, and all field rules return valid.
1648 %% - normalized when extra keys are removed, keys are normalized, or any field
1649 %% value normalizes. Reasons are:
1650 %% {invalid_struct_field, Key} for extra keys or key normalization, and
1651 %% {invalid_struct_value, Field, Reason} for value normalization.
1652 %% When multiple normalization causes exist, the reason reports extra keys
1653 %% first, then key normalization, then value normalization.
1654 %% - reject when required fields are missing, a field has duplicate keys, or a
1655 %% field rule rejects. Reasons are:
1656 %% {missing_required_field, Field},
1657 %% {struct_field_conflict, Field},
1658 %% {invalid_struct_value, Field, Reason}.
1659 %% - reject with {invalid, struct, Input} when Input or the spec is malformed.
1660 %%
1661 %% normalize/2 returns the output map and drops the reason.
1662 %% validate/2 raises error({klsn_rule, Reason}) on normalized or reject.
1663 %%
1664 %% Examples:
1665 %% ```
1666 %% 1> Rule = {struct, #{name => {required, binstr}, age => {optional, integer}}}.
1667 %% 2> klsn_rule:eval(#{name => <<"Ada">>, age => 32}, Rule, #{}).
1668 %% {valid, #{name => <<"Ada">>, age => 32}}
1669 %% 3> klsn_rule:eval(#{<<"name">> => <<"Ada">>, age => <<"42">>}, Rule, #{}).
1670 %% {normalized, #{name => <<"Ada">>, age => 42},
1671 %% {invalid_struct_field, <<"name">>}}
1672 %% 4> klsn_rule:normalize(#{<<"name">> => <<"Ada">>, extra => 1}, Rule).
1673 %% #{name => <<"Ada">>}
1674 %% 5> klsn_rule:eval(#{age => 32}, Rule, #{}).
1675 %% {reject, {missing_required_field, name}}
1676 %% 6> klsn_rule:eval(#{name => <<"Ada">>, <<"name">> => <<"Ada">>}, Rule, #{}).
1677 %% {reject, {struct_field_conflict, name}}
1678 %% '''
1679 %% @see validate/2
1680 %% @see normalize/2
1681 %% @see eval/3
1682 -spec struct_rule(input(), rule_param(), state()) -> result().
1683 struct_rule(Input, StructSpec, State) when is_map(Input), is_map(StructSpec) ->
1684 801 SpecList0 = maps:to_list(StructSpec),
1685 801 SpecList = lists:map(fun
1686 ({Field, {ReqOpt, Rule}}) when is_atom(Field), (ReqOpt =:= required orelse ReqOpt =:= optional) ->
1687 2761 {Field, {ReqOpt, Rule}};
1688 (_) ->
1689 1 error
1690 end, SpecList0),
1691 801 case lists:member(error, SpecList) of
1692 true ->
1693 1 reject;
1694 false ->
1695 800 BinToField = maps:from_list(lists:map(fun({Field, _}) ->
1696 2761 {klsn_binstr:from_any(Field), Field}
1697 end, SpecList)),
1698 800 InputList = maps:to_list(Input),
1699 800 {Matches0, ExtraKeysRev, KeyNormKeysRev} = lists:foldl(fun({Key, Value}, {MatchesAcc, ExtraAcc, NormAcc}) ->
1700 991 case maybe_binstr_from_any_(Key) of
1701 {value, KeyBin} ->
1702 990 case maps:find(KeyBin, BinToField) of
1703 {ok, Field} ->
1704 451 Existing = maps:get(Field, MatchesAcc, []),
1705 451 MatchesAcc1 = maps:put(Field, [{Key, Value}|Existing], MatchesAcc),
1706 451 NormAcc1 = case Key =:= Field of
1707 true ->
1708 141 NormAcc;
1709 false ->
1710 310 [Key|NormAcc]
1711 end,
1712 451 {MatchesAcc1, ExtraAcc, NormAcc1};
1713 error ->
1714 539 {MatchesAcc, [Key|ExtraAcc], NormAcc}
1715 end;
1716 none ->
1717 1 {MatchesAcc, [Key|ExtraAcc], NormAcc}
1718 end
1719 end, {#{}, [], []}, InputList),
1720 800 ExtraKeys = lists:reverse(ExtraKeysRev),
1721 800 KeyNormKeys = lists:reverse(KeyNormKeysRev),
1722 800 MaybeConflict = lists:search(fun({Field, _}) ->
1723 2761 case maps:get(Field, Matches0, []) of
1724 2312 [] -> false;
1725 447 [_] -> false;
1726 2 [_|_] -> true
1727 end
1728 end, SpecList),
1729 800 case MaybeConflict of
1730 {value, {Field, _}} ->
1731 2 {reject, {struct_field_conflict, Field}};
1732 _ ->
1733 798 MaybeMissingRequired = lists:search(fun
1734 ({Field, {required, _}}) ->
1735 763 maps:get(Field, Matches0, []) =:= [];
1736 (_) ->
1737 1702 false
1738 end, SpecList),
1739 798 case MaybeMissingRequired of
1740 {value, {Field, _}} ->
1741 380 {reject, {missing_required_field, Field}};
1742 _ ->
1743 418 case struct_eval_fields_(SpecList, Matches0, #{}, none, State) of
1744 {reject, Reason} ->
1745 242 {reject, Reason};
1746 {ok, Output, MaybeValueNormReason} ->
1747 176 IsNormalized = (ExtraKeys =/= []) orelse (KeyNormKeys =/= []) orelse (MaybeValueNormReason =/= none),
1748 176 case IsNormalized of
1749 false ->
1750 97 valid;
1751 true ->
1752 79 NormReason = case ExtraKeys of
1753 [ExtraKey|_] ->
1754 37 {invalid_struct_field, ExtraKey};
1755 [] ->
1756 42 case KeyNormKeys of
1757 [KeyNormKey|_] ->
1758 39 {invalid_struct_field, KeyNormKey};
1759 [] ->
1760 3 klsn_maybe:get_value(MaybeValueNormReason)
1761 end
1762 end,
1763 79 {normalized, Output, NormReason}
1764 end
1765 end
1766 end
1767 end
1768 end;
1769 struct_rule(_, _, _State) ->
1770 1 reject.
1771
1772 -spec maybe_binstr_from_any_(term()) -> klsn:optnl(klsn:binstr()).
1773 maybe_binstr_from_any_(Value) ->
1774 991 try
1775 991 {value, klsn_binstr:from_any(Value)}
1776 catch
1777 _:_ ->
1778 1 none
1779 end.
1780
1781 -spec struct_eval_fields_(
1782 [{atom(), {required | optional, rule()}}]
1783 , #{atom() => [{term(), term()}]}
1784 , maps:map(atom(), term())
1785 , klsn:optnl(reason())
1786 , state()
1787 ) -> {ok, maps:map(atom(), term()), klsn:optnl(reason())} | {reject, reason()}.
1788 struct_eval_fields_([], _Matches, Output, MaybeNormReason, _State) ->
1789 176 {ok, Output, MaybeNormReason};
1790 struct_eval_fields_([{Field, {_ReqOpt, Rule}}|T], Matches, Output0, MaybeNormReason0, State) ->
1791 1099 case maps:get(Field, Matches, []) of
1792 [] ->
1793 695 struct_eval_fields_(T, Matches, Output0, MaybeNormReason0, State);
1794 [{_Key, Value}] ->
1795 404 case eval(Value, Rule, State) of
1796 {valid, ValueOut} ->
1797 119 struct_eval_fields_(T, Matches, maps:put(Field, ValueOut, Output0), MaybeNormReason0, State);
1798 {normalized, ValueOut, Reason} ->
1799 43 MaybeNormReason1 = case MaybeNormReason0 of
1800 none ->
1801 37 {value, {invalid_struct_value, Field, Reason}};
1802 _ ->
1803 6 MaybeNormReason0
1804 end,
1805 43 struct_eval_fields_(T, Matches, maps:put(Field, ValueOut, Output0), MaybeNormReason1, State);
1806 {reject, Reason} ->
1807 242 {reject, {invalid_struct_value, Field, Reason}}
1808 end;
1809 _ ->
1810
:-(
{reject, {struct_field_conflict, Field}}
1811 end.
1812
1813 -spec do(fun((input()) -> boolean()), [fun((input()) -> output())], input()) -> result().
1814 do(Guard, Converts, Input) ->
1815 206 try Guard(Input) of
1816 true ->
1817 87 valid;
1818 false ->
1819 119 do(Converts, Input)
1820 catch _:_ ->
1821
:-(
do(Converts, Input)
1822 end.
1823
1824 -spec do([fun((input()) -> output())], input()) -> result().
1825 do([], _Input) ->
1826 65 reject;
1827 do([H|T], Input) ->
1828 217 try H(Input) of
1829 Output when Output =:= Input ->
1830 19 valid;
1831 Output ->
1832 62 {normalized, Output}
1833 catch _:_ ->
1834 136 do(T, Input)
1835 end.
1836
1837
1838 %% @doc
1839 %% Evaluate a rule against an input and return a strict result.
1840 %%
1841 %% Builtin rules may be passed as atoms (for example, `integer') or
1842 %% tuples; they are dispatched to `name_rule/3' functions. Unknown
1843 %% rules return `{reject, {unknown_rule, Rule}}'.
1844 %%
1845 %% Custom rules use `{custom, Name, Fun, Param}' where
1846 %% `Fun(Input, Param, State) -> result()'. Return handling:
1847 %% - `valid' or `{valid, Input}' maps to `{valid, Input}'
1848 %% - `{valid, Output}' with `Output =/= Input' raises
1849 %% `error({invalid_custom_rule, ...})'
1850 %% - `{normalized, Output}' maps to
1851 %% `{normalized, Output, {invalid, Name, Input}}'
1852 %% - `reject' maps to `{reject, {invalid, Name, Input}}'
1853 %% - `{normalized, Output, Reason}' and `{reject, Reason}'
1854 %% pass through unchanged
1855 %%
1856 %% Custom reasons are expected to use `{custom, term()}' so they do
1857 %% not conflict with reasons from builtin rules.
1858 %%
1859 %% Examples:
1860 %% ```
1861 %% 1> klsn_rule:eval(<<"10">>, integer, #{}).
1862 %% {normalized,10,{invalid,integer,<<"10">>}}
1863 %% 2> Unwrap = fun({ok, V}, _Param, _State) ->
1864 %% {normalized, V, {custom, unwrapped}};
1865 %% (_, _Param, _State) ->
1866 %% {reject, {custom, unexpected}}
1867 %% end.
1868 %% 3> Rule = {custom, unwrap_ok, Unwrap, []}.
1869 %% 4> klsn_rule:eval({ok, 5}, Rule, #{}).
1870 %% {normalized,5,{custom,unwrapped}}
1871 %% 5> klsn_rule:eval(error, Rule, #{}).
1872 %% {reject,{custom,unexpected}}
1873 %% '''
1874 %% @see validate/2
1875 %% @see normalize/2
1876 -spec eval(input(), rule(), state()) -> strict_result().
1877 eval(Input, {custom, Name, Custom, Param}=Arg2, State) ->
1878 2822 case Custom(Input, Param, State) of
1879 valid ->
1880 385 {valid, Input};
1881 {valid, Input} ->
1882 147 {valid, Input};
1883 {valid, Modified} ->
1884 1 error({invalid_custom_rule, klsn_binstr:from_any(io_lib:format(
1885 "custom rule ~p returned {valid, Output}=~p for input ~p, but Output /= Input. "
1886 "{valid, output()} must return the unmodified input (or return valid)."
1887 , [Name, Modified, Input]
1888 ))}, [Arg2, Input]);
1889 {normalized, Output} ->
1890 136 {normalized, Output, {invalid, Name, Input}};
1891 {normalized, Output, Reason} ->
1892 276 {normalized, Output, Reason};
1893 reject ->
1894 130 {reject, {invalid, Name, Input}};
1895 {reject, Reason} ->
1896 1746 {reject, Reason};
1897 Unexpected ->
1898 1 error({invalid_custom_rule, klsn_binstr:from_any(io_lib:format(
1899 "custom rule ~p returned ~p when input is ~p. klsn_rule:result() expected."
1900 , [Name, Unexpected, Input]
1901 ))}, [Arg2, Input])
1902 end;
1903 eval(Input, Rule, State) ->
1904 2796 case rule_to_custom_(Rule) of
1905 {value, CustomRule} ->
1906 2795 eval(Input, CustomRule, State);
1907 none ->
1908 1 {reject, {unknown_rule, Rule}}
1909 end.
1910
1911
1912 -spec rule_to_custom_(rule()) -> klsn:optnl({custom, name(), custom(), rule_param()}).
1913 rule_to_custom_({custom, Name, Custom, Param}) ->
1914
:-(
{value, {custom, Name, Custom, Param}};
1915 rule_to_custom_(Rule) ->
1916 2796 {Name, Param} = case Rule of
1917 {_, _} ->
1918 2474 Rule;
1919 Name0 ->
1920 322 {Name0, []}
1921 end,
1922 2796 MaybeRule = lists:search(fun
1923 ({FunName, 3})->
1924 40058 try
1925 40058 <<(klsn_binstr:from_any(Name))/binary, "_rule">> =:= klsn_binstr:from_any(FunName)
1926 catch _:_ ->
1927
:-(
false
1928 end;
1929 (_) ->
1930 8390 false
1931 end, ?MODULE:module_info(exports)),
1932 2796 case MaybeRule of
1933 {value, {Function, Arity}} ->
1934 2795 {value, {custom, Name, fun ?MODULE:Function/Arity, Param}};
1935 _ ->
1936 1 none
1937 end.
Line Hits Source