From 58e225fe939930cd85517ea3b77b794ae5d52486 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Wed, 25 Jun 2025 13:38:38 +0200 Subject: [PATCH 1/2] refactor: modularize generator --- rebar.config | 1 + src/ndto_generator.erl | 2559 +---------------- src/ndto_generator/ndto_generator_array.erl | 536 ++++ src/ndto_generator/ndto_generator_boolean.erl | 43 + .../ndto_generator_complement.erl | 66 + src/ndto_generator/ndto_generator_empty.erl | 34 + src/ndto_generator/ndto_generator_enum.erl | 47 + src/ndto_generator/ndto_generator_float.erl | 173 ++ src/ndto_generator/ndto_generator_integer.erl | 210 ++ .../ndto_generator_intersection.erl | 123 + src/ndto_generator/ndto_generator_object.erl | 1026 +++++++ src/ndto_generator/ndto_generator_ref.erl | 51 + src/ndto_generator/ndto_generator_string.erl | 395 +++ .../ndto_generator_symmetric_difference.erl | 145 + src/ndto_generator/ndto_generator_union.erl | 104 + .../ndto_generator_universal.erl | 39 + 16 files changed, 3045 insertions(+), 2507 deletions(-) create mode 100644 src/ndto_generator/ndto_generator_array.erl create mode 100644 src/ndto_generator/ndto_generator_boolean.erl create mode 100644 src/ndto_generator/ndto_generator_complement.erl create mode 100644 src/ndto_generator/ndto_generator_empty.erl create mode 100644 src/ndto_generator/ndto_generator_enum.erl create mode 100644 src/ndto_generator/ndto_generator_float.erl create mode 100644 src/ndto_generator/ndto_generator_integer.erl create mode 100644 src/ndto_generator/ndto_generator_intersection.erl create mode 100644 src/ndto_generator/ndto_generator_object.erl create mode 100644 src/ndto_generator/ndto_generator_ref.erl create mode 100644 src/ndto_generator/ndto_generator_string.erl create mode 100644 src/ndto_generator/ndto_generator_symmetric_difference.erl create mode 100644 src/ndto_generator/ndto_generator_union.erl create mode 100644 src/ndto_generator/ndto_generator_universal.erl diff --git a/rebar.config b/rebar.config index e702137..622728a 100644 --- a/rebar.config +++ b/rebar.config @@ -54,6 +54,7 @@ {xref_ignores, [ ndto, + ndto_generator, ndto_parser, ndto_parser_json_schema, ndto_validation diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index d09392f..8ba07d7 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -21,6 +21,36 @@ %%% EXTERNAL EXPORTS -export([generate/2]). +%%% GENERATION EXPORTS +-export([ + is_valid/2 +]). + +%%% UTIL EXPORTS +-export([ + chain_conditions/3, + chain_conditions/4, + clauses/1, + false_clause/2, + false_return/2, + guard/2, + literal/1, + null_clause/1, + optional_clause/1, + type_guard/1, + type_guard/2 +]). + +%%%----------------------------------------------------------------------------- +%%% BEHAVIOUR CALLBACKS +%%%----------------------------------------------------------------------------- +-callback is_valid(Prefix, Schema) -> Result when + Prefix :: binary(), + Schema :: ndto:schema(), + Result :: {IsValidFun, ExtraFuns}, + IsValidFun :: erl_syntax:syntaxTree(), + ExtraFuns :: [erl_syntax:syntaxTree()]. + %%%----------------------------------------------------------------------------- %%% EXTERNAL EXPORTS %%%----------------------------------------------------------------------------- @@ -84,7 +114,7 @@ generate(Name, Schema) -> ). %%%----------------------------------------------------------------------------- -%%% GENERATION FUNCTIONS +%%% GENERATION EXPORTS %%%----------------------------------------------------------------------------- -spec is_valid(Prefix, Schema) -> Result when Prefix :: binary(), @@ -93,2521 +123,36 @@ generate(Name, Schema) -> IsValidFun :: erl_syntax:syntaxTree(), ExtraFuns :: [erl_syntax:syntaxTree()]. is_valid(Prefix, false) -> - FunName = Prefix, - FalseClause = false_clause(Prefix, "Unexpected value for false schema"), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [FalseClause] - ), - {Fun, []}; -is_valid(Prefix, #{ref := Ref} = Schema) -> - FunName = <>, - DTO = erlang:binary_to_atom(Ref), - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:application( - erl_syntax:atom(DTO), - erl_syntax:atom(is_valid), - [ - erl_syntax:variable('Val') - ] - ) - ] - ), - Clauses = clauses([OptionalClause, NullClause, TrueClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, []}; -is_valid(Prefix, #{enum := Enum} = Schema) -> - FunName = Prefix, - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClauses = lists:map( - fun(EnumVal) -> - erl_syntax:clause( - [literal(EnumVal)], - none, - [erl_syntax:atom(true)] - ) - end, - Enum - ), - FalseClause = false_clause(Prefix, "Value is not one in the enum"), - Clauses = clauses(lists:flatten([OptionalClause, NullClause, TrueClauses, FalseClause])), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, []}; + ndto_generator_empty:is_valid(Prefix, false); +is_valid(Prefix, #{ref := _Ref} = Schema) -> + ndto_generator_ref:is_valid(Prefix, Schema); +is_valid(Prefix, #{enum := _Enum} = Schema) -> + ndto_generator_enum:is_valid(Prefix, Schema); is_valid(Prefix, #{type := string} = Schema) -> - FunName = Prefix, - ExtraFuns = - lists:foldl( - fun(Keyword, Acc) -> - case maps:get(Keyword, Schema, undefined) of - undefined -> - Acc; - Value -> - case is_valid_string(FunName, Keyword, Value) of - undefined -> - Acc; - Fun -> - [Fun | Acc] - end - end - end, - [], - [ - min_length, - max_length, - format, - pattern - ] - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- ExtraFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(string), - chain_conditions(FunName, ValidationConditions, 'andalso') - ), - FalseClause = false_clause(<>, "Value is not a string"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, ExtraFuns}; + ndto_generator_string:is_valid(Prefix, Schema); is_valid(Prefix, #{type := integer} = Schema) -> - FunName = Prefix, - ExtraFuns = lists:foldl( - fun(Keyword, Acc) -> - case maps:get(Keyword, Schema, undefined) of - undefined -> - Acc; - Value -> - case is_valid_number(integer, FunName, Keyword, Value, Schema) of - undefined -> - Acc; - NewIsValidFun -> - [NewIsValidFun | Acc] - end - end - end, - [], - [ - minimum, - maximum, - multiple_of - ] - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- ExtraFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(integer), - chain_conditions(FunName, ValidationConditions, 'andalso') - ), - FalseClause = false_clause(<>, "Value is not an integer"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, ExtraFuns}; + ndto_generator_integer:is_valid(Prefix, Schema); is_valid(Prefix, #{type := float} = Schema) -> - FunName = Prefix, - ExtraFuns = lists:foldl( - fun(Keyword, Acc) -> - case maps:get(Keyword, Schema, undefined) of - undefined -> - Acc; - Value -> - case is_valid_number(float, FunName, Keyword, Value, Schema) of - undefined -> - Acc; - NewIsValidFun -> - [NewIsValidFun | Acc] - end - end - end, - [], - [ - minimum, - maximum, - multiple_of - ] - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- ExtraFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(float), - chain_conditions(FunName, ValidationConditions, 'andalso') - ), - FalseClause = false_clause(<>, "Value is not a float"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, ExtraFuns}; + ndto_generator_float:is_valid(Prefix, Schema); is_valid(Prefix, #{type := boolean} = Schema) -> - FunName = Prefix, - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(boolean), - [erl_syntax:atom(true)] - ), - FalseClause = false_clause(<>, "Value is not a boolean"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, []}; + ndto_generator_boolean:is_valid(Prefix, Schema); is_valid(Prefix, #{type := array} = Schema) -> - FunName = Prefix, - {IsValidFuns, ExtraFuns} = - lists:foldl( - fun(Keyword, {IsValidFunsAcc, ExtraFunsAcc} = Acc) -> - case maps:get(Keyword, Schema, undefined) of - undefined -> - Acc; - _Value -> - case is_valid_array(FunName, Keyword, Schema) of - {undefined, _EmptyList} -> - Acc; - {NewIsValidFun, NewExtraFuns} -> - { - [NewIsValidFun | IsValidFunsAcc], - NewExtraFuns ++ ExtraFunsAcc - } - end - end - end, - {[], []}, - [ - items, - min_items, - max_items, - unique_items - ] - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- IsValidFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(array), - chain_conditions(FunName, ValidationConditions, 'andalso') - ), - FalseClause = false_clause(<>, "Value is not an array"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, IsValidFuns ++ ExtraFuns}; + ndto_generator_array:is_valid(Prefix, Schema); is_valid(Prefix, #{type := object} = Schema) -> - FunName = Prefix, - {IsValidFuns, ExtraFuns} = - lists:foldl( - fun(Keyword, {IsValidFunsAcc, ExtraFunsAcc} = Acc) -> - case maps:get(Keyword, Schema, undefined) of - undefined -> - Acc; - _Value -> - case is_valid_object(<>, Keyword, Schema) of - {undefined, _EmptyList} -> - Acc; - {NewIsValidFun, NewExtraFuns} -> - { - [NewIsValidFun | IsValidFunsAcc], - NewExtraFuns ++ ExtraFunsAcc - } - end - end - end, - {[], []}, - [ - properties, - required, - min_properties, - max_properties, - pattern_properties, - additional_properties - ] - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- IsValidFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - type_guard(object), - chain_conditions(FunName, ValidationConditions, 'andalso') - ), - FalseClause = false_clause(<>, "Value is not an object"), - Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{one_of := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, - {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( - fun(Subschema, {Idx, IsValidFunsAcc, ExtraFunsAcc}) -> - {IsValidFun, ExtraFuns} = is_valid( - <>, Subschema - ), - { - Idx + 1, - [IsValidFun | IsValidFunsAcc], - ExtraFuns ++ ExtraFunsAcc - } - end, - {0, [], []}, - Subschemas - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- IsValidFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erlang:hd(chain_conditions(FunName, ValidationConditions, 'xor')), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none_matched') - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(binary_to_atom(FunName)), - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - "Value is not matching exactly one condition. None matched." - ) - ) - ]) - ]) - ]) - ] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom('many_matched'), - erl_syntax:tuple([ - erl_syntax:variable('First'), - erl_syntax:variable('Second') - ]) - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(binary_to_atom(FunName)), - erl_syntax:application( - erl_syntax:atom('unicode'), - erl_syntax:atom('characters_to_binary'), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - "Value is not matching exactly one condition. More than one (conditions ~p and ~p) matched." - ), - erl_syntax:list([ - erl_syntax:variable('Second'), - erl_syntax:variable('First') - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Clauses = clauses([OptionalClause, NullClause, TrueClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{any_of := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, - {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( - fun(Subschema, {RawIdx, IsValidFunsAcc, ExtraFunsAcc}) -> - Idx = erlang:integer_to_binary(RawIdx), - {IsValidFun, ExtraFuns} = is_valid( - <>, Subschema - ), - { - RawIdx + 1, - [IsValidFun | IsValidFunsAcc], - ExtraFuns ++ ExtraFunsAcc - } - end, - {0, [], []}, - Subschemas - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- IsValidFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erlang:hd(chain_conditions(FunName, ValidationConditions, 'orelse', true)), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none_matched') - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(erlang:binary_to_atom(FunName)), - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - "Value is not matching at least one condition. None matched." - ) - ) - ]) - ]) - ]) - ] - ) - ] - ) - ] - ), - Clauses = clauses([OptionalClause, NullClause, TrueClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, - {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( - fun(Subschema, {RawIdx, IsValidFunsAcc, ExtraFunsAcc}) -> - Idx = erlang:integer_to_binary(RawIdx), - {IsValidFun, ExtraFuns} = is_valid( - <>, Subschema - ), - { - RawIdx + 1, - [IsValidFun | IsValidFunsAcc], - ExtraFuns ++ ExtraFunsAcc - } - end, - {0, [], []}, - Subschemas - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(Fun), - erl_syntax:integer(erl_syntax:function_arity(Fun)) - ) - ), - erl_syntax:list([erl_syntax:variable('Val')]) - ]) - || Fun <- IsValidFuns - ], - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erlang:hd(chain_conditions(FunName, ValidationConditions, 'andalso', true)), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:tuple([ - erl_syntax:variable('AllOfReasonPath'), - erl_syntax:variable('ReasonMsg') - ]), - erl_syntax:variable('N') - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(erlang:binary_to_atom(FunName)), - erl_syntax:application( - erl_syntax:atom('unicode'), - erl_syntax:atom('characters_to_binary'), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - "Value is not matching all conditions. Condition ~p failed because of schema path '~ts' : ~ts" - ), - erl_syntax:list([ - erl_syntax:variable('N'), - erl_syntax:variable('AllOfReasonPath'), - erl_syntax:variable('ReasonMsg') - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Clauses = clauses([OptionalClause, NullClause, TrueClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{'not' := Subschema} = Schema) -> - FunName = Prefix, - {IsValidFun, ExtraFuns} = is_valid(<>, Subschema), - OptionalClause = optional_clause(Schema), - NullClause = null_clause(Schema), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:function_name(IsValidFun), - [erl_syntax:variable('Val')] - ), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('false')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:variable('_') - ]) - ], - none, - [erl_syntax:atom('true')] - ) - ] - ) - ] - ), - Clauses = clauses([OptionalClause, NullClause, TrueClause]), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - Clauses - ), - {Fun, [IsValidFun | ExtraFuns]}; + ndto_generator_object:is_valid(Prefix, Schema); +is_valid(Prefix, #{one_of := _Subschemas} = Schema) when is_list(_Subschemas) -> + ndto_generator_symmetric_difference:is_valid(Prefix, Schema); +is_valid(Prefix, #{any_of := _Subschemas} = Schema) when is_list(_Subschemas) -> + ndto_generator_union:is_valid(Prefix, Schema); +is_valid(Prefix, #{all_of := _Subschemas} = Schema) when is_list(_Subschemas) -> + ndto_generator_intersection:is_valid(Prefix, Schema); +is_valid(Prefix, #{'not' := _Subschema} = Schema) -> + ndto_generator_complement:is_valid(Prefix, Schema); is_valid(Prefix, _Schema) -> - FunName = <>, - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('_Val')], - none, - [erl_syntax:atom(true)] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}. - --spec is_valid_array(Prefix, Keyword, Value) -> Result when - Prefix :: binary(), - Keyword :: atom(), - Value :: term(), - Result :: {Fun, ExtraFuns}, - Fun :: erl_syntax:syntaxTree() | undefined, - ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_array(Prefix, items, #{items := Items} = Schema) when is_map(Items) -> - FunName = <>, - {IsValidFun, ExtraFuns} = is_valid(<>, Items), - OptionalClause = optional_clause(Schema), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(mfoldl), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [ - erl_syntax:variable('Item'), - erl_syntax:variable('Acc') - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:application( - erl_syntax:function_name(IsValidFun), - [erl_syntax:variable('Item')] - ), - erl_syntax:infix_expr( - erl_syntax:variable('Acc'), - erl_syntax:operator('+'), - erl_syntax:integer(1) - ) - ]) - ] - ) - ]), - erl_syntax:integer(0), - erl_syntax:variable('Val') - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:variable('_Acc') - ]) - ], - none, - [ - erl_syntax:atom('true') - ] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:variable('Acc'), - erl_syntax:tuple([ - erl_syntax:variable('Function'), - erl_syntax:variable('Reason') - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Function'), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(list_to_binary), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - "Item ~p in ~ts is invalid. ~s" - ), - erl_syntax:list([ - erl_syntax:variable('Acc'), - erl_syntax:string( - binary_to_list(Prefix) - ), - erl_syntax:variable('Reason') - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - clauses([OptionalClause, TrueClause]) - ), - {Fun, [IsValidFun | ExtraFuns]}; -is_valid_array(Prefix, items, #{items := Items} = Schema) when is_list(Items) -> - FunName = <>, - {_Size, IsValidFuns, ExtraFuns} = lists:foldl( - fun(Item, {Idx, IsValidFunsAcc, ExtraFunsAcc}) -> - ItemFunName = <>, - {ItemIsValidFun, ItemExtraFuns} = is_valid(ItemFunName, Item), - {Idx + 1, [{Idx, ItemIsValidFun} | IsValidFunsAcc], ItemExtraFuns ++ ExtraFunsAcc} - end, - {1, [], []}, - Items - ), - AdditionalItems = maps:get(additional_items, Schema, true), - {IsValidAdditionalItemsFun, AdditionalItemsExtraFuns} = - is_valid(<>, AdditionalItems), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:match_expr( - erl_syntax:variable('FunsMap'), - erl_syntax:map_expr( - lists:map( - fun({Idx, IsValidFun}) -> - erl_syntax:map_field_assoc( - erl_syntax:integer(Idx), - erl_syntax:fun_expr(erl_syntax:function_clauses(IsValidFun)) - ) - end, - IsValidFuns - ) - ) - ), - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(find), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:variable('Item'), - erl_syntax:variable('FunKey') - ]) - ], - none, - [ - erl_syntax:match_expr( - erl_syntax:variable('Result'), - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(get), - [ - erl_syntax:variable('FunKey'), - erl_syntax:variable('FunsMap'), - erl_syntax:atom(undefined) - ] - ), - [ - erl_syntax:clause( - [erl_syntax:atom(undefined)], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:function_name( - IsValidAdditionalItemsFun - ), - [erl_syntax:variable('Item')] - ), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom( - 'false' - ), - erl_syntax:variable( - '_FalseReason' - ) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom( - 'false' - ), - erl_syntax:tuple([ - erl_syntax:atom( - erlang:binary_to_atom( - FunName - ) - ), - erl_syntax:application( - erl_syntax:atom( - 'list_to_binary' - ), - [ - erl_syntax:application( - erl_syntax:atom( - io_lib - ), - erl_syntax:atom( - format - ), - [ - erl_syntax:string( - "Value at position ~p does not match the schema" - ), - erl_syntax:list( - [ - erl_syntax:variable( - 'FunKey' - ) - ] - ) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - erl_syntax:clause( - [erl_syntax:variable('IsValidItemFun')], - none, - [ - erl_syntax:application( - erl_syntax:variable( - 'IsValidItemFun' - ), - [ - erl_syntax:variable( - 'Item' - ) - ] - ) - ] - ) - ] - ) - ), - erl_syntax:case_expr( - erl_syntax:variable('Result'), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('false')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:variable('FalseResult') - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:variable('FalseResult') - ]) - ] - ) - ] - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(zip), - [ - erl_syntax:variable('Val'), - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(seq), - [ - erl_syntax:integer(1), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(length), - [erl_syntax:variable('Val')] - ) - ] - ) - ] - ) - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none') - ]) - ], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:tuple([ - erl_syntax:variable('_Item'), - erl_syntax:variable('_FunKey') - ]), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, ExtraFuns ++ [IsValidAdditionalItemsFun | AdditionalItemsExtraFuns]}; -is_valid_array(Prefix, min_items, #{min_items := MinItems}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - erl_syntax:infix_expr( - erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]), - erl_syntax:operator('>='), - erl_syntax:integer(MinItems) - ), - [erl_syntax:atom(true)] - ), - FalseClause = false_clause( - FunName, - unicode:characters_to_list( - io_lib:format("Array does not have at least ~p items", [MinItems]) - ) - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause, FalseClause] - ), - {Fun, []}; -is_valid_array(Prefix, max_items, #{max_items := MaxItems}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - erl_syntax:infix_expr( - erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]), - erl_syntax:operator('=<'), - erl_syntax:integer(MaxItems) - ), - [erl_syntax:atom(true)] - ), - FalseClause = false_clause( - FunName, - unicode:characters_to_list( - io_lib:format("Array does not have at most ~p items", [MaxItems]) - ) - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause, FalseClause] - ), - {Fun, []}; -is_valid_array(Prefix, unique_items, #{unique_items := true}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:match_expr( - erl_syntax:variable('ArraySize'), - erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]) - ), - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(size), - [ - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(from_list), - [erl_syntax:variable('Val')] - ) - ] - ), - [ - erl_syntax:clause( - [erl_syntax:variable('ArraySize')], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format("Array has non unique items", []) - ) - ) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}; -is_valid_array(_Prefix, unique_items, #{unique_items := false}) -> - {undefined, []}. - --spec is_valid_number(Type, Prefix, Keyword, Value, Schema) -> Result when - Type :: integer | float, - Prefix :: binary(), - Keyword :: atom(), - Value :: term(), - Schema :: ndto:integer_schema() | ndto:float_schema(), - Result :: undefined | erl_syntax:syntaxTree(). -is_valid_number(_Type, Prefix, minimum, Minimum, Schema) -> - FunName = <>, - MinimumSt = - case Minimum of - Integer when is_integer(Integer) -> - erl_syntax:integer(Minimum); - _Float -> - erl_syntax:float(Minimum) - end, - ExclusiveMinimum = maps:get(exclusive_minimum, Schema, false), - Operator = - case ExclusiveMinimum of - true -> - erl_syntax:operator('>'); - _false -> - erl_syntax:operator('>=') - end, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - erl_syntax:infix_expr( - erl_syntax:variable('Val'), - Operator, - MinimumSt - ), - [erl_syntax:atom(true)] - ), - ComparisonTerm = - case ExclusiveMinimum of - true -> - "than"; - false -> - "or equal to" - end, - FalseClause = false_clause( - FunName, - unicode:characters_to_list( - io_lib:format("Value is not a number greater ~s ~p", [ComparisonTerm, Minimum]) - ) - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause, FalseClause] - ); -is_valid_number(_Type, Prefix, maximum, Maximum, Schema) -> - FunName = <>, - MaximumSt = - case Maximum of - Integer when is_integer(Integer) -> - erl_syntax:integer(Maximum); - _Float -> - erl_syntax:float(Maximum) - end, - ExclusiveMaximum = maps:get(exclusive_maximum, Schema, false), - Operator = - case ExclusiveMaximum of - true -> - erl_syntax:operator('<'); - _false -> - erl_syntax:operator('=<') - end, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - erl_syntax:infix_expr( - erl_syntax:variable('Val'), - Operator, - MaximumSt - ), - [erl_syntax:atom(true)] - ), - ComparisonTerm = - case ExclusiveMaximum of - true -> - "than"; - false -> - "or equal to" - end, - FalseClause = false_clause( - FunName, - unicode:characters_to_list( - io_lib:format("Number is not lower ~s ~p", [ComparisonTerm, Maximum]) - ) - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause, FalseClause] - ); -is_valid_number(integer, Prefix, multiple_of, MultipleOf, _Schema) -> - FunName = <>, - TrueClause = - erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:infix_expr( - erl_syntax:variable('Val'), - erl_syntax:operator('rem'), - erl_syntax:integer(MultipleOf) - ), - [ - erl_syntax:clause( - [erl_syntax:integer(0)], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format("Value is not multiple of ~p", [MultipleOf]) - ) - ) - ] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_number(_Number, _Prefix, multiple_of, _Value, _Schema) -> - undefined. - --spec is_valid_object(Prefix, Keyword, Schema) -> Result when - Prefix :: binary(), - Keyword :: atom(), - Schema :: ndto:object_schema(), - Result :: {Fun, ExtraFuns}, - Fun :: erl_syntax:syntaxTree() | undefined, - ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_object(Prefix, properties, #{properties := Properties}) -> - FunName = <>, - {PropertiesFuns, ExtraFuns} = maps:fold( - fun(PropertyName, Property, {IsValidFunsAcc, ExtraFunsAcc}) -> - {IsValidPropertyFun, ExtraPropertyFuns} = - is_valid(<>, Property#{ - optional => true - }), - { - [{PropertyName, IsValidPropertyFun} | IsValidFunsAcc], - ExtraFunsAcc ++ ExtraPropertyFuns - } - end, - {[], []}, - Properties - ), - ValidationConditions = [ - erl_syntax:tuple([ - erl_syntax:implicit_fun( - erl_syntax:arity_qualifier( - erl_syntax:function_name(PropertyFun), - erl_syntax:integer(erl_syntax:function_arity(PropertyFun)) - ) - ), - erl_syntax:list([ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(get), - [ - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string(erlang:binary_to_list(PropertyName)) - ) - ]), - erl_syntax:variable('Val'), - erl_syntax:atom(undefined) - ] - ) - ]) - ]) - || {PropertyName, PropertyFun} <- PropertiesFuns - ], - FunBody = chain_conditions(FunName, ValidationConditions, 'andalso'), - - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - FunBody - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {_PropertyNames, IsValidFuns} = lists:unzip(PropertiesFuns), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid_object(Prefix, required, #{required := Required}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(find), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('Property')], - none, - [ - erl_syntax:prefix_expr( - erl_syntax:operator('not'), - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(is_key), - [ - erl_syntax:variable('Property'), - erl_syntax:variable('Val') - ] - ) - ) - ] - ) - ]), - erl_syntax:list( - lists:map( - fun(PropertyName) -> - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string(erlang:binary_to_list(PropertyName)) - ) - ]) - end, - Required - ) - ) - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none') - ]) - ], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:variable('MissingProperty') - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(erlang:binary_to_list(Prefix)), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(list_to_binary), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - lists:flatten( - io_lib:format( - "~ts is missing required property ~~p", - [Prefix] - ) - ) - ), - erl_syntax:list([ - erl_syntax:variable( - 'MissingProperty' - ) - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}; -is_valid_object(Prefix, min_properties, #{min_properties := MinProperties}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(length), - [ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ) - ] - ), - [ - erl_syntax:clause( - [erl_syntax:variable('N')], - [ - erl_syntax:infix_expr( - erl_syntax:variable('N'), - erl_syntax:operator('>='), - erl_syntax:integer(MinProperties) - ) - ], - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format( - "Object has less properties than required minimum (~p)", [ - MinProperties - ] - ) - ) - ) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}; -is_valid_object(Prefix, max_properties, #{max_properties := MaxProperties}) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(length), - [ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ) - ] - ), - [ - erl_syntax:clause( - [erl_syntax:variable('N')], - [ - erl_syntax:infix_expr( - erl_syntax:variable('N'), - erl_syntax:operator('=<'), - erl_syntax:integer(MaxProperties) - ) - ], - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format( - "Object has more properties than allowed maximum (~p)", [ - MaxProperties - ] - ) - ) - ) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}; -is_valid_object(Prefix, pattern_properties, #{pattern_properties := PatternProperties}) -> - FunName = <>, - {IsValidPatterns, ExtraFuns} = lists:foldl( - fun({PropertyPattern, PropertySchema}, {IsValidPatternsAcc, ExtraFunsAcc}) -> - {IsValidFun, ExtraFuns} = is_valid( - <>, PropertySchema - ), - {[{PropertyPattern, IsValidFun} | IsValidPatternsAcc], ExtraFuns ++ ExtraFunsAcc} - end, - {[], []}, - maps:to_list(PatternProperties) - ), - Conditions = lists:map( - fun({PropertyPattern, IsValidFun}) -> - Filter = - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(filter), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('_PropertyValue') - ]) - ], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(re), - erl_syntax:atom(run), - [ - erl_syntax:variable('PropertyName'), - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list( - PropertyPattern - ) - ) - ) - ]) - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom(match), - erl_syntax:variable('_Captured') - ]) - ], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('_nomatch')], - none, - [erl_syntax:atom(false)] - ) - ] - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(to_list), - [erl_syntax:variable('Val')] - ) - ] - ), - FunBody = - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(find), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('PropertyValue') - ]) - ], - none, - [ - erl_syntax:match_expr( - erl_syntax:variable('Result'), - erl_syntax:application( - erl_syntax:function_name(IsValidFun), - [erl_syntax:variable('PropertyValue')] - ) - ), - erl_syntax:case_expr( - erl_syntax:variable('Result'), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('false')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:tuple([ - erl_syntax:tuple([ - erl_syntax:variable( - 'PropertyName' - ), - erl_syntax:variable( - 'PropertyValue' - ) - ]), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ]) - ] - ) - ] - ) - ] - ) - ]), - Filter - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none') - ]) - ], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:tuple([ - erl_syntax:tuple([ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('_PropertyValue') - ]), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(list_to_binary), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - "Property \"~ts\" failed validation: ~ts" - ), - erl_syntax:list([ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('Reason') - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ), - erl_syntax:tuple([ - erl_syntax:fun_expr([ - erl_syntax:clause(none, [FunBody]) - ]), - erl_syntax:list([]) - ]) - end, - IsValidPatterns - ), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - chain_conditions(FunName, Conditions, 'andalso') - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - IsValidFuns = [IsValidFun || {_PatternName, IsValidFun} <- IsValidPatterns], - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid_object( - Prefix, - additional_properties, - #{additional_properties := false} = Schema -) -> - FunName = <>, - Properties = maps:get(properties, Schema, #{}), - PatternProperties = maps:get(pattern_properties, Schema, #{}), - PatternPropertiesList = erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(filter), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('PropertyName')], - none, - [ - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(any), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('Pattern')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(re), - erl_syntax:atom(run), - [ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('Pattern') - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom(match), - erl_syntax:variable('_Captured') - ]) - ], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('_nomatch')], - none, - [erl_syntax:atom(false)] - ) - ] - ) - ] - ) - ]), - erl_syntax:list( - lists:map( - fun(PropertyPattern) -> - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(PropertyPattern) - ) - ) - ]) - end, - maps:keys(PatternProperties) - ) - ) - ] - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ) - ] - ), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(to_list), - [ - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(subtract), - [ - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(from_list), - [ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ) - ] - ), - erl_syntax:application( - erl_syntax:atom(sets), - erl_syntax:atom(from_list), - [ - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(append), - [ - erl_syntax:list( - lists:map( - fun(PropertyName) -> - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list( - PropertyName - ) - ) - ) - ]) - end, - maps:keys(Properties) - ) - ), - PatternPropertiesList - ] - ) - ] - ) - ] - ) - ] - ), - [ - erl_syntax:clause( - [erl_syntax:list([])], - none, - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('UnsupportedKeys')], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:atom(erlang:binary_to_list(FunName)), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(list_to_binary), - [ - erl_syntax:application( - erl_syntax:atom(io_lib), - erl_syntax:atom(format), - [ - erl_syntax:string( - "Object has unsupported keys: ~ts" - ), - erl_syntax:list([ - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(format_properties), - [ - erl_syntax:variable( - 'UnsupportedKeys' - ) - ] - ) - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, []}; -is_valid_object( - Prefix, - additional_properties, - #{additional_properties := AdditionalProperties} = Schema -) -> - FunName = <>, - {IsValidFun, ExtraFuns} = is_valid( - <>, AdditionalProperties - ), - Properties = maps:get(properties, Schema, #{}), - PatternProperties = maps:get(pattern_properties, Schema, #{}), - PatternPropertiesList = erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(filter), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('PropertyName')], - none, - [ - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(any), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('Pattern')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(re), - erl_syntax:atom(run), - [ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('Pattern') - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom(match), - erl_syntax:variable('_Captured') - ]) - ], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('_nomatch')], - none, - [erl_syntax:atom(false)] - ) - ] - ) - ] - ) - ]), - erl_syntax:list( - lists:map( - fun(PropertyPattern) -> - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(PropertyPattern) - ) - ) - ]) - end, - maps:keys(PatternProperties) - ) - ) - ] - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ) - ] - ), - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ndto_validation), - erl_syntax:atom(find), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:variable('Property')], - none, - [ - erl_syntax:match_expr( - erl_syntax:variable('Result'), - erl_syntax:application( - erl_syntax:function_name(IsValidFun), - [ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(get), - [ - erl_syntax:variable('Property'), - erl_syntax:variable('Val') - ] - ) - ] - ) - ), - erl_syntax:case_expr( - erl_syntax:variable('Result'), - [ - erl_syntax:clause( - [erl_syntax:atom('true')], - none, - [erl_syntax:atom('false')] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:tuple([ - erl_syntax:variable('Property'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ]) - ] - ) - ] - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(subtract), - [ - erl_syntax:application( - erl_syntax:atom(maps), - erl_syntax:atom(keys), - [erl_syntax:variable('Val')] - ), - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(append), - [ - erl_syntax:list( - lists:map( - fun(PropertyName) -> - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(PropertyName) - ) - ) - ]) - end, - maps:keys(Properties) - ) - ), - PatternPropertiesList - ] - ) - ] - ) - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:atom('none') - ]) - ], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom('true'), - erl_syntax:tuple([ - erl_syntax:variable('PropertyName'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:variable('Reason') - ]) - ]) - ]) - ], - none, - [ - erl_syntax:tuple([ - erl_syntax:atom('false'), - erl_syntax:tuple([ - erl_syntax:variable('Prefix'), - erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(list_to_binary), - [ - erl_syntax:application( - erl_syntax:atom('io_lib'), - erl_syntax:atom('format'), - [ - erl_syntax:string( - "Property \"~ts\" failed validation: ~ts" - ), - erl_syntax:list([ - erl_syntax:variable('PropertyName'), - erl_syntax:variable('Reason') - ]) - ] - ) - ] - ) - ]) - ]) - ] - ) - ] - ) - ] - ), - Fun = erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ), - {Fun, [IsValidFun | ExtraFuns]}; -is_valid_object(_Prefix, additional_properties, _Schema) -> - {undefined, []}. - --spec is_valid_string(Prefix, Keyword, Value) -> Result when - Prefix :: binary(), - Keyword :: atom(), - Value :: term(), - Result :: undefined | erl_syntax:syntaxTree(). -is_valid_string(Prefix, min_length, MinLength) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ - erl_syntax:variable('Val') - ]), - [ - erl_syntax:clause( - [erl_syntax:variable('N')], - [ - erl_syntax:infix_expr( - erl_syntax:variable('N'), - erl_syntax:operator('>='), - erl_syntax:integer(MinLength) - ) - ], - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format("Value is lower than minimum allowed (~p)", [ - MinLength - ]) - ) - ) - ] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_string(Prefix, max_length, MaxLength) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ - erl_syntax:variable('Val') - ]), - [ - erl_syntax:clause( - [erl_syntax:variable('N')], - [ - erl_syntax:infix_expr( - erl_syntax:variable('N'), - erl_syntax:operator('=<'), - erl_syntax:integer(MaxLength) - ) - ], - [erl_syntax:atom('true')] - ), - erl_syntax:clause( - [erl_syntax:variable('_')], - none, - [ - false_return( - FunName, - unicode:characters_to_list( - io_lib:format("Value is greater than maximum allowed (~p)", [ - MaxLength - ]) - ) - ) - ] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_string(Prefix, pattern, Pattern) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(re), - erl_syntax:atom(run), - [ - erl_syntax:variable('Val'), - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string(erlang:binary_to_list(Pattern)) - ) - ]) - ] - ), - [ - erl_syntax:clause( - [ - erl_syntax:tuple([ - erl_syntax:atom(match), erl_syntax:variable('_Captured') - ]) - ], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('_nomatch')], - none, - [false_return(FunName, "Value does not match pattern.")] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_string(Prefix, format, iso8601) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom(ncalendar), - erl_syntax:atom(is_valid), - [ - erl_syntax:atom(iso8601), - erl_syntax:variable('Val') - ] - ), - [ - erl_syntax:clause([erl_syntax:atom('true')], none, [erl_syntax:atom('true')]), - erl_syntax:clause( - [erl_syntax:atom('false')], - none, - [false_return(FunName, "Value is not a valid iso8601 string")] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_string(Prefix, format, base64) -> - FunName = <>, - TrueClause = erl_syntax:clause( - [erl_syntax:variable('Val')], - none, - [ - erl_syntax:case_expr( - erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ - erl_syntax:variable('Val') - ]), - [ - erl_syntax:clause( - [erl_syntax:variable('Length')], - erl_syntax:infix_expr( - erl_syntax:infix_expr( - erl_syntax:variable('Length'), - erl_syntax:operator('rem'), - erl_syntax:integer(4) - ), - erl_syntax:operator('=:='), - erl_syntax:integer(0) - ), - [ - erl_syntax:match_expr( - erl_syntax:variable('Unpad'), - erl_syntax:application( - erl_syntax:atom(string), - erl_syntax:atom(trim), - [ - erl_syntax:variable('Val'), - erl_syntax:atom(trailing), - erl_syntax:list([erl_syntax:char($=)]) - ] - ) - ), - erl_syntax:application( - erl_syntax:atom(lists), - erl_syntax:atom(all), - [ - erl_syntax:fun_expr([ - erl_syntax:clause( - [erl_syntax:integer(43)], - none, - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('Char')], - erl_syntax:infix_expr( - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('>='), - erl_syntax:integer(47) - ), - erl_syntax:operator('andalso'), - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('=<'), - erl_syntax:integer(57) - ) - ), - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('Char')], - erl_syntax:infix_expr( - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('>='), - erl_syntax:integer(65) - ), - erl_syntax:operator('andalso'), - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('=<'), - erl_syntax:integer(90) - ) - ), - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('Char')], - erl_syntax:infix_expr( - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('>='), - erl_syntax:integer(97) - ), - erl_syntax:operator('andalso'), - erl_syntax:infix_expr( - erl_syntax:variable('Char'), - erl_syntax:operator('=<'), - erl_syntax:integer(122) - ) - ), - [erl_syntax:atom(true)] - ), - erl_syntax:clause( - [erl_syntax:variable('_Char')], - none, - [ - false_return( - FunName, "Value is an invalid base64 string." - ) - ] - ) - ]), - erl_syntax:application( - erl_syntax:atom(string), - erl_syntax:atom(to_graphemes), - [erl_syntax:variable('Unpad')] - ) - ] - ) - ] - ), - erl_syntax:clause( - [erl_syntax:variable('_Length')], - none, - [ - false_return(FunName, "Value is an invalid base64 string.") - ] - ) - ] - ) - ] - ), - erl_syntax:function( - erl_syntax:atom(erlang:binary_to_atom(FunName)), - [TrueClause] - ); -is_valid_string(_Prefix, format, _Format) -> - undefined. + ndto_generator_universal:is_valid(Prefix, _Schema). %%%----------------------------------------------------------------------------- -%%% INTERNAL FUNCTIONS +%%% UTIL EXPORTS %%%----------------------------------------------------------------------------- chain_conditions(FunName, ValidationConditions, Operator) -> chain_conditions(FunName, ValidationConditions, Operator, false). diff --git a/src/ndto_generator/ndto_generator_array.erl b/src/ndto_generator/ndto_generator_array.erl new file mode 100644 index 0000000..5323967 --- /dev/null +++ b/src/ndto_generator/ndto_generator_array.erl @@ -0,0 +1,536 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_array). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := array} = Schema) -> + FunName = Prefix, + {IsValidFuns, ExtraFuns} = + lists:foldl( + fun(Keyword, {IsValidFunsAcc, ExtraFunsAcc} = Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + _Value -> + case is_valid_(FunName, Keyword, Schema) of + {undefined, _EmptyList} -> + Acc; + {NewIsValidFun, NewExtraFuns} -> + { + [NewIsValidFun | IsValidFunsAcc], + NewExtraFuns ++ ExtraFunsAcc + } + end + end + end, + {[], []}, + [ + items, + min_items, + max_items, + unique_items + ] + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- IsValidFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(array), + ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso') + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not an array"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, IsValidFuns ++ ExtraFuns}. + +-spec is_valid_(Prefix, Keyword, Value) -> Result when + Prefix :: binary(), + Keyword :: atom(), + Value :: term(), + Result :: {Fun, ExtraFuns}, + Fun :: erl_syntax:syntaxTree() | undefined, + ExtraFuns :: [erl_syntax:syntaxTree()]. +is_valid_(Prefix, items, #{items := Items} = Schema) when is_map(Items) -> + FunName = <>, + {IsValidFun, ExtraFuns} = ndto_generator:is_valid(<>, Items), + OptionalClause = ndto_generator:optional_clause(Schema), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(mfoldl), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [ + erl_syntax:variable('Item'), + erl_syntax:variable('Acc') + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:application( + erl_syntax:function_name(IsValidFun), + [erl_syntax:variable('Item')] + ), + erl_syntax:infix_expr( + erl_syntax:variable('Acc'), + erl_syntax:operator('+'), + erl_syntax:integer(1) + ) + ]) + ] + ) + ]), + erl_syntax:integer(0), + erl_syntax:variable('Val') + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:variable('_Acc') + ]) + ], + none, + [ + erl_syntax:atom('true') + ] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:variable('Acc'), + erl_syntax:tuple([ + erl_syntax:variable('Function'), + erl_syntax:variable('Reason') + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Function'), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(list_to_binary), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + "Item ~p in ~ts is invalid. ~s" + ), + erl_syntax:list([ + erl_syntax:variable('Acc'), + erl_syntax:string( + binary_to_list(Prefix) + ), + erl_syntax:variable('Reason') + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + ndto_generator:clauses([OptionalClause, TrueClause]) + ), + {Fun, [IsValidFun | ExtraFuns]}; +is_valid_(Prefix, items, #{items := Items} = Schema) when is_list(Items) -> + FunName = <>, + {_Size, IsValidFuns, ExtraFuns} = lists:foldl( + fun(Item, {Idx, IsValidFunsAcc, ExtraFunsAcc}) -> + ItemFunName = <>, + {ItemIsValidFun, ItemExtraFuns} = ndto_generator:is_valid(ItemFunName, Item), + {Idx + 1, [{Idx, ItemIsValidFun} | IsValidFunsAcc], ItemExtraFuns ++ ExtraFunsAcc} + end, + {1, [], []}, + Items + ), + AdditionalItems = maps:get(additional_items, Schema, true), + {IsValidAdditionalItemsFun, AdditionalItemsExtraFuns} = + ndto_generator:is_valid(<>, AdditionalItems), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:match_expr( + erl_syntax:variable('FunsMap'), + erl_syntax:map_expr( + lists:map( + fun({Idx, IsValidFun}) -> + erl_syntax:map_field_assoc( + erl_syntax:integer(Idx), + erl_syntax:fun_expr(erl_syntax:function_clauses(IsValidFun)) + ) + end, + IsValidFuns + ) + ) + ), + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(find), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:variable('Item'), + erl_syntax:variable('FunKey') + ]) + ], + none, + [ + erl_syntax:match_expr( + erl_syntax:variable('Result'), + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(get), + [ + erl_syntax:variable('FunKey'), + erl_syntax:variable('FunsMap'), + erl_syntax:atom(undefined) + ] + ), + [ + erl_syntax:clause( + [erl_syntax:atom(undefined)], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:function_name( + IsValidAdditionalItemsFun + ), + [erl_syntax:variable('Item')] + ), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom( + 'false' + ), + erl_syntax:variable( + '_FalseReason' + ) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom( + 'false' + ), + erl_syntax:tuple([ + erl_syntax:atom( + erlang:binary_to_atom( + FunName + ) + ), + erl_syntax:application( + erl_syntax:atom( + 'list_to_binary' + ), + [ + erl_syntax:application( + erl_syntax:atom( + io_lib + ), + erl_syntax:atom( + format + ), + [ + erl_syntax:string( + "Value at position ~p does not match the schema" + ), + erl_syntax:list( + [ + erl_syntax:variable( + 'FunKey' + ) + ] + ) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + erl_syntax:clause( + [erl_syntax:variable('IsValidItemFun')], + none, + [ + erl_syntax:application( + erl_syntax:variable( + 'IsValidItemFun' + ), + [ + erl_syntax:variable( + 'Item' + ) + ] + ) + ] + ) + ] + ) + ), + erl_syntax:case_expr( + erl_syntax:variable('Result'), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('false')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:variable('FalseResult') + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:variable('FalseResult') + ]) + ] + ) + ] + ) + ] + ) + ]), + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(zip), + [ + erl_syntax:variable('Val'), + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(seq), + [ + erl_syntax:integer(1), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(length), + [erl_syntax:variable('Val')] + ) + ] + ) + ] + ) + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none') + ]) + ], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:tuple([ + erl_syntax:variable('_Item'), + erl_syntax:variable('_FunKey') + ]), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, ExtraFuns ++ [IsValidAdditionalItemsFun | AdditionalItemsExtraFuns]}; +is_valid_(Prefix, min_items, #{min_items := MinItems}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]), + erl_syntax:operator('>='), + erl_syntax:integer(MinItems) + ), + [erl_syntax:atom(true)] + ), + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Array does not have at least ~p items", [MinItems]) + ) + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ), + {Fun, []}; +is_valid_(Prefix, max_items, #{max_items := MaxItems}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]), + erl_syntax:operator('=<'), + erl_syntax:integer(MaxItems) + ), + [erl_syntax:atom(true)] + ), + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Array does not have at most ~p items", [MaxItems]) + ) + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ), + {Fun, []}; +is_valid_(Prefix, unique_items, #{unique_items := true}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:match_expr( + erl_syntax:variable('ArraySize'), + erl_syntax:application(erl_syntax:atom(length), [erl_syntax:variable('Val')]) + ), + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(size), + [ + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(from_list), + [erl_syntax:variable('Val')] + ) + ] + ), + [ + erl_syntax:clause( + [erl_syntax:variable('ArraySize')], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [erl_syntax:variable('_')], + none, + [ + ndto_generator:false_return( + FunName, + unicode:characters_to_list( + io_lib:format("Array has non unique items", []) + ) + ) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}; +is_valid_(_Prefix, unique_items, #{unique_items := false}) -> + {undefined, []}. diff --git a/src/ndto_generator/ndto_generator_boolean.erl b/src/ndto_generator/ndto_generator_boolean.erl new file mode 100644 index 0000000..b0e039c --- /dev/null +++ b/src/ndto_generator/ndto_generator_boolean.erl @@ -0,0 +1,43 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_boolean). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := boolean} = Schema) -> + FunName = Prefix, + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(boolean), + [erl_syntax:atom(true)] + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not a boolean"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, []}. diff --git a/src/ndto_generator/ndto_generator_complement.erl b/src/ndto_generator/ndto_generator_complement.erl new file mode 100644 index 0000000..e80c29c --- /dev/null +++ b/src/ndto_generator/ndto_generator_complement.erl @@ -0,0 +1,66 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_complement). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{'not' := Subschema} = Schema) -> + FunName = Prefix, + {IsValidFun, ExtraFuns} = ndto_generator:is_valid(<>, Subschema), + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:function_name(IsValidFun), + [erl_syntax:variable('Val')] + ), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('false')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:variable('_') + ]) + ], + none, + [erl_syntax:atom('true')] + ) + ] + ) + ] + ), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, [IsValidFun | ExtraFuns]}. diff --git a/src/ndto_generator/ndto_generator_empty.erl b/src/ndto_generator/ndto_generator_empty.erl new file mode 100644 index 0000000..5320146 --- /dev/null +++ b/src/ndto_generator/ndto_generator_empty.erl @@ -0,0 +1,34 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_empty). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, false) -> + FunName = Prefix, + FalseClause = ndto_generator:false_clause(Prefix, "Unexpected value for false schema"), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [FalseClause] + ), + {Fun, []}. diff --git a/src/ndto_generator/ndto_generator_enum.erl b/src/ndto_generator/ndto_generator_enum.erl new file mode 100644 index 0000000..df2a6fc --- /dev/null +++ b/src/ndto_generator/ndto_generator_enum.erl @@ -0,0 +1,47 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_enum). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{enum := Enum} = Schema) -> + FunName = Prefix, + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClauses = lists:map( + fun(EnumVal) -> + erl_syntax:clause( + [ndto_generator:literal(EnumVal)], + none, + [erl_syntax:atom(true)] + ) + end, + Enum + ), + FalseClause = ndto_generator:false_clause(Prefix, "Value is not one in the enum"), + Clauses = ndto_generator:clauses(lists:flatten([OptionalClause, NullClause, TrueClauses, FalseClause])), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, []}. diff --git a/src/ndto_generator/ndto_generator_float.erl b/src/ndto_generator/ndto_generator_float.erl new file mode 100644 index 0000000..c1a236d --- /dev/null +++ b/src/ndto_generator/ndto_generator_float.erl @@ -0,0 +1,173 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_float). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := float} = Schema) -> + FunName = Prefix, + ExtraFuns = lists:foldl( + fun(Keyword, Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + Value -> + case is_valid_(FunName, Keyword, Value, Schema) of + undefined -> + Acc; + NewIsValidFun -> + [NewIsValidFun | Acc] + end + end + end, + [], + [ + minimum, + maximum, + multiple_of + ] + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- ExtraFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(float), + ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso') + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not a float"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, ExtraFuns}. + +-spec is_valid_(Prefix, Keyword, Value, Schema) -> Result when + Prefix :: binary(), + Keyword :: atom(), + Value :: term(), + Schema :: ndto:float_schema(), + Result :: undefined | erl_syntax:syntaxTree(). +is_valid_(Prefix, minimum, Minimum, Schema) -> + FunName = <>, + MinimumSt = + case Minimum of + Integer when is_integer(Integer) -> + erl_syntax:integer(Minimum); + _Float -> + erl_syntax:float(Minimum) + end, + ExclusiveMinimum = maps:get(exclusive_minimum, Schema, false), + Operator = + case ExclusiveMinimum of + true -> + erl_syntax:operator('>'); + _false -> + erl_syntax:operator('>=') + end, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:variable('Val'), + Operator, + MinimumSt + ), + [erl_syntax:atom(true)] + ), + ComparisonTerm = + case ExclusiveMinimum of + true -> + "than"; + false -> + "or equal to" + end, + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Value is not a number greater ~s ~p", [ComparisonTerm, Minimum]) + ) + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ); +is_valid_(Prefix, maximum, Maximum, Schema) -> + FunName = <>, + MaximumSt = + case Maximum of + Integer when is_integer(Integer) -> + erl_syntax:integer(Maximum); + _Float -> + erl_syntax:float(Maximum) + end, + ExclusiveMaximum = maps:get(exclusive_maximum, Schema, false), + Operator = + case ExclusiveMaximum of + true -> + erl_syntax:operator('<'); + _false -> + erl_syntax:operator('=<') + end, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:variable('Val'), + Operator, + MaximumSt + ), + [erl_syntax:atom(true)] + ), + ComparisonTerm = + case ExclusiveMaximum of + true -> + "than"; + false -> + "or equal to" + end, + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Number is not lower ~s ~p", [ComparisonTerm, Maximum]) + ) + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ); +is_valid_(_Prefix, multiple_of, _MultipleOf, _Schema) -> + undefined; +is_valid_(_Prefix, _Keyword, _Value, _Schema) -> + undefined. diff --git a/src/ndto_generator/ndto_generator_integer.erl b/src/ndto_generator/ndto_generator_integer.erl new file mode 100644 index 0000000..b172d90 --- /dev/null +++ b/src/ndto_generator/ndto_generator_integer.erl @@ -0,0 +1,210 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_integer). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := integer} = Schema) -> + FunName = Prefix, + ExtraFuns = lists:foldl( + fun(Keyword, Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + Value -> + case is_valid_(FunName, Keyword, Value, Schema) of + undefined -> + Acc; + NewIsValidFun -> + [NewIsValidFun | Acc] + end + end + end, + [], + [ + minimum, + maximum, + multiple_of + ] + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- ExtraFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(integer), + ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso') + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not an integer"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, ExtraFuns}. + +-spec is_valid_(Prefix, Keyword, Value, Schema) -> Result when + Prefix :: binary(), + Keyword :: atom(), + Value :: term(), + Schema :: ndto:integer_schema(), + Result :: undefined | erl_syntax:syntaxTree(). +is_valid_(Prefix, minimum, Minimum, Schema) -> + FunName = <>, + MinimumSt = + case Minimum of + Integer when is_integer(Integer) -> + erl_syntax:integer(Minimum); + _Float -> + erl_syntax:float(Minimum) + end, + ExclusiveMinimum = maps:get(exclusive_minimum, Schema, false), + Operator = + case ExclusiveMinimum of + true -> + erl_syntax:operator('>'); + _false -> + erl_syntax:operator('>=') + end, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:variable('Val'), + Operator, + MinimumSt + ), + [erl_syntax:atom(true)] + ), + ComparisonTerm = + case ExclusiveMinimum of + true -> + "than"; + false -> + "or equal to" + end, + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Value is not a number greater ~s ~p", [ComparisonTerm, Minimum]) + ) + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ); +is_valid_(Prefix, maximum, Maximum, Schema) -> + FunName = <>, + MaximumSt = + case Maximum of + Integer when is_integer(Integer) -> + erl_syntax:integer(Maximum); + _Float -> + erl_syntax:float(Maximum) + end, + ExclusiveMaximum = maps:get(exclusive_maximum, Schema, false), + Operator = + case ExclusiveMaximum of + true -> + erl_syntax:operator('<'); + _false -> + erl_syntax:operator('=<') + end, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + erl_syntax:infix_expr( + erl_syntax:variable('Val'), + Operator, + MaximumSt + ), + [erl_syntax:atom(true)] + ), + ComparisonTerm = + case ExclusiveMaximum of + true -> + "than"; + false -> + "or equal to" + end, + FalseClause = ndto_generator:false_clause( + FunName, + unicode:characters_to_list( + io_lib:format("Number is not lower ~s ~p", [ComparisonTerm, Maximum]) + ) + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause, FalseClause] + ); +is_valid_(Prefix, multiple_of, MultipleOf, _Schema) -> + FunName = <>, + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Val'), + erl_syntax:operator('rem'), + erl_syntax:integer(MultipleOf) + ), + [ + erl_syntax:clause( + [erl_syntax:integer(0)], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [erl_syntax:variable('_')], + none, + [ + ndto_generator:false_return( + FunName, + unicode:characters_to_list( + io_lib:format("Value is not multiple of ~p", [MultipleOf]) + ) + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(_Prefix, _Keyword, _Value, _Schema) -> + undefined. diff --git a/src/ndto_generator/ndto_generator_intersection.erl b/src/ndto_generator/ndto_generator_intersection.erl new file mode 100644 index 0000000..572e218 --- /dev/null +++ b/src/ndto_generator/ndto_generator_intersection.erl @@ -0,0 +1,123 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_intersection). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> + FunName = <>, + {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( + fun(Subschema, {RawIdx, IsValidFunsAcc, ExtraFunsAcc}) -> + Idx = erlang:integer_to_binary(RawIdx), + {IsValidFun, ExtraFuns} = ndto_generator:is_valid( + <>, Subschema + ), + { + RawIdx + 1, + [IsValidFun | IsValidFunsAcc], + ExtraFuns ++ ExtraFunsAcc + } + end, + {0, [], []}, + Subschemas + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- IsValidFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erlang:hd(ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso', true)), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:tuple([ + erl_syntax:variable('AllOfReasonPath'), + erl_syntax:variable('ReasonMsg') + ]), + erl_syntax:variable('N') + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(erlang:binary_to_atom(FunName)), + erl_syntax:application( + erl_syntax:atom('unicode'), + erl_syntax:atom('characters_to_binary'), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + "Value is not matching all conditions. Condition ~p failed because of schema path '~ts' : ~ts" + ), + erl_syntax:list([ + erl_syntax:variable('N'), + erl_syntax:variable('AllOfReasonPath'), + erl_syntax:variable('ReasonMsg') + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, IsValidFuns ++ ExtraFuns}. diff --git a/src/ndto_generator/ndto_generator_object.erl b/src/ndto_generator/ndto_generator_object.erl new file mode 100644 index 0000000..9c0a942 --- /dev/null +++ b/src/ndto_generator/ndto_generator_object.erl @@ -0,0 +1,1026 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_object). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := object} = Schema) -> + FunName = Prefix, + {IsValidFuns, ExtraFuns} = + lists:foldl( + fun(Keyword, {IsValidFunsAcc, ExtraFunsAcc} = Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + _Value -> + case is_valid_(<>, Keyword, Schema) of + {undefined, _EmptyList} -> + Acc; + {NewIsValidFun, NewExtraFuns} -> + { + [NewIsValidFun | IsValidFunsAcc], + NewExtraFuns ++ ExtraFunsAcc + } + end + end + end, + {[], []}, + [ + properties, + required, + min_properties, + max_properties, + pattern_properties, + additional_properties + ] + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- IsValidFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(object), + ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso') + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not an object"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, IsValidFuns ++ ExtraFuns}. + +-spec is_valid_(Prefix, Keyword, Schema) -> Result when + Prefix :: binary(), + Keyword :: atom(), + Schema :: ndto:object_schema(), + Result :: {Fun, ExtraFuns}, + Fun :: erl_syntax:syntaxTree() | undefined, + ExtraFuns :: [erl_syntax:syntaxTree()]. +is_valid_(Prefix, properties, #{properties := Properties}) -> + FunName = <>, + {PropertiesFuns, ExtraFuns} = maps:fold( + fun(PropertyName, Property, {IsValidFunsAcc, ExtraFunsAcc}) -> + {IsValidPropertyFun, ExtraPropertyFuns} = + ndto_generator:is_valid(<>, Property#{ + optional => true + }), + { + [{PropertyName, IsValidPropertyFun} | IsValidFunsAcc], + ExtraFunsAcc ++ ExtraPropertyFuns + } + end, + {[], []}, + Properties + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(PropertyFun), + erl_syntax:integer(erl_syntax:function_arity(PropertyFun)) + ) + ), + erl_syntax:list([ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(get), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string(erlang:binary_to_list(PropertyName)) + ) + ]), + erl_syntax:variable('Val'), + erl_syntax:atom(undefined) + ] + ) + ]) + ]) + || {PropertyName, PropertyFun} <- PropertiesFuns + ], + FunBody = ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso'), + + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + FunBody + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {_PropertyNames, IsValidFuns} = lists:unzip(PropertiesFuns), + {Fun, IsValidFuns ++ ExtraFuns}; +is_valid_(Prefix, required, #{required := Required}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(find), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('Property')], + none, + [ + erl_syntax:prefix_expr( + erl_syntax:operator('not'), + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(is_key), + [ + erl_syntax:variable('Property'), + erl_syntax:variable('Val') + ] + ) + ) + ] + ) + ]), + erl_syntax:list( + lists:map( + fun(PropertyName) -> + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string(erlang:binary_to_list(PropertyName)) + ) + ]) + end, + Required + ) + ) + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none') + ]) + ], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:variable('MissingProperty') + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(erlang:binary_to_list(Prefix)), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(list_to_binary), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + lists:flatten( + io_lib:format( + "~ts is missing required property ~~p", + [Prefix] + ) + ) + ), + erl_syntax:list([ + erl_syntax:variable( + 'MissingProperty' + ) + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}; +is_valid_(Prefix, min_properties, #{min_properties := MinProperties}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(length), + [ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ) + ] + ), + [ + erl_syntax:clause( + [erl_syntax:variable('N')], + [ + erl_syntax:infix_expr( + erl_syntax:variable('N'), + erl_syntax:operator('>='), + erl_syntax:integer(MinProperties) + ) + ], + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [erl_syntax:variable('_')], + none, + [ + ndto_generator:false_return( + FunName, + unicode:characters_to_list( + io_lib:format( + "Object has less properties than required minimum (~p)", [ + MinProperties + ] + ) + ) + ) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}; +is_valid_(Prefix, max_properties, #{max_properties := MaxProperties}) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(length), + [ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ) + ] + ), + [ + erl_syntax:clause( + [erl_syntax:variable('N')], + [ + erl_syntax:infix_expr( + erl_syntax:variable('N'), + erl_syntax:operator('=<'), + erl_syntax:integer(MaxProperties) + ) + ], + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [erl_syntax:variable('_')], + none, + [ + ndto_generator:false_return( + FunName, + unicode:characters_to_list( + io_lib:format( + "Object has more properties than allowed maximum (~p)", [ + MaxProperties + ] + ) + ) + ) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}; +is_valid_(Prefix, pattern_properties, #{pattern_properties := PatternProperties}) -> + FunName = <>, + {IsValidPatterns, ExtraFuns} = lists:foldl( + fun({PropertyPattern, PropertySchema}, {IsValidPatternsAcc, ExtraFunsAcc}) -> + {IsValidFun, ExtraFuns} = ndto_generator:is_valid( + <>, PropertySchema + ), + {[{PropertyPattern, IsValidFun} | IsValidPatternsAcc], ExtraFuns ++ ExtraFunsAcc} + end, + {[], []}, + maps:to_list(PatternProperties) + ), + Conditions = lists:map( + fun({PropertyPattern, IsValidFun}) -> + Filter = + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(filter), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('_PropertyValue') + ]) + ], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(re), + erl_syntax:atom(run), + [ + erl_syntax:variable('PropertyName'), + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list( + PropertyPattern + ) + ) + ) + ]) + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom(match), + erl_syntax:variable('_Captured') + ]) + ], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('_nomatch')], + none, + [erl_syntax:atom(false)] + ) + ] + ) + ] + ) + ]), + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(to_list), + [erl_syntax:variable('Val')] + ) + ] + ), + FunBody = + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(find), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('PropertyValue') + ]) + ], + none, + [ + erl_syntax:match_expr( + erl_syntax:variable('Result'), + erl_syntax:application( + erl_syntax:function_name(IsValidFun), + [erl_syntax:variable('PropertyValue')] + ) + ), + erl_syntax:case_expr( + erl_syntax:variable('Result'), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('false')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:tuple([ + erl_syntax:tuple([ + erl_syntax:variable( + 'PropertyName' + ), + erl_syntax:variable( + 'PropertyValue' + ) + ]), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ]) + ] + ) + ] + ) + ] + ) + ]), + Filter + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none') + ]) + ], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:tuple([ + erl_syntax:tuple([ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('_PropertyValue') + ]), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(list_to_binary), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + "Property \"~ts\" failed validation: ~ts" + ), + erl_syntax:list([ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('Reason') + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ), + erl_syntax:tuple([ + erl_syntax:fun_expr([ + erl_syntax:clause(none, [FunBody]) + ]), + erl_syntax:list([]) + ]) + end, + IsValidPatterns + ), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + ndto_generator:chain_conditions(FunName, Conditions, 'andalso') + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + IsValidFuns = [IsValidFun || {_PatternName, IsValidFun} <- IsValidPatterns], + {Fun, IsValidFuns ++ ExtraFuns}; +is_valid_( + Prefix, + additional_properties, + #{additional_properties := false} = Schema +) -> + FunName = <>, + Properties = maps:get(properties, Schema, #{}), + PatternProperties = maps:get(pattern_properties, Schema, #{}), + PatternPropertiesList = erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(filter), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('PropertyName')], + none, + [ + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(any), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('Pattern')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(re), + erl_syntax:atom(run), + [ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('Pattern') + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom(match), + erl_syntax:variable('_Captured') + ]) + ], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('_nomatch')], + none, + [erl_syntax:atom(false)] + ) + ] + ) + ] + ) + ]), + erl_syntax:list( + lists:map( + fun(PropertyPattern) -> + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(PropertyPattern) + ) + ) + ]) + end, + maps:keys(PatternProperties) + ) + ) + ] + ) + ] + ) + ]), + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ) + ] + ), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(to_list), + [ + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(subtract), + [ + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(from_list), + [ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ) + ] + ), + erl_syntax:application( + erl_syntax:atom(sets), + erl_syntax:atom(from_list), + [ + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(append), + [ + erl_syntax:list( + lists:map( + fun(PropertyName) -> + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list( + PropertyName + ) + ) + ) + ]) + end, + maps:keys(Properties) + ) + ), + PatternPropertiesList + ] + ) + ] + ) + ] + ) + ] + ), + [ + erl_syntax:clause( + [erl_syntax:list([])], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [erl_syntax:variable('UnsupportedKeys')], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(erlang:binary_to_list(FunName)), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(list_to_binary), + [ + erl_syntax:application( + erl_syntax:atom(io_lib), + erl_syntax:atom(format), + [ + erl_syntax:string( + "Object has unsupported keys: ~ts" + ), + erl_syntax:list([ + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(format_properties), + [ + erl_syntax:variable( + 'UnsupportedKeys' + ) + ] + ) + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}; +is_valid_( + Prefix, + additional_properties, + #{additional_properties := AdditionalProperties} = Schema +) -> + FunName = <>, + {IsValidFun, ExtraFuns} = ndto_generator:is_valid( + <>, AdditionalProperties + ), + Properties = maps:get(properties, Schema, #{}), + PatternProperties = maps:get(pattern_properties, Schema, #{}), + PatternPropertiesList = erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(filter), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('PropertyName')], + none, + [ + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(any), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('Pattern')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(re), + erl_syntax:atom(run), + [ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('Pattern') + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom(match), + erl_syntax:variable('_Captured') + ]) + ], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('_nomatch')], + none, + [erl_syntax:atom(false)] + ) + ] + ) + ] + ) + ]), + erl_syntax:list( + lists:map( + fun(PropertyPattern) -> + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(PropertyPattern) + ) + ) + ]) + end, + maps:keys(PatternProperties) + ) + ) + ] + ) + ] + ) + ]), + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ) + ] + ), + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ndto_validation), + erl_syntax:atom(find), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:variable('Property')], + none, + [ + erl_syntax:match_expr( + erl_syntax:variable('Result'), + erl_syntax:application( + erl_syntax:function_name(IsValidFun), + [ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(get), + [ + erl_syntax:variable('Property'), + erl_syntax:variable('Val') + ] + ) + ] + ) + ), + erl_syntax:case_expr( + erl_syntax:variable('Result'), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('false')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:tuple([ + erl_syntax:variable('Property'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ]) + ] + ) + ] + ) + ] + ) + ]), + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(subtract), + [ + erl_syntax:application( + erl_syntax:atom(maps), + erl_syntax:atom(keys), + [erl_syntax:variable('Val')] + ), + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(append), + [ + erl_syntax:list( + lists:map( + fun(PropertyName) -> + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(PropertyName) + ) + ) + ]) + end, + maps:keys(Properties) + ) + ), + PatternPropertiesList + ] + ) + ] + ) + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none') + ]) + ], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('true'), + erl_syntax:tuple([ + erl_syntax:variable('PropertyName'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:variable('Reason') + ]) + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:variable('Prefix'), + erl_syntax:application( + erl_syntax:atom(erlang), + erl_syntax:atom(list_to_binary), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + "Property \"~ts\" failed validation: ~ts" + ), + erl_syntax:list([ + erl_syntax:variable('PropertyName'), + erl_syntax:variable('Reason') + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, [IsValidFun | ExtraFuns]}; +is_valid_(_Prefix, additional_properties, _Schema) -> + {undefined, []}. diff --git a/src/ndto_generator/ndto_generator_ref.erl b/src/ndto_generator/ndto_generator_ref.erl new file mode 100644 index 0000000..b7944df --- /dev/null +++ b/src/ndto_generator/ndto_generator_ref.erl @@ -0,0 +1,51 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_ref). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{ref := Ref} = Schema) -> + FunName = <>, + DTO = erlang:binary_to_atom(Ref), + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:application( + erl_syntax:atom(DTO), + erl_syntax:atom(is_valid), + [ + erl_syntax:variable('Val') + ] + ) + ] + ), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, []}. diff --git a/src/ndto_generator/ndto_generator_string.erl b/src/ndto_generator/ndto_generator_string.erl new file mode 100644 index 0000000..d086c52 --- /dev/null +++ b/src/ndto_generator/ndto_generator_string.erl @@ -0,0 +1,395 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_string). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{type := string} = Schema) -> + FunName = Prefix, + ExtraFuns = + lists:foldl( + fun(Keyword, Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + Value -> + case is_valid_(FunName, Keyword, Value) of + undefined -> + Acc; + Fun -> + [Fun | Acc] + end + end + end, + [], + [ + min_length, + max_length, + format, + pattern + ] + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- ExtraFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + ndto_generator:type_guard(string), + ndto_generator:chain_conditions(FunName, ValidationConditions, 'andalso') + ), + FalseClause = ndto_generator:false_clause(<>, "Value is not a string"), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause, FalseClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, ExtraFuns}. + +-spec is_valid_(Prefix, Keyword, Value) -> Result when + Prefix :: binary(), + Keyword :: atom(), + Value :: term(), + Result :: undefined | erl_syntax:syntaxTree(). +is_valid_(Prefix, min_length, MinLength) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ + erl_syntax:variable('Val') + ]), + [ + erl_syntax:clause( + [erl_syntax:variable('Length')], + erl_syntax:infix_expr( + erl_syntax:variable('Length'), + erl_syntax:operator('>='), + erl_syntax:integer(MinLength) + ), + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('Length')], + none, + [ + ndto_generator:false_return( + FunName, + lists:flatten( + io_lib:format( + "String length ~p is less than ~p", + ["Length", MinLength] + ) + ) + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(Prefix, max_length, MaxLength) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ + erl_syntax:variable('Val') + ]), + [ + erl_syntax:clause( + [erl_syntax:variable('Length')], + erl_syntax:infix_expr( + erl_syntax:variable('Length'), + erl_syntax:operator('=<'), + erl_syntax:integer(MaxLength) + ), + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('Length')], + none, + [ + ndto_generator:false_return( + FunName, + lists:flatten( + io_lib:format( + "String length ~p is greater than ~p", + ["Length", MaxLength] + ) + ) + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(Prefix, pattern, Pattern) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(re), + erl_syntax:atom(run), + [ + erl_syntax:variable('Val'), + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string(erlang:binary_to_list(Pattern)) + ) + ]) + ] + ), + [ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom(match), + erl_syntax:variable('_Match') + ]) + ], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:atom(nomatch)], + none, + [ + ndto_generator:false_return( + FunName, + lists:flatten( + io_lib:format("String does not match pattern ~s", [Pattern]) + ) + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(Prefix, format, iso8601) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ncalendar), + erl_syntax:atom(is_valid), + [ + erl_syntax:atom(iso8601), + erl_syntax:variable('Val') + ] + ), + [ + erl_syntax:clause( + [erl_syntax:atom(true)], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:atom(false)], + none, + [ + ndto_generator:false_return( + FunName, + "String is not a valid ISO8601 date" + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(Prefix, format, base64) -> + FunName = <>, + TrueClause = erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erl_syntax:application(erl_syntax:atom(string), erl_syntax:atom(length), [ + erl_syntax:variable('Val') + ]), + [ + erl_syntax:clause( + [erl_syntax:variable('Length')], + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Length'), + erl_syntax:operator('rem'), + erl_syntax:integer(4) + ), + erl_syntax:operator('=:='), + erl_syntax:integer(0) + ), + [ + erl_syntax:match_expr( + erl_syntax:variable('Unpad'), + erl_syntax:application( + erl_syntax:atom(string), + erl_syntax:atom(trim), + [ + erl_syntax:variable('Val'), + erl_syntax:atom(trailing), + erl_syntax:list([erl_syntax:char($=)]) + ] + ) + ), + erl_syntax:application( + erl_syntax:atom(lists), + erl_syntax:atom(all), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [erl_syntax:integer(43)], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('Char')], + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('>='), + erl_syntax:integer(48) + ), + erl_syntax:operator('andalso'), + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('=<'), + erl_syntax:integer(57) + ) + ), + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:integer(47)], + none, + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('Char')], + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('>='), + erl_syntax:integer(65) + ), + erl_syntax:operator('andalso'), + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('=<'), + erl_syntax:integer(90) + ) + ), + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('Char')], + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('>='), + erl_syntax:integer(97) + ), + erl_syntax:operator('andalso'), + erl_syntax:infix_expr( + erl_syntax:variable('Char'), + erl_syntax:operator('=<'), + erl_syntax:integer(122) + ) + ), + [erl_syntax:atom(true)] + ), + erl_syntax:clause( + [erl_syntax:variable('_Char')], + none, + [erl_syntax:atom(false)] + ) + ]), + erl_syntax:application( + erl_syntax:atom(string), + erl_syntax:atom(to_graphemes), + [erl_syntax:variable('Unpad')] + ) + ] + ) + ] + ), + erl_syntax:clause( + [erl_syntax:variable('_Length')], + none, + [ + ndto_generator:false_return( + FunName, + "String length is not a multiple of 4" + ) + ] + ) + ] + ) + ] + ), + erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ); +is_valid_(_Prefix, format, _Format) -> + undefined. diff --git a/src/ndto_generator/ndto_generator_symmetric_difference.erl b/src/ndto_generator/ndto_generator_symmetric_difference.erl new file mode 100644 index 0000000..f2c8b96 --- /dev/null +++ b/src/ndto_generator/ndto_generator_symmetric_difference.erl @@ -0,0 +1,145 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_symmetric_difference). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{one_of := Subschemas} = Schema) when is_list(Subschemas) -> + FunName = <>, + {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( + fun(Subschema, {Idx, IsValidFunsAcc, ExtraFunsAcc}) -> + {IsValidFun, ExtraFuns} = ndto_generator:is_valid( + <>, Subschema + ), + { + Idx + 1, + [IsValidFun | IsValidFunsAcc], + ExtraFuns ++ ExtraFunsAcc + } + end, + {0, [], []}, + Subschemas + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- IsValidFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erlang:hd(ndto_generator:chain_conditions(FunName, ValidationConditions, 'xor')), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none_matched') + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(binary_to_atom(FunName)), + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + "Value is not matching exactly one condition. None matched." + ) + ) + ]) + ]) + ]) + ] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom('many_matched'), + erl_syntax:tuple([ + erl_syntax:variable('First'), + erl_syntax:variable('Second') + ]) + ]) + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(binary_to_atom(FunName)), + erl_syntax:application( + erl_syntax:atom('unicode'), + erl_syntax:atom('characters_to_binary'), + [ + erl_syntax:application( + erl_syntax:atom('io_lib'), + erl_syntax:atom('format'), + [ + erl_syntax:string( + "Value is not matching exactly one condition. More than one (conditions ~p and ~p) matched." + ), + erl_syntax:list([ + erl_syntax:variable('Second'), + erl_syntax:variable('First') + ]) + ] + ) + ] + ) + ]) + ]) + ] + ) + ] + ) + ] + ), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, IsValidFuns ++ ExtraFuns}. diff --git a/src/ndto_generator/ndto_generator_union.erl b/src/ndto_generator/ndto_generator_union.erl new file mode 100644 index 0000000..80b0884 --- /dev/null +++ b/src/ndto_generator/ndto_generator_union.erl @@ -0,0 +1,104 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_union). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, #{any_of := Subschemas} = Schema) when is_list(Subschemas) -> + FunName = <>, + {_Idx, IsValidFuns, ExtraFuns} = lists:foldl( + fun(Subschema, {RawIdx, IsValidFunsAcc, ExtraFunsAcc}) -> + Idx = erlang:integer_to_binary(RawIdx), + {IsValidFun, ExtraFuns} = ndto_generator:is_valid( + <>, Subschema + ), + { + RawIdx + 1, + [IsValidFun | IsValidFunsAcc], + ExtraFuns ++ ExtraFunsAcc + } + end, + {0, [], []}, + Subschemas + ), + ValidationConditions = [ + erl_syntax:tuple([ + erl_syntax:implicit_fun( + erl_syntax:arity_qualifier( + erl_syntax:function_name(Fun), + erl_syntax:integer(erl_syntax:function_arity(Fun)) + ) + ), + erl_syntax:list([erl_syntax:variable('Val')]) + ]) + || Fun <- IsValidFuns + ], + OptionalClause = ndto_generator:optional_clause(Schema), + NullClause = ndto_generator:null_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + none, + [ + erl_syntax:case_expr( + erlang:hd(ndto_generator:chain_conditions(FunName, ValidationConditions, 'orelse', true)), + [ + erl_syntax:clause( + [erl_syntax:atom('true')], + none, + [erl_syntax:atom('true')] + ), + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:atom('none_matched') + ]) + ], + none, + [ + erl_syntax:tuple([ + erl_syntax:atom('false'), + erl_syntax:tuple([ + erl_syntax:atom(erlang:binary_to_atom(FunName)), + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + "Value is not matching at least one condition. None matched." + ) + ) + ]) + ]) + ]) + ] + ) + ] + ) + ] + ), + Clauses = ndto_generator:clauses([OptionalClause, NullClause, TrueClause]), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + Clauses + ), + {Fun, IsValidFuns ++ ExtraFuns}. diff --git a/src/ndto_generator/ndto_generator_universal.erl b/src/ndto_generator/ndto_generator_universal.erl new file mode 100644 index 0000000..61fcbd8 --- /dev/null +++ b/src/ndto_generator/ndto_generator_universal.erl @@ -0,0 +1,39 @@ +%%% Copyright 2023 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @private +-module(ndto_generator_universal). + +%%% BEHAVIOR +-behaviour(ndto_generator). + +%%% EXTERNAL EXPORTS +-export([is_valid/2]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +is_valid(Prefix, _Schema) -> + FunName = <>, + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('_Val')], + none, + [erl_syntax:atom(true)] + ), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + [TrueClause] + ), + {Fun, []}. From 8afbc5dd7c29d4b2108fcb0c6d2fcbb8fe40b08f Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Wed, 25 Jun 2025 14:31:03 +0200 Subject: [PATCH 2/2] refactor: move large clauses to independent functions --- src/ndto_generator/ndto_generator_array.erl | 43 +++++++++++---- src/ndto_generator/ndto_generator_float.erl | 23 +++++--- src/ndto_generator/ndto_generator_integer.erl | 26 ++++++--- src/ndto_generator/ndto_generator_object.erl | 55 ++++++++++++------- src/ndto_generator/ndto_generator_string.erl | 31 ++++++++--- 5 files changed, 123 insertions(+), 55 deletions(-) diff --git a/src/ndto_generator/ndto_generator_array.erl b/src/ndto_generator/ndto_generator_array.erl index 5323967..6ea3a7e 100644 --- a/src/ndto_generator/ndto_generator_array.erl +++ b/src/ndto_generator/ndto_generator_array.erl @@ -80,14 +80,30 @@ is_valid(Prefix, #{type := array} = Schema) -> ), {Fun, IsValidFuns ++ ExtraFuns}. --spec is_valid_(Prefix, Keyword, Value) -> Result when +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- +-spec is_valid_(Prefix, Keyword, Schema) -> Result when Prefix :: binary(), Keyword :: atom(), - Value :: term(), + Schema :: ndto:array_schema(), Result :: {Fun, ExtraFuns}, Fun :: erl_syntax:syntaxTree() | undefined, ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_(Prefix, items, #{items := Items} = Schema) when is_map(Items) -> +is_valid_(Prefix, items, Schema) -> + is_valid_items(Prefix, Schema); +is_valid_(Prefix, min_items, Schema) -> + is_valid_min_items(Prefix, Schema); +is_valid_(Prefix, max_items, Schema) -> + is_valid_max_items(Prefix, Schema); +is_valid_(Prefix, unique_items, #{unique_items := true} = Schema) -> + is_valid_unique_items_true(Prefix, Schema); +is_valid_(_Prefix, unique_items, #{unique_items := false}) -> + {undefined, []}; +is_valid_(_Prefix, _Keyword, _Schema) -> + {undefined, []}. + +is_valid_items(Prefix, #{items := Items} = Schema) when is_map(Items) -> FunName = <>, {IsValidFun, ExtraFuns} = ndto_generator:is_valid(<>, Items), OptionalClause = ndto_generator:optional_clause(Schema), @@ -191,7 +207,7 @@ is_valid_(Prefix, items, #{items := Items} = Schema) when is_map(Items) -> ndto_generator:clauses([OptionalClause, TrueClause]) ), {Fun, [IsValidFun | ExtraFuns]}; -is_valid_(Prefix, items, #{items := Items} = Schema) when is_list(Items) -> +is_valid_items(Prefix, #{items := Items} = Schema) when is_list(Items) -> FunName = <>, {_Size, IsValidFuns, ExtraFuns} = lists:foldl( fun(Item, {Idx, IsValidFunsAcc, ExtraFunsAcc}) -> @@ -439,7 +455,10 @@ is_valid_(Prefix, items, #{items := Items} = Schema) when is_list(Items) -> [TrueClause] ), {Fun, ExtraFuns ++ [IsValidAdditionalItemsFun | AdditionalItemsExtraFuns]}; -is_valid_(Prefix, min_items, #{min_items := MinItems}) -> +is_valid_items(_Prefix, _Schema) -> + {undefined, []}. + +is_valid_min_items(Prefix, #{min_items := MinItems}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -460,8 +479,9 @@ is_valid_(Prefix, min_items, #{min_items := MinItems}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] ), - {Fun, []}; -is_valid_(Prefix, max_items, #{max_items := MaxItems}) -> + {Fun, []}. + +is_valid_max_items(Prefix, #{max_items := MaxItems}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -482,8 +502,9 @@ is_valid_(Prefix, max_items, #{max_items := MaxItems}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] ), - {Fun, []}; -is_valid_(Prefix, unique_items, #{unique_items := true}) -> + {Fun, []}. + +is_valid_unique_items_true(Prefix, #{unique_items := true}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -531,6 +552,4 @@ is_valid_(Prefix, unique_items, #{unique_items := true}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ), - {Fun, []}; -is_valid_(_Prefix, unique_items, #{unique_items := false}) -> - {undefined, []}. + {Fun, []}. diff --git a/src/ndto_generator/ndto_generator_float.erl b/src/ndto_generator/ndto_generator_float.erl index c1a236d..f37d611 100644 --- a/src/ndto_generator/ndto_generator_float.erl +++ b/src/ndto_generator/ndto_generator_float.erl @@ -75,6 +75,9 @@ is_valid(Prefix, #{type := float} = Schema) -> ), {Fun, ExtraFuns}. +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- -spec is_valid_(Prefix, Keyword, Value, Schema) -> Result when Prefix :: binary(), Keyword :: atom(), @@ -82,6 +85,15 @@ is_valid(Prefix, #{type := float} = Schema) -> Schema :: ndto:float_schema(), Result :: undefined | erl_syntax:syntaxTree(). is_valid_(Prefix, minimum, Minimum, Schema) -> + is_valid_minimum(Prefix, Minimum, Schema); +is_valid_(Prefix, maximum, Maximum, Schema) -> + is_valid_maximum(Prefix, Maximum, Schema); +is_valid_(_Prefix, multiple_of, _MultipleOf, _Schema) -> + undefined; +is_valid_(_Prefix, _Keyword, _Value, _Schema) -> + undefined. + +is_valid_minimum(Prefix, Minimum, Schema) -> FunName = <>, MinimumSt = case Minimum of @@ -123,8 +135,9 @@ is_valid_(Prefix, minimum, Minimum, Schema) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] - ); -is_valid_(Prefix, maximum, Maximum, Schema) -> + ). + +is_valid_maximum(Prefix, Maximum, Schema) -> FunName = <>, MaximumSt = case Maximum of @@ -166,8 +179,4 @@ is_valid_(Prefix, maximum, Maximum, Schema) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] - ); -is_valid_(_Prefix, multiple_of, _MultipleOf, _Schema) -> - undefined; -is_valid_(_Prefix, _Keyword, _Value, _Schema) -> - undefined. + ). diff --git a/src/ndto_generator/ndto_generator_integer.erl b/src/ndto_generator/ndto_generator_integer.erl index b172d90..a9b9c00 100644 --- a/src/ndto_generator/ndto_generator_integer.erl +++ b/src/ndto_generator/ndto_generator_integer.erl @@ -75,6 +75,9 @@ is_valid(Prefix, #{type := integer} = Schema) -> ), {Fun, ExtraFuns}. +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- -spec is_valid_(Prefix, Keyword, Value, Schema) -> Result when Prefix :: binary(), Keyword :: atom(), @@ -82,6 +85,15 @@ is_valid(Prefix, #{type := integer} = Schema) -> Schema :: ndto:integer_schema(), Result :: undefined | erl_syntax:syntaxTree(). is_valid_(Prefix, minimum, Minimum, Schema) -> + is_valid_minimum(Prefix, Minimum, Schema); +is_valid_(Prefix, maximum, Maximum, Schema) -> + is_valid_maximum(Prefix, Maximum, Schema); +is_valid_(Prefix, multiple_of, MultipleOf, _Schema) -> + is_valid_multiple_of(Prefix, MultipleOf); +is_valid_(_Prefix, _Keyword, _Value, _Schema) -> + undefined. + +is_valid_minimum(Prefix, Minimum, Schema) -> FunName = <>, MinimumSt = case Minimum of @@ -123,8 +135,9 @@ is_valid_(Prefix, minimum, Minimum, Schema) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] - ); -is_valid_(Prefix, maximum, Maximum, Schema) -> + ). + +is_valid_maximum(Prefix, Maximum, Schema) -> FunName = <>, MaximumSt = case Maximum of @@ -166,8 +179,9 @@ is_valid_(Prefix, maximum, Maximum, Schema) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] - ); -is_valid_(Prefix, multiple_of, MultipleOf, _Schema) -> + ). + +is_valid_multiple_of(Prefix, MultipleOf) -> FunName = <>, TrueClause = erl_syntax:clause( @@ -205,6 +219,4 @@ is_valid_(Prefix, multiple_of, MultipleOf, _Schema) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] - ); -is_valid_(_Prefix, _Keyword, _Value, _Schema) -> - undefined. + ). diff --git a/src/ndto_generator/ndto_generator_object.erl b/src/ndto_generator/ndto_generator_object.erl index 9c0a942..440d01e 100644 --- a/src/ndto_generator/ndto_generator_object.erl +++ b/src/ndto_generator/ndto_generator_object.erl @@ -82,6 +82,9 @@ is_valid(Prefix, #{type := object} = Schema) -> ), {Fun, IsValidFuns ++ ExtraFuns}. +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- -spec is_valid_(Prefix, Keyword, Schema) -> Result when Prefix :: binary(), Keyword :: atom(), @@ -89,7 +92,20 @@ is_valid(Prefix, #{type := object} = Schema) -> Result :: {Fun, ExtraFuns}, Fun :: erl_syntax:syntaxTree() | undefined, ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_(Prefix, properties, #{properties := Properties}) -> +is_valid_(Prefix, properties, Schema) -> + is_valid_properties(Prefix, Schema); +is_valid_(Prefix, required, Schema) -> + is_valid_required(Prefix, Schema); +is_valid_(Prefix, min_properties, Schema) -> + is_valid_min_properties(Prefix, Schema); +is_valid_(Prefix, max_properties, Schema) -> + is_valid_max_properties(Prefix, Schema); +is_valid_(Prefix, pattern_properties, Schema) -> + is_valid_pattern_properties(Prefix, Schema); +is_valid_(Prefix, additional_properties, Schema) -> + is_valid_additional_properties(Prefix, Schema). + +is_valid_properties(Prefix, #{properties := Properties}) -> FunName = <>, {PropertiesFuns, ExtraFuns} = maps:fold( fun(PropertyName, Property, {IsValidFunsAcc, ExtraFunsAcc}) -> @@ -143,8 +159,9 @@ is_valid_(Prefix, properties, #{properties := Properties}) -> [TrueClause] ), {_PropertyNames, IsValidFuns} = lists:unzip(PropertiesFuns), - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid_(Prefix, required, #{required := Required}) -> + {Fun, IsValidFuns ++ ExtraFuns}. + +is_valid_required(Prefix, #{required := Required}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -249,8 +266,9 @@ is_valid_(Prefix, required, #{required := Required}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ), - {Fun, []}; -is_valid_(Prefix, min_properties, #{min_properties := MinProperties}) -> + {Fun, []}. + +is_valid_min_properties(Prefix, #{min_properties := MinProperties}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -304,8 +322,9 @@ is_valid_(Prefix, min_properties, #{min_properties := MinProperties}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ), - {Fun, []}; -is_valid_(Prefix, max_properties, #{max_properties := MaxProperties}) -> + {Fun, []}. + +is_valid_max_properties(Prefix, #{max_properties := MaxProperties}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -359,8 +378,9 @@ is_valid_(Prefix, max_properties, #{max_properties := MaxProperties}) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ), - {Fun, []}; -is_valid_(Prefix, pattern_properties, #{pattern_properties := PatternProperties}) -> + {Fun, []}. + +is_valid_pattern_properties(Prefix, #{pattern_properties := PatternProperties}) -> FunName = <>, {IsValidPatterns, ExtraFuns} = lists:foldl( fun({PropertyPattern, PropertySchema}, {IsValidPatternsAcc, ExtraFunsAcc}) -> @@ -581,12 +601,9 @@ is_valid_(Prefix, pattern_properties, #{pattern_properties := PatternProperties} [TrueClause] ), IsValidFuns = [IsValidFun || {_PatternName, IsValidFun} <- IsValidPatterns], - {Fun, IsValidFuns ++ ExtraFuns}; -is_valid_( - Prefix, - additional_properties, - #{additional_properties := false} = Schema -) -> + {Fun, IsValidFuns ++ ExtraFuns}. + +is_valid_additional_properties(Prefix, #{additional_properties := false} = Schema) -> FunName = <>, Properties = maps:get(properties, Schema, #{}), PatternProperties = maps:get(pattern_properties, Schema, #{}), @@ -774,11 +791,7 @@ is_valid_( [TrueClause] ), {Fun, []}; -is_valid_( - Prefix, - additional_properties, - #{additional_properties := AdditionalProperties} = Schema -) -> +is_valid_additional_properties(Prefix, #{additional_properties := AdditionalProperties} = Schema) -> FunName = <>, {IsValidFun, ExtraFuns} = ndto_generator:is_valid( <>, AdditionalProperties @@ -1022,5 +1035,5 @@ is_valid_( [TrueClause] ), {Fun, [IsValidFun | ExtraFuns]}; -is_valid_(_Prefix, additional_properties, _Schema) -> +is_valid_additional_properties(_Prefix, _Schema) -> {undefined, []}. diff --git a/src/ndto_generator/ndto_generator_string.erl b/src/ndto_generator/ndto_generator_string.erl index d086c52..b143651 100644 --- a/src/ndto_generator/ndto_generator_string.erl +++ b/src/ndto_generator/ndto_generator_string.erl @@ -77,12 +77,24 @@ is_valid(Prefix, #{type := string} = Schema) -> ), {Fun, ExtraFuns}. +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- -spec is_valid_(Prefix, Keyword, Value) -> Result when Prefix :: binary(), Keyword :: atom(), Value :: term(), Result :: undefined | erl_syntax:syntaxTree(). is_valid_(Prefix, min_length, MinLength) -> + is_valid_min_length(Prefix, MinLength); +is_valid_(Prefix, max_length, MaxLength) -> + is_valid_max_length(Prefix, MaxLength); +is_valid_(Prefix, pattern, Pattern) -> + is_valid_pattern(Prefix, Pattern); +is_valid_(Prefix, format, Format) -> + is_valid_format(Prefix, Format). + +is_valid_min_length(Prefix, MinLength) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -124,8 +136,9 @@ is_valid_(Prefix, min_length, MinLength) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] - ); -is_valid_(Prefix, max_length, MaxLength) -> + ). + +is_valid_max_length(Prefix, MaxLength) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -167,8 +180,9 @@ is_valid_(Prefix, max_length, MaxLength) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] - ); -is_valid_(Prefix, pattern, Pattern) -> + ). + +is_valid_pattern(Prefix, Pattern) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -217,8 +231,9 @@ is_valid_(Prefix, pattern, Pattern) -> erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] - ); -is_valid_(Prefix, format, iso8601) -> + ). + +is_valid_format(Prefix, iso8601) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -257,7 +272,7 @@ is_valid_(Prefix, format, iso8601) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_(Prefix, format, base64) -> +is_valid_format(Prefix, base64) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -391,5 +406,5 @@ is_valid_(Prefix, format, base64) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_(_Prefix, format, _Format) -> +is_valid_format(_Prefix, _Format) -> undefined.