From 263d7a80bda66acaf776c1c7b0a50ad12817a778 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Sat, 21 Oct 2023 14:36:44 +0200 Subject: [PATCH 1/6] refactor: numbers as an union (#93) --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- README.md | 6 +++--- rebar.config | 1 - src/ndto.erl | 15 +++++++-------- src/ndto_generator.erl | 8 ++++---- src/ndto_parser_json_schema_draft_04.erl | 23 +++++++++++++++++------ test/ndto_SUITE.erl | 15 +++++++-------- test/ndto_dom.erl | 10 +++++----- test/property_test/ndto_properties.erl | 16 ++++++++-------- 10 files changed, 53 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a5addc..394cf01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: ndto ci +name: CI on: push: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3092b48..edca155 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: ndto docs +name: Docs on: push: branches: diff --git a/README.md b/README.md index 11cbad6..9531e33 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ndto -[![ndto ci](https://github.com/nomasystems/ndto/actions/workflows/ci.yml/badge.svg)](https://github.com/nomasystems/ndto/actions/workflows/ci.yml) -[![ndto docs](https://github.com/nomasystems/ndto/actions/workflows/docs.yml/badge.svg)](https://nomasystems.github.io/ndto) +[![CI](https://github.com/nomasystems/ndto/actions/workflows/ci.yml/badge.svg)](https://github.com/nomasystems/ndto/actions/workflows/ci.yml) +[![Docs](https://github.com/nomasystems/ndto/actions/workflows/docs.yml/badge.svg)](https://nomasystems.github.io/ndto) `ndto` is an Erlang library for generating DTO (Data Transfer Object) validation modules from schemas. @@ -57,7 +57,7 @@ Schemas are built according to the `ndto:schema()` type. | enum_schema() | boolean_schema() | integer_schema() - | number_schema() + | float_schema() | string_schema() | array_schema() | object_schema() diff --git a/rebar.config b/rebar.config index 2181a80..cafd9eb 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,6 @@ {xref_ignores, [ ndto, - {ndto_openapi, is_valid, 2}, ndto_parser ]}. diff --git a/src/ndto.erl b/src/ndto.erl index beaeddf..411718d 100644 --- a/src/ndto.erl +++ b/src/ndto.erl @@ -32,7 +32,7 @@ | enum_schema() | boolean_schema() | integer_schema() - | number_schema() + | float_schema() | string_schema() | array_schema() | object_schema() @@ -59,14 +59,13 @@ % <<"exclusiveMaximum">> => boolean(), % <<"multipleOf">> => integer() % } --type number_schema() :: map(). +-type float_schema() :: map(). % #{ -% <<"type">> := <<"number">>, -% <<"minimum">> => integer(), +% <<"type">> := <<"float">>, +% <<"minimum">> => float(), % <<"exclusiveMinimum">> => boolean(), -% <<"maximum">> => integer(), -% <<"exclusiveMaximum">> => boolean(), -% <<"multipleOf">> => integer() +% <<"maximum">> => float(), +% <<"exclusiveMaximum">> => boolean() % } -type string_schema() :: map(). % #{ @@ -127,7 +126,7 @@ enum_schema/0, boolean_schema/0, integer_schema/0, - number_schema/0, + float_schema/0, string_schema/0, array_schema/0, object_schema/0, diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index be65126..1697579 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -181,7 +181,7 @@ is_valid(Prefix, #{<<"type">> := <<"string">>} = Schema) -> ), {Fun, ExtraFuns}; is_valid(Prefix, #{<<"type">> := Type} = Schema) when - is_binary(Type) andalso (Type =:= <<"number">> orelse Type =:= <<"integer">>) + is_binary(Type) andalso (Type =:= <<"float">> orelse Type =:= <<"integer">>) -> FunName = <>, ExtraFuns = lists:foldl( @@ -591,7 +591,7 @@ is_valid_array(_Prefix, <<"uniqueItems">>, false) -> Prefix :: binary(), Keyword :: binary(), Value :: term(), - Schema :: ndto:number_schema(), + Schema :: ndto:integer_schema() | ndto:float_schema(), Result :: undefined | erl_syntax:syntaxTree(). is_valid_number(_Type, Prefix, <<"minimum">>, Minimum, Schema) -> FunName = <>, @@ -1540,8 +1540,8 @@ type_guard(Type) -> type_guard(string, Var) -> guard(is_binary, Var); -type_guard(number, Var) -> - guard(is_number, Var); +type_guard(float, Var) -> + guard(is_float, Var); type_guard(integer, Var) -> guard(is_integer, Var); type_guard(boolean, Var) -> diff --git a/src/ndto_parser_json_schema_draft_04.erl b/src/ndto_parser_json_schema_draft_04.erl index 2fe9a11..36a2740 100644 --- a/src/ndto_parser_json_schema_draft_04.erl +++ b/src/ndto_parser_json_schema_draft_04.erl @@ -172,12 +172,23 @@ parse_schemas(CTX, #{<<"type">> := <<"number">>} = RawSchema) -> MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), Schema = #{ - <<"type">> => <<"number">>, - <<"minimum">> => Minimum, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Maximum, - <<"exclusiveMaximum">> => ExclusiveMaximum, - <<"multipleOf">> => MultipleOf + <<"anyOf">> => [ + #{ + <<"type">> => <<"integer">>, + <<"minimum">> => Minimum, + <<"exclusiveMinimum">> => ExclusiveMinimum, + <<"maximum">> => Maximum, + <<"exclusiveMaximum">> => ExclusiveMaximum, + <<"multipleOf">> => MultipleOf + }, + #{ + <<"type">> => <<"float">>, + <<"minimum">> => Minimum, + <<"exclusiveMinimum">> => ExclusiveMinimum, + <<"maximum">> => Maximum, + <<"exclusiveMaximum">> => ExclusiveMaximum + } + ] }, {Schema, [], CTX}; parse_schemas(CTX, #{<<"type">> := <<"string">>} = RawSchema) -> diff --git a/test/ndto_SUITE.erl b/test/ndto_SUITE.erl index 359aea1..85c2f2d 100644 --- a/test/ndto_SUITE.erl +++ b/test/ndto_SUITE.erl @@ -42,7 +42,7 @@ groups() -> ref, enum, string, - number, + float, integer, boolean, array, @@ -119,9 +119,9 @@ string(Conf) -> Conf ). -number(Conf) -> +float(Conf) -> ct_property_test:quickcheck( - ndto_properties:prop_number(), + ndto_properties:prop_float(), Conf ). @@ -177,14 +177,14 @@ oneOf(_Conf) -> <<"oneOf">> => [ #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"number">>, <<"minimum">> => 0} + #{<<"type">> => <<"float">>, <<"minimum">> => 0} ] }, DTO = ndto:generate(test_one_of, Schema), ok = ndto:load(DTO), ?assertEqual(false, test_one_of:is_valid(<<"0">>)), - ?assertEqual(false, test_one_of:is_valid(0)), + ?assertEqual(false, test_one_of:is_valid(1)), ?assertEqual(true, test_one_of:is_valid(0.0)). anyOf(_Conf) -> @@ -192,7 +192,7 @@ anyOf(_Conf) -> <<"anyOf">> => [ #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"number">>, <<"minimum">> => 0} + #{<<"type">> => <<"float">>, <<"minimum">> => 0} ] }, DTO = ndto:generate(test_any_of, Schema), @@ -206,8 +206,7 @@ allOf(_Conf) -> Schema = #{ <<"allOf">> => [ #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, - #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"number">>, <<"minimum">> => 0} + #{<<"type">> => <<"integer">>, <<"minimum">> => 1} ] }, DTO = ndto:generate(test_all_of, Schema), diff --git a/test/ndto_dom.erl b/test/ndto_dom.erl index ce101a0..43ee475 100644 --- a/test/ndto_dom.erl +++ b/test/ndto_dom.erl @@ -21,7 +21,7 @@ any_value/0, undefined_value/0, string_value/0, - number_value/0, + float_value/0, integer_value/0, boolean_value/0, array_value/0, @@ -43,7 +43,7 @@ any_value() -> triq_dom:oneof([ undefined_value(), string_value(), - number_value(), + float_value(), integer_value(), boolean_value(), array_value(), @@ -56,8 +56,8 @@ undefined_value() -> string_value() -> triq_dom:unicode_binary(). -number_value() -> - triq_dom:oneof([triq_dom:int(), triq_dom:float()]). +float_value() -> + triq_dom:float(). integer_value() -> triq_dom:int(). @@ -117,4 +117,4 @@ object_value(Type) -> %%% UTIL EXPORTS %%%----------------------------------------------------------------------------- types() -> - [<<"string">>, <<"number">>, <<"integer">>, <<"boolean">>, <<"array">>, <<"object">>]. + [<<"string">>, <<"float">>, <<"integer">>, <<"boolean">>, <<"array">>, <<"object">>]. diff --git a/test/property_test/ndto_properties.erl b/test/property_test/ndto_properties.erl index 786896d..be942df 100644 --- a/test/property_test/ndto_properties.erl +++ b/test/property_test/ndto_properties.erl @@ -100,24 +100,24 @@ prop_string() -> end ). -prop_number() -> +prop_float() -> ?FORALL( - Number, - ndto_dom:number_value(), + Float, + ndto_dom:float_value(), begin Schema = #{ - <<"type">> => <<"number">>, - <<"minimum">> => Number, + <<"type">> => <<"float">>, + <<"minimum">> => Float, <<"exclusiveMinimum">> => false, - <<"maximum">> => Number + 1, + <<"maximum">> => Float + 1, <<"exclusiveMaximum">> => true }, - DTO = ndto:generate(test_number, Schema), + DTO = ndto:generate(test_float, Schema), ok = ndto:load(DTO), ?assertEqual( true, - test_number:is_valid(Number) + test_float:is_valid(Float) ), true end From 448b5dc0230734ea366d0624ca78ff3a4d3bb686 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Mon, 23 Oct 2023 10:29:29 +0200 Subject: [PATCH 2/6] refactor: schemas as maps with atom keys (#94) --- README.md | 6 +- src/ndto.erl | 120 +++++----- src/ndto_generator.erl | 214 +++++++++++------- src/ndto_parser_json_schema_draft_04.erl | 94 ++++---- test/ndto_SUITE.erl | 94 ++++---- test/ndto_dom.erl | 16 +- ...ndto_parser_json_schema_draft_04_SUITE.erl | 16 +- test/property_test/ndto_properties.erl | 60 ++--- 8 files changed, 336 insertions(+), 284 deletions(-) diff --git a/README.md b/README.md index 9531e33..a59c587 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ With `ndto`, you can define validation schemas that describe the structure and c 2. Define an `ndto` schema. ```erl Schema = #{ - <<"type">> => <<"string">>, - <<"minLength">> => 8, - <<"pattern">> => <<"^hello">> + type => string, + min_length => 8, + pattern => <<"^hello">> }. ``` diff --git a/src/ndto.erl b/src/ndto.erl index 411718d..3979097 100644 --- a/src/ndto.erl +++ b/src/ndto.erl @@ -44,63 +44,65 @@ -type empty_schema() :: false. -type universal_schema() :: true | #{} | union_schema(). --type ref_schema() :: map(). -% #{<<"ref">> := binary()} --type enum_schema() :: map(). -% #{<<"enum">> := [value()]} --type boolean_schema() :: map(). -% #{<<"type">> := <<"boolean">>} --type integer_schema() :: map(). -% #{ -% <<"type">> := <<"integer">>, -% <<"minimum">> => integer(), -% <<"exclusiveMinimum">> => boolean(), -% <<"maximum">> => integer(), -% <<"exclusiveMaximum">> => boolean(), -% <<"multipleOf">> => integer() -% } --type float_schema() :: map(). -% #{ -% <<"type">> := <<"float">>, -% <<"minimum">> => float(), -% <<"exclusiveMinimum">> => boolean(), -% <<"maximum">> => float(), -% <<"exclusiveMaximum">> => boolean() -% } --type string_schema() :: map(). -% #{ -% <<"type">> := <<"string">>, -% <<"minLength">> => non_neg_integer(), -% <<"maxLength">> => non_neg_integer(), -% <<"format">> => format(), -% <<"pattern">> => pattern() -% } --type array_schema() :: map(). -% #{ -% <<"type">> := <<"array">>, -% <<"items">> => schema(), -% <<"minItems">> => non_neg_integer(), -% <<"maxItems">> => non_neg_integer(), -% <<"uniqueItems">> => boolean() -% } --type object_schema() :: map(). -% #{ -% <<"type">> := <<"object">>, -% <<"properties">> => #{binary() => schema()}, -% <<"required">> => [binary()], -% <<"minProperties">> => non_neg_integer(), -% <<"maxProperties">> => non_neg_integer(), -% <<"patternProperties">> => #{pattern() => schema()}, -% <<"additionalProperties">> => schema() -% } --type union_schema() :: map(). -% #{<<"anyOf">> := [schema()]} --type intersection_schema() :: map(). -% #{<<"allOf">> := [schema()]} --type complement_schema() :: map(). -% #{<<"not">> := schema()} --type symmetric_difference_schema() :: map(). -% #{<<"oneOf">> := [schema()]} +-type ref_schema() :: #{ + ref := binary() +}. +-type enum_schema() :: #{ + enum := [value()] +}. +-type boolean_schema() :: #{ + type := boolean +}. +-type integer_schema() :: #{ + type := integer, + minimum => integer(), + exclusive_minimum => boolean(), + maximum => integer(), + exclusive_maximum => boolean(), + multiple_of => integer() +}. +-type float_schema() :: #{ + type := float, + minimum => float(), + exclusive_minimum => boolean(), + maximum => float(), + exclusive_maximum => boolean() +}. +-type string_schema() :: #{ + type := string, + min_length => non_neg_integer(), + max_length => non_neg_integer(), + format => format(), + pattern => pattern() +}. +-type array_schema() :: #{ + type := array, + items => schema(), + min_items => non_neg_integer(), + max_items => non_neg_integer(), + unique_items => boolean() +}. +-type object_schema() :: #{ + type := object, + properties => #{binary() => schema()}, + required => [binary()], + min_properties => non_neg_integer(), + max_properties => non_neg_integer(), + pattern_properties => #{pattern() => schema()}, + additional_properties => schema() +}. +-type union_schema() :: #{ + any_of := [schema()] +}. +-type intersection_schema() :: #{ + all_of := [schema()] +}. +-type complement_schema() :: #{ + 'not' := schema() +}. +-type symmetric_difference_schema() :: #{ + one_of := [schema()] +}. -type value() :: boolean() @@ -111,8 +113,8 @@ | object(). -type array() :: [value()]. -type object() :: #{binary() => value()}. --type format() :: binary(). -% <<"iso8601">> | <<"base64">> +-type format() :: iso8601 | base64. +% TODO: use openapi defined formats -type pattern() :: binary(). %%% TYPE EXPORTS diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index 1697579..8545f0f 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -92,7 +92,7 @@ generate(Name, Schema) -> Result :: {IsValidFun, ExtraFuns}, IsValidFun :: erl_syntax:syntaxTree(), ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid(Prefix, #{<<"$ref">> := Ref} = Schema) -> +is_valid(Prefix, #{ref := Ref} = Schema) -> FunName = <>, DTO = erlang:binary_to_atom(Ref), OptionalClause = optional_clause(Schema), @@ -115,7 +115,7 @@ is_valid(Prefix, #{<<"$ref">> := Ref} = Schema) -> OptionalClause ++ [TrueClause] ), {Fun, []}; -is_valid(Prefix, #{<<"enum">> := Enum} = Schema) -> +is_valid(Prefix, #{enum := Enum} = Schema) -> FunName = <>, OptionalClause = optional_clause(Schema), TrueClauses = lists:map( @@ -135,7 +135,7 @@ is_valid(Prefix, #{<<"enum">> := Enum} = Schema) -> Clauses ), {Fun, []}; -is_valid(Prefix, #{<<"type">> := <<"string">>} = Schema) -> +is_valid(Prefix, #{type := string} = Schema) -> FunName = <>, ExtraFuns = lists:foldl( @@ -154,10 +154,10 @@ is_valid(Prefix, #{<<"type">> := <<"string">>} = Schema) -> end, [], [ - <<"minLength">>, - <<"maxLength">>, - <<"format">>, - <<"pattern">> + min_length, + max_length, + format, + pattern ] ), BodyFunCalls = [ @@ -180,17 +180,17 @@ is_valid(Prefix, #{<<"type">> := <<"string">>} = Schema) -> OptionalClause ++ [TrueClause, FalseClause] ), {Fun, ExtraFuns}; -is_valid(Prefix, #{<<"type">> := Type} = Schema) when - is_binary(Type) andalso (Type =:= <<"float">> orelse Type =:= <<"integer">>) --> - FunName = <>, +is_valid(Prefix, #{type := integer} = Schema) -> + FunName = <>, ExtraFuns = lists:foldl( fun(Keyword, Acc) -> case maps:get(Keyword, Schema, undefined) of undefined -> Acc; Value -> - case is_valid_number(Type, <>, Keyword, Value, Schema) of + case + is_valid_number(integer, <>, Keyword, Value, Schema) + of undefined -> Acc; NewIsValidFun -> @@ -200,9 +200,9 @@ is_valid(Prefix, #{<<"type">> := Type} = Schema) when end, [], [ - <<"minimum">>, - <<"maximum">>, - <<"multipleOf">> + minimum, + maximum, + multipleOf ] ), BodyFunCalls = [ @@ -216,7 +216,7 @@ is_valid(Prefix, #{<<"type">> := Type} = Schema) when TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], - type_guard(erlang:binary_to_atom(Type)), + type_guard(integer), [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), @@ -225,7 +225,50 @@ is_valid(Prefix, #{<<"type">> := Type} = Schema) when OptionalClause ++ [TrueClause, FalseClause] ), {Fun, ExtraFuns}; -is_valid(Prefix, #{<<"type">> := <<"boolean">>} = Schema) -> +is_valid(Prefix, #{type := float} = Schema) -> + FunName = <>, + ExtraFuns = lists:foldl( + fun(Keyword, Acc) -> + case maps:get(Keyword, Schema, undefined) of + undefined -> + Acc; + Value -> + case is_valid_number(float, <>, Keyword, Value, Schema) of + undefined -> + Acc; + NewIsValidFun -> + [NewIsValidFun | Acc] + end + end + end, + [], + [ + minimum, + maximum, + multipleOf + ] + ), + BodyFunCalls = [ + erl_syntax:application( + erl_syntax:function_name(Fun), + [erl_syntax:variable('Val')] + ) + || Fun <- ExtraFuns + ], + OptionalClause = optional_clause(Schema), + TrueClause = + erl_syntax:clause( + [erl_syntax:variable('Val')], + type_guard(float), + [chain_conditions(BodyFunCalls, 'andalso')] + ), + FalseClause = false_clause(), + Fun = erl_syntax:function( + erl_syntax:atom(erlang:binary_to_atom(FunName)), + OptionalClause ++ [TrueClause, FalseClause] + ), + {Fun, ExtraFuns}; +is_valid(Prefix, #{type := boolean} = Schema) -> FunName = <>, OptionalClause = optional_clause(Schema), TrueClause = @@ -240,7 +283,7 @@ is_valid(Prefix, #{<<"type">> := <<"boolean">>} = Schema) -> OptionalClause ++ [TrueClause, FalseClause] ), {Fun, []}; -is_valid(Prefix, #{<<"type">> := <<"array">>} = Schema) -> +is_valid(Prefix, #{type := array} = Schema) -> FunName = <>, {IsValidFuns, ExtraFuns} = lists:foldl( @@ -262,10 +305,10 @@ is_valid(Prefix, #{<<"type">> := <<"array">>} = Schema) -> end, {[], []}, [ - <<"items">>, - <<"minItems">>, - <<"maxItems">>, - <<"uniqueItems">> + items, + min_items, + max_items, + unique_items ] ), BodyFunCalls = [ @@ -288,7 +331,7 @@ is_valid(Prefix, #{<<"type">> := <<"array">>} = Schema) -> OptionalClause ++ [TrueClause, FalseClause] ), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{<<"type">> := <<"object">>} = Schema) -> +is_valid(Prefix, #{type := object} = Schema) -> FunName = <>, {IsValidFuns, ExtraFuns} = lists:foldl( @@ -310,12 +353,12 @@ is_valid(Prefix, #{<<"type">> := <<"object">>} = Schema) -> end, {[], []}, [ - <<"properties">>, - <<"required">>, - <<"minProperties">>, - <<"maxProperties">>, - <<"patternProperties">>, - <<"additionalProperties">> + properties, + required, + min_properties, + max_properties, + pattern_properties, + additional_properties ] ), BodyFunCalls = [ @@ -338,8 +381,8 @@ is_valid(Prefix, #{<<"type">> := <<"object">>} = Schema) -> OptionalClause ++ [TrueClause, FalseClause] ), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{<<"oneOf">> := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, +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( @@ -373,8 +416,8 @@ is_valid(Prefix, #{<<"oneOf">> := Subschemas} = Schema) when is_list(Subschemas) OptionalClause ++ [TrueClause] ), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{<<"anyOf">> := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, +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), @@ -409,8 +452,8 @@ is_valid(Prefix, #{<<"anyOf">> := Subschemas} = Schema) when is_list(Subschemas) OptionalClause ++ [TrueClause] ), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{<<"allOf">> := Subschemas} = Schema) when is_list(Subschemas) -> - FunName = <>, +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), @@ -445,7 +488,7 @@ is_valid(Prefix, #{<<"allOf">> := Subschemas} = Schema) when is_list(Subschemas) OptionalClause ++ [TrueClause] ), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid(Prefix, #{<<"not">> := Subschema} = Schema) -> +is_valid(Prefix, #{'not' := Subschema} = Schema) -> FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, Subschema), OptionalClause = optional_clause(Schema), @@ -483,12 +526,12 @@ is_valid(Prefix, _Schema) -> -spec is_valid_array(Prefix, Keyword, Value) -> Result when Prefix :: binary(), - Keyword :: binary(), + Keyword :: atom(), Value :: term(), Result :: {Fun, ExtraFuns}, Fun :: erl_syntax:syntaxTree() | undefined, ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_array(Prefix, <<"items">>, Items) -> +is_valid_array(Prefix, items, Items) -> FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, Items), TrueClause = erl_syntax:clause( @@ -521,8 +564,8 @@ is_valid_array(Prefix, <<"items">>, Items) -> [TrueClause] ), {Fun, [IsValidFun | ExtraFuns]}; -is_valid_array(Prefix, <<"minItems">>, MinItems) -> - FunName = <>, +is_valid_array(Prefix, min_items, MinItems) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], erl_syntax:infix_expr( @@ -538,8 +581,8 @@ is_valid_array(Prefix, <<"minItems">>, MinItems) -> [TrueClause, FalseClause] ), {Fun, []}; -is_valid_array(Prefix, <<"maxItems">>, MaxItems) -> - FunName = <>, +is_valid_array(Prefix, max_items, MaxItems) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], erl_syntax:infix_expr( @@ -555,8 +598,8 @@ is_valid_array(Prefix, <<"maxItems">>, MaxItems) -> [TrueClause, FalseClause] ), {Fun, []}; -is_valid_array(Prefix, <<"uniqueItems">>, true) -> - FunName = <>, +is_valid_array(Prefix, unique_items, true) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -583,17 +626,17 @@ is_valid_array(Prefix, <<"uniqueItems">>, true) -> [TrueClause] ), {Fun, []}; -is_valid_array(_Prefix, <<"uniqueItems">>, false) -> +is_valid_array(_Prefix, unique_items, false) -> {undefined, []}. -spec is_valid_number(Type, Prefix, Keyword, Value, Schema) -> Result when - Type :: binary(), + Type :: integer | float, Prefix :: binary(), - Keyword :: 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) -> +is_valid_number(_Type, Prefix, minimum, Minimum, Schema) -> FunName = <>, MinimumSt = case Minimum of @@ -603,7 +646,7 @@ is_valid_number(_Type, Prefix, <<"minimum">>, Minimum, Schema) -> erl_syntax:float(Minimum) end, Operator = - case maps:get(<<"exclusiveMinimum">>, Schema, false) of + case maps:get(exclusive_minimum, Schema, false) of true -> erl_syntax:operator('>'); _false -> @@ -623,7 +666,7 @@ is_valid_number(_Type, Prefix, <<"minimum">>, Minimum, Schema) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] ); -is_valid_number(_Type, Prefix, <<"maximum">>, Maximum, Schema) -> +is_valid_number(_Type, Prefix, maximum, Maximum, Schema) -> FunName = <>, MaximumSt = case Maximum of @@ -633,7 +676,7 @@ is_valid_number(_Type, Prefix, <<"maximum">>, Maximum, Schema) -> erl_syntax:float(Maximum) end, Operator = - case maps:get(<<"exclusiveMaximum">>, Schema, false) of + case maps:get(exclusive_maximum, Schema, false) of true -> erl_syntax:operator('<'); _false -> @@ -653,8 +696,8 @@ is_valid_number(_Type, Prefix, <<"maximum">>, Maximum, Schema) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause, FalseClause] ); -is_valid_number(<<"integer">>, Prefix, <<"multipleOf">>, MultipleOf, _Schema) -> - FunName = <>, +is_valid_number(integer, Prefix, multiple_of, MultipleOf, _Schema) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -675,21 +718,20 @@ is_valid_number(<<"integer">>, Prefix, <<"multipleOf">>, MultipleOf, _Schema) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_number(_Number, _Prefix, <<"multipleOf">>, _Value, _Schema) -> +is_valid_number(_Number, _Prefix, multiple_of, _Value, _Schema) -> undefined. -spec is_valid_object(Prefix, Keyword, Schema) -> Result when Prefix :: binary(), - Keyword :: 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} = Schema) -> +is_valid_object(Prefix, properties, #{properties := Properties}) -> FunName = <>, {PropertiesFuns, ExtraFuns} = maps:fold( - fun(PropertyName, RawProperty, {IsValidFunsAcc, ExtraFunsAcc}) -> - Property = RawProperty#{<<"nullable">> => maps:get(<<"nullable">>, Schema, true)}, + fun(PropertyName, Property, {IsValidFunsAcc, ExtraFunsAcc}) -> {IsValidPropertyFun, ExtraPropertyFuns} = is_valid(<>, Property), { @@ -733,7 +775,7 @@ is_valid_object(Prefix, <<"properties">>, #{<<"properties">> := Properties} = Sc ), {_PropertyNames, IsValidFuns} = lists:unzip(PropertiesFuns), {Fun, IsValidFuns ++ ExtraFuns}; -is_valid_object(Prefix, <<"required">>, #{<<"required">> := Required}) -> +is_valid_object(Prefix, required, #{required := Required}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -780,8 +822,8 @@ is_valid_object(Prefix, <<"required">>, #{<<"required">> := Required}) -> [TrueClause] ), {Fun, []}; -is_valid_object(Prefix, <<"minProperties">>, #{<<"minProperties">> := MinProperties}) -> - FunName = <>, +is_valid_object(Prefix, min_properties, #{min_properties := MinProperties}) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -808,8 +850,8 @@ is_valid_object(Prefix, <<"minProperties">>, #{<<"minProperties">> := MinPropert [TrueClause] ), {Fun, []}; -is_valid_object(Prefix, <<"maxProperties">>, #{<<"maxProperties">> := MaxProperties}) -> - FunName = <>, +is_valid_object(Prefix, max_properties, #{max_properties := MaxProperties}) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -836,8 +878,8 @@ is_valid_object(Prefix, <<"maxProperties">>, #{<<"maxProperties">> := MaxPropert [TrueClause] ), {Fun, []}; -is_valid_object(Prefix, <<"patternProperties">>, #{<<"patternProperties">> := PatternProperties}) -> - FunName = <>, +is_valid_object(Prefix, pattern_properties, #{pattern_properties := PatternProperties}) -> + FunName = <>, {IsValidPatterns, ExtraFuns} = lists:foldl( fun({PropertyPattern, PropertySchema}, {IsValidPatternsAcc, ExtraFunsAcc}) -> {IsValidFun, ExtraFuns} = is_valid( @@ -950,12 +992,12 @@ is_valid_object(Prefix, <<"patternProperties">>, #{<<"patternProperties">> := Pa {Fun, IsValidFuns ++ ExtraFuns}; is_valid_object( Prefix, - <<"additionalProperties">>, - #{<<"additionalProperties">> := false} = Schema + additional_properties, + #{additional_properties := false} = Schema ) -> - FunName = <>, - Properties = maps:get(<<"properties">>, Schema, #{}), - PatternProperties = maps:get(<<"patternProperties">>, 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), @@ -1087,13 +1129,13 @@ is_valid_object( {Fun, []}; is_valid_object( Prefix, - <<"additionalProperties">>, - #{<<"additionalProperties">> := AdditionalProperties} = Schema + additional_properties, + #{additional_properties := AdditionalProperties} = Schema ) -> - FunName = <>, + FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, AdditionalProperties), - Properties = maps:get(<<"properties">>, Schema, #{}), - PatternProperties = maps:get(<<"patternProperties">>, Schema, #{}), + Properties = maps:get(properties, Schema, #{}), + PatternProperties = maps:get(pattern_properties, Schema, #{}), PatternPropertiesList = erl_syntax:application( erl_syntax:atom(lists), erl_syntax:atom(filter), @@ -1239,16 +1281,16 @@ is_valid_object( [TrueClause] ), {Fun, [IsValidFun | ExtraFuns]}; -is_valid_object(_Prefix, <<"additionalProperties">>, _Schema) -> +is_valid_object(_Prefix, additional_properties, _Schema) -> {undefined, []}. -spec is_valid_string(Prefix, Keyword, Value) -> Result when Prefix :: binary(), - Keyword :: binary(), + Keyword :: atom(), Value :: term(), Result :: undefined | erl_syntax:syntaxTree(). -is_valid_string(Prefix, <<"minLength">>, MinLength) -> - FunName = <>, +is_valid_string(Prefix, min_length, MinLength) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -1266,8 +1308,8 @@ is_valid_string(Prefix, <<"minLength">>, MinLength) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_string(Prefix, <<"maxLength">>, MaxLength) -> - FunName = <>, +is_valid_string(Prefix, max_length, MaxLength) -> + FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -1285,7 +1327,7 @@ is_valid_string(Prefix, <<"maxLength">>, MaxLength) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_string(Prefix, <<"pattern">>, Pattern) -> +is_valid_string(Prefix, pattern, Pattern) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -1327,7 +1369,7 @@ is_valid_string(Prefix, <<"pattern">>, Pattern) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_string(Prefix, <<"format">>, <<"iso8601-datetime">>) -> +is_valid_string(Prefix, format, iso8601) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -1347,7 +1389,7 @@ is_valid_string(Prefix, <<"format">>, <<"iso8601-datetime">>) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_string(Prefix, <<"format">>, <<"base64">>) -> +is_valid_string(Prefix, format, base64) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -1473,7 +1515,7 @@ is_valid_string(Prefix, <<"format">>, <<"base64">>) -> erl_syntax:atom(erlang:binary_to_atom(FunName)), [TrueClause] ); -is_valid_string(_Prefix, <<"format">>, _Format) -> +is_valid_string(_Prefix, format, _Format) -> undefined. %%%----------------------------------------------------------------------------- @@ -1524,7 +1566,7 @@ literal(Val) when is_map(Val) -> || {K, V} <- maps:to_list(Val) ]). -optional_clause(#{<<"nullable">> := true}) -> +optional_clause(#{nullable := true}) -> [ erl_syntax:clause( [erl_syntax:atom('undefined')], diff --git a/src/ndto_parser_json_schema_draft_04.erl b/src/ndto_parser_json_schema_draft_04.erl index 36a2740..718752b 100644 --- a/src/ndto_parser_json_schema_draft_04.erl +++ b/src/ndto_parser_json_schema_draft_04.erl @@ -132,7 +132,7 @@ parse_schemas(CTX, UniversalSchema) when is_map(UniversalSchema), map_size(Unive {Schema, [], CTX}; parse_schemas(CTX, #{<<"$ref">> := Ref}) -> {RefName, RefSchema, RefCTX} = resolve_ref(Ref, CTX), - Schema = #{<<"$ref">> => RefName}, + Schema = #{ref => RefName}, case lists:member(RefName, CTX#ctx.resolved) of true -> {Schema, [], CTX}; @@ -143,10 +143,10 @@ parse_schemas(CTX, #{<<"$ref">> := Ref}) -> }} end; parse_schemas(CTX, #{<<"enum">> := Enum}) -> - Schema = #{<<"enum">> => Enum}, + Schema = #{enum => Enum}, {Schema, [], CTX}; parse_schemas(CTX, #{<<"type">> := <<"boolean">>}) -> - Schema = #{<<"type">> => <<"boolean">>}, + Schema = #{type => boolean}, {Schema, [], CTX}; parse_schemas(CTX, #{<<"type">> := <<"integer">>} = RawSchema) -> Minimum = maps:get(<<"minimum">>, RawSchema, undefined), @@ -156,12 +156,12 @@ parse_schemas(CTX, #{<<"type">> := <<"integer">>} = RawSchema) -> MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), Schema = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Minimum, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Maximum, - <<"exclusiveMaximum">> => ExclusiveMaximum, - <<"multipleOf">> => MultipleOf + type => integer, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf }, {Schema, [], CTX}; parse_schemas(CTX, #{<<"type">> := <<"number">>} = RawSchema) -> @@ -172,21 +172,21 @@ parse_schemas(CTX, #{<<"type">> := <<"number">>} = RawSchema) -> MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), Schema = #{ - <<"anyOf">> => [ + any_of => [ #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Minimum, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Maximum, - <<"exclusiveMaximum">> => ExclusiveMaximum, - <<"multipleOf">> => MultipleOf + type => integer, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf }, #{ - <<"type">> => <<"float">>, - <<"minimum">> => Minimum, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Maximum, - <<"exclusiveMaximum">> => ExclusiveMaximum + type => float, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum } ] }, @@ -194,15 +194,23 @@ parse_schemas(CTX, #{<<"type">> := <<"number">>} = RawSchema) -> parse_schemas(CTX, #{<<"type">> := <<"string">>} = RawSchema) -> MinLength = maps:get(<<"minLength">>, RawSchema, undefined), MaxLength = maps:get(<<"maxLength">>, RawSchema, undefined), - Format = maps:get(<<"format">>, RawSchema, undefined), + Format = + case maps:get(<<"format">>, RawSchema, undefined) of + <<"iso8601">> -> + iso8601; + <<"byte">> -> + base64; + _Otherwise -> + undefined + end, Pattern = maps:get(<<"pattern">>, RawSchema, undefined), Schema = #{ - <<"type">> => <<"string">>, - <<"minLength">> => MinLength, - <<"maxLength">> => MaxLength, - <<"format">> => Format, - <<"pattern">> => Pattern + type => string, + min_length => MinLength, + max_length => MaxLength, + format => Format, + pattern => Pattern }, {Schema, [], CTX}; parse_schemas(CTX, #{<<"type">> := <<"array">>} = RawSchema) -> @@ -218,11 +226,11 @@ parse_schemas(CTX, #{<<"type">> := <<"array">>} = RawSchema) -> UniqueItems = maps:get(<<"uniqueItems">>, RawSchema, undefined), Schema = #{ - <<"type">> => <<"array">>, - <<"items">> => Items, - <<"minItems">> => MinItems, - <<"maxItems">> => MaxItems, - <<"uniqueItems">> => UniqueItems + type => array, + items => Items, + min_items => MinItems, + max_items => MaxItems, + unique_items => UniqueItems }, {Schema, ItemsExtraSchemas, ItemsCTX}; parse_schemas(CTX, #{<<"type">> := <<"object">>} = RawSchema) -> @@ -280,13 +288,13 @@ parse_schemas(CTX, #{<<"type">> := <<"object">>} = RawSchema) -> end, Schema = #{ - <<"type">> => <<"object">>, - <<"properties">> => Properties, - <<"required">> => Required, - <<"minProperties">> => MinProperties, - <<"maxProperties">> => MaxProperties, - <<"additionalProperties">> => AdditionalProperties, - <<"patternProperties">> => PatternProperties + type => object, + properties => Properties, + required => Required, + minProperties => MinProperties, + maxProperties => MaxProperties, + additionalProperties => AdditionalProperties, + patternProperties => PatternProperties }, {Schema, PropertiesExtraSchemas ++ AdditionalPropertiesExtraSchemas ++ PatternPropertiesExtraSchemas, @@ -303,7 +311,7 @@ parse_schemas(CTX, #{<<"anyOf">> := RawAnyOf}) -> {[], [], CTX}, RawAnyOf ), - Schema = #{<<"anyOf">> => AnyOf}, + Schema = #{any_of => AnyOf}, {Schema, ExtraSchemas, NewCTX}; parse_schemas(CTX, #{<<"allOf">> := RawAllOf}) -> {AllOf, ExtraSchemas, NewCTX} = @@ -317,11 +325,11 @@ parse_schemas(CTX, #{<<"allOf">> := RawAllOf}) -> {[], [], CTX}, RawAllOf ), - Schema = #{<<"allOf">> => AllOf}, + Schema = #{all_of => AllOf}, {Schema, ExtraSchemas, NewCTX}; parse_schemas(CTX, #{<<"not">> := RawNot}) -> {Not, ExtraSchemas, NewCTX} = parse_schemas(CTX, RawNot), - Schema = #{<<"not">> => Not}, + Schema = #{'not' => Not}, {Schema, ExtraSchemas, NewCTX}; parse_schemas(CTX, #{<<"oneOf">> := RawOneOf}) -> {OneOf, ExtraSchemas, NewCTX} = @@ -335,7 +343,7 @@ parse_schemas(CTX, #{<<"oneOf">> := RawOneOf}) -> {[], [], CTX}, RawOneOf ), - Schema = #{<<"oneOf">> => OneOf}, + Schema = #{one_of => OneOf}, {Schema, ExtraSchemas, NewCTX}. -spec read_spec(SpecPath) -> Result when diff --git a/test/ndto_SUITE.erl b/test/ndto_SUITE.erl index 85c2f2d..1aed82c 100644 --- a/test/ndto_SUITE.erl +++ b/test/ndto_SUITE.erl @@ -49,9 +49,9 @@ groups() -> object ]}, {subschemas, [parallel], [ - oneOf, - anyOf, - allOf, + one_of, + any_of, + all_of, 'not' ]}, {string_formats, [parallel], [ @@ -153,8 +153,8 @@ nullable(_Conf) -> lists:foreach( fun(Type) -> Schema1 = #{ - <<"type">> => Type, - <<"nullable">> => true + type => Type, + nullable => true }, DTO1 = ndto:generate(test_nullable1, Schema1), ok = ndto:load(DTO1), @@ -162,7 +162,7 @@ nullable(_Conf) -> ?assertEqual(true, test_nullable1:is_valid(undefined)), Schema2 = #{ - <<"type">> => Type + type => Type }, DTO2 = ndto:generate(test_nullable2, Schema2), ok = ndto:load(DTO2), @@ -172,12 +172,12 @@ nullable(_Conf) -> ndto_dom:types() ). -oneOf(_Conf) -> +one_of(_Conf) -> Schema = #{ - <<"oneOf">> => [ - #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, - #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"float">>, <<"minimum">> => 0} + one_of => [ + #{type => integer, minimum => 0}, + #{type => integer, minimum => 1}, + #{type => float, minimum => 0} ] }, DTO = ndto:generate(test_one_of, Schema), @@ -187,12 +187,12 @@ oneOf(_Conf) -> ?assertEqual(false, test_one_of:is_valid(1)), ?assertEqual(true, test_one_of:is_valid(0.0)). -anyOf(_Conf) -> +any_of(_Conf) -> Schema = #{ - <<"anyOf">> => [ - #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, - #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"float">>, <<"minimum">> => 0} + any_of => [ + #{type => integer, minimum => 0}, + #{type => integer, minimum => 1}, + #{type => float, minimum => 0} ] }, DTO = ndto:generate(test_any_of, Schema), @@ -202,11 +202,11 @@ anyOf(_Conf) -> ?assertEqual(true, test_any_of:is_valid(0)), ?assertEqual(true, test_any_of:is_valid(0.0)). -allOf(_Conf) -> +all_of(_Conf) -> Schema = #{ - <<"allOf">> => [ - #{<<"type">> => <<"integer">>, <<"minimum">> => 0}, - #{<<"type">> => <<"integer">>, <<"minimum">> => 1} + all_of => [ + #{type => integer, minimum => 0}, + #{type => integer, minimum => 1} ] }, DTO = ndto:generate(test_all_of, Schema), @@ -219,7 +219,7 @@ allOf(_Conf) -> 'not'(_Conf) -> Schema = #{ - <<"not">> => #{<<"type">> => <<"integer">>, <<"minimum">> => 0} + 'not' => #{type => integer, minimum => 0} }, DTO = ndto:generate(test_not, Schema), ok = ndto:load(DTO), @@ -230,8 +230,8 @@ allOf(_Conf) -> pattern(_Conf) -> Schema = #{ - <<"type">> => <<"string">>, - <<"pattern">> => <<"[a-z]+@[a-z]+\.[a-z]+">> + type => string, + pattern => <<"[a-z]+@[a-z]+\.[a-z]+">> }, DTO = ndto:generate(test_pattern, Schema), ok = ndto:load(DTO), @@ -240,9 +240,9 @@ pattern(_Conf) -> pattern_properties(_Conf) -> Schema = #{ - <<"type">> => <<"object">>, - <<"patternProperties">> => #{ - <<"[a-z]+">> => #{<<"type">> => <<"string">>} + type => object, + pattern_properties => #{ + <<"[a-z]+">> => #{type => string} } }, DTO = ndto:generate(test_pattern_properties, Schema), @@ -254,10 +254,10 @@ pattern_properties(_Conf) -> additional_properties(_Conf) -> Schema1 = #{ - <<"type">> => <<"object">>, - <<"properties">> => #{<<"foo">> => #{}}, - <<"patternProperties">> => #{<<"[a-z]+">> => #{<<"type">> => <<"string">>}}, - <<"additionalProperties">> => false + type => object, + properties => #{<<"foo">> => #{}}, + pattern_properties => #{<<"[a-z]+">> => #{type => string}}, + additional_properties => false }, DTO1 = ndto:generate(test_additional_properties1, Schema1), ok = ndto:load(DTO1), @@ -275,10 +275,10 @@ additional_properties(_Conf) -> ), Schema2 = #{ - <<"type">> => <<"object">>, - <<"properties">> => #{<<"foo">> => #{}}, - <<"patternProperties">> => #{<<"[a-z]+">> => #{<<"type">> => <<"string">>}}, - <<"additionalProperties">> => true + type => <<"object">>, + properties => #{<<"foo">> => #{}}, + pattern_properties => #{<<"[a-z]+">> => #{type => string}}, + additional_properties => true }, DTO2 = ndto:generate(test_additional_properties2, Schema2), ok = ndto:load(DTO2), @@ -292,10 +292,10 @@ additional_properties(_Conf) -> ), Schema3 = #{ - <<"type">> => <<"object">>, - <<"properties">> => #{<<"foo">> => #{}}, - <<"patternProperties">> => #{<<"[a-z]+">> => #{<<"type">> => <<"string">>}}, - <<"additionalProperties">> => #{<<"type">> => <<"boolean">>} + type => object, + properties => #{<<"foo">> => #{}}, + pattern_properties => #{<<"[a-z]+">> => #{type => string}}, + additional_properties => #{type => boolean} }, DTO3 = ndto:generate(test_additional_properties3, Schema3), ok = ndto:load(DTO3), @@ -311,9 +311,9 @@ additional_properties(_Conf) -> ), Schema4 = #{ - <<"type">> => <<"object">>, - <<"patternProperties">> => #{<<"^[A-Z]+$">> => true}, - <<"additionalProperties">> => false + type => object, + pattern_properties => #{<<"^[A-Z]+$">> => true}, + additional_properties => false }, DTO4 = ndto:generate(test_additional_properties4, Schema4), ok = ndto:load(DTO4), @@ -327,8 +327,8 @@ additional_properties(_Conf) -> unique_items(_Conf) -> Schema = #{ - <<"type">> => <<"array">>, - <<"uniqueItems">> => true + type => array, + unique_items => true }, DTO = ndto:generate(test_unique_items, Schema), ok = ndto:load(DTO), @@ -341,8 +341,8 @@ unique_items(_Conf) -> iso8601(_Conf) -> String = ncalendar:now(iso8601), Schema = #{ - <<"type">> => <<"string">>, - <<"format">> => <<"iso8601-datetime">> + type => string, + format => iso8601 }, DTO = ndto:generate(test_iso8601, Schema), ok = ndto:load(DTO), @@ -352,8 +352,8 @@ iso8601(_Conf) -> base64(_Conf) -> String = base64:encode(<<"this is a test">>), Schema = #{ - <<"type">> => <<"string">>, - <<"format">> => <<"base64">> + type => string, + format => base64 }, DTO = ndto:generate(test_base64, Schema), ok = ndto:load(DTO), diff --git a/test/ndto_dom.erl b/test/ndto_dom.erl index 43ee475..7209795 100644 --- a/test/ndto_dom.erl +++ b/test/ndto_dom.erl @@ -34,7 +34,7 @@ -export([types/0]). %%% MACROS --define(NON_RECURSIVE_TYPES, lists:subtract(types(), [<<"array">>, <<"object">>])). +-define(NON_RECURSIVE_TYPES, lists:subtract(types(), [array, object])). %%%----------------------------------------------------------------------------- %%% VALUE EXPORTS @@ -68,22 +68,22 @@ boolean_value() -> array_value() -> ?LET(Type, triq_dom:elements(types()), array_value(Type)). -array_value(<<"array">>) -> +array_value(array) -> triq_dom:list( ?LET(Type, triq_dom:elements(?NON_RECURSIVE_TYPES), array_value(Type)) ); -array_value(<<"object">>) -> +array_value(object) -> triq_dom:list( ?LET(Type, triq_dom:elements(?NON_RECURSIVE_TYPES), object_value(Type)) ); array_value(Type) -> - Fun = erlang:binary_to_atom(<>), + Fun = erlang:binary_to_atom(<<(erlang:atom_to_binary(Type))/binary, "_value">>), triq_dom:list(erlang:apply(?MODULE, Fun, [])). object_value() -> ?LET(Type, triq_dom:elements(types()), object_value(Type)). -object_value(<<"array">>) -> +object_value(array) -> ?LET( Proplist, ?LET( @@ -93,7 +93,7 @@ object_value(<<"array">>) -> ), maps:from_list(Proplist) ); -object_value(<<"object">>) -> +object_value(object) -> ?LET( Proplist, ?LET( @@ -104,7 +104,7 @@ object_value(<<"object">>) -> maps:from_list(Proplist) ); object_value(Type) -> - Fun = erlang:binary_to_atom(<>), + Fun = erlang:binary_to_atom(<<(erlang:atom_to_binary(Type))/binary, "_value">>), ?LET( Proplist, triq_dom:list({ @@ -117,4 +117,4 @@ object_value(Type) -> %%% UTIL EXPORTS %%%----------------------------------------------------------------------------- types() -> - [<<"string">>, <<"float">>, <<"integer">>, <<"boolean">>, <<"array">>, <<"object">>]. + [string, float, integer, boolean, array, object]. diff --git a/test/ndto_parser_json_schema_draft_04_SUITE.erl b/test/ndto_parser_json_schema_draft_04_SUITE.erl index 1df9a5a..6a0fa29 100644 --- a/test/ndto_parser_json_schema_draft_04_SUITE.erl +++ b/test/ndto_parser_json_schema_draft_04_SUITE.erl @@ -62,25 +62,25 @@ oas_3_0(_Conf) -> SpecPath = filename:join(code:lib_dir(ndto, priv), "oas/3.0/specs/oas_3_0.json"), {ok, Schemas} = ndto_parser:parse(ndto_parser_json_schema_draft_04, oas_3_0, SpecPath), #{ - <<"type">> := <<"object">>, - <<"properties">> := #{ + type := object, + properties := #{ <<"openapi">> := #{ - <<"type">> := <<"string">>, - <<"pattern">> := Pattern + type := string, + pattern := Pattern } } } = proplists:get_value(oas_3_0, Schemas), ?assertEqual(<<"^3\\.0\\.\\d(-.+)?$">>, Pattern), #{ - <<"type">> := <<"object">>, - <<"properties">> := #{ + type := object, + properties := #{ <<"not">> := #{ - <<"oneOf">> := SchemaOneOf + one_of := SchemaOneOf } } } = proplists:get_value(oas_3_0_Schema, Schemas), Expected = lists:sort([ - #{<<"$ref">> => <<"oas_3_0_Reference">>}, #{<<"$ref">> => <<"oas_3_0_Schema">>} + #{ref => <<"oas_3_0_Reference">>}, #{ref => <<"oas_3_0_Schema">>} ]), Actual = lists:sort(SchemaOneOf), ?assertEqual(Expected, Actual). diff --git a/test/property_test/ndto_properties.erl b/test/property_test/ndto_properties.erl index be942df..3c3cb18 100644 --- a/test/property_test/ndto_properties.erl +++ b/test/property_test/ndto_properties.erl @@ -42,7 +42,7 @@ prop_ref() -> ReferencedName = test_referenced_dto, ReferencedSchema = #{}, Schema = #{ - <<"$ref">> => erlang:atom_to_binary(ReferencedName) + ref => erlang:atom_to_binary(ReferencedName) }, ReferencedDTO = ndto:generate(ReferencedName, ReferencedSchema), @@ -62,7 +62,7 @@ prop_enum() -> Enum, triq_dom:list(ndto_dom:array_value()), begin - Schema = #{<<"enum">> => Enum}, + Schema = #{enum => Enum}, DTO = ndto:generate(test_enum, Schema), ok = ndto:load(DTO), @@ -84,10 +84,10 @@ prop_string() -> ndto_dom:string_value(), begin Schema = #{ - <<"type">> => <<"string">>, - <<"minLength">> => string:length(String), - <<"maxLength">> => string:length(String), - <<"pattern">> => <<".*">> + type => string, + min_length => string:length(String), + max_length => string:length(String), + pattern => <<".*">> }, DTO = ndto:generate(test_string, Schema), ok = ndto:load(DTO), @@ -106,11 +106,11 @@ prop_float() -> ndto_dom:float_value(), begin Schema = #{ - <<"type">> => <<"float">>, - <<"minimum">> => Float, - <<"exclusiveMinimum">> => false, - <<"maximum">> => Float + 1, - <<"exclusiveMaximum">> => true + type => float, + minimum => Float, + exclusive_minimum => false, + maximum => Float + 1, + exclusive_maximum => true }, DTO = ndto:generate(test_float, Schema), ok = ndto:load(DTO), @@ -134,12 +134,12 @@ prop_integer() -> begin Integer = RawInteger * MultipleOf, Schema = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Integer - 1, - <<"exclusiveMinimum">> => true, - <<"maximum">> => Integer, - <<"exclusiveMaximum">> => false, - <<"multipleOf">> => MultipleOf + type => integer, + minimum => Integer - 1, + exclusive_minimum => true, + maximum => Integer, + exclusive_maximum => false, + multiple_of => MultipleOf }, DTO = ndto:generate(test_integer, Schema), ok = ndto:load(DTO), @@ -158,7 +158,7 @@ prop_boolean() -> ndto_dom:boolean_value(), begin Schema = #{ - <<"type">> => <<"boolean">> + type => boolean }, DTO = ndto:generate(test_boolean, Schema), ok = ndto:load(DTO), @@ -181,14 +181,14 @@ prop_array() -> ), begin Schema = #{ - <<"type">> => <<"array">>, - <<"items">> => #{ - <<"type">> => Type, - <<"nullable">> => false + type => array, + items => #{ + type => Type, + nullable => false }, - <<"minItems">> => erlang:length(Array), - <<"maxItems">> => erlang:length(Array), - <<"uniqueItems">> => false + min_items => erlang:length(Array), + max_items => erlang:length(Array), + unique_items => false }, DTO = ndto:generate(test_array, Schema), ok = ndto:load(DTO), @@ -207,17 +207,17 @@ prop_object() -> ndto_dom:object_value(), begin Schema = #{ - <<"type">> => <<"object">>, - <<"properties">> => maps:fold( + type => object, + properties => maps:fold( fun(Key, _Value, Acc) -> maps:put(Key, #{}, Acc) end, #{}, Object ), - <<"required">> => maps:keys(Object), - <<"minProperties">> => erlang:length(maps:keys(Object)) - 1, - <<"maxProperties">> => erlang:length(maps:keys(Object)) + 1 + required => maps:keys(Object), + min_properties => erlang:length(maps:keys(Object)) - 1, + max_properties => erlang:length(maps:keys(Object)) + 1 }, DTO = ndto:generate(test_object, Schema), ok = ndto:load(DTO), From 70217a211baf0e4e41e0f7a500cdfd1544e9d182 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Thu, 23 Nov 2023 19:15:35 +0100 Subject: [PATCH 3/6] fix(#95): non required properties are still required (#96) closes #95 --- rebar.lock | 2 +- src/ndto.erl | 40 ++++++---- src/ndto_generator.erl | 99 ++++++++++++++++-------- src/ndto_parser_json_schema_draft_04.erl | 9 +-- test/ndto_SUITE.erl | 26 ++++++- 5 files changed, 120 insertions(+), 56 deletions(-) diff --git a/rebar.lock b/rebar.lock index 4fe2bab..091bc8c 100644 --- a/rebar.lock +++ b/rebar.lock @@ -4,5 +4,5 @@ 0}, {<<"njson">>, {git,"git@github.com:nomasystems/njson.git", - {ref,"76ab40033ee977f876e7b3addca5de981ff4a9ef"}}, + {ref,"338ecbac922343874765abebfbcc8af131724732"}}, 0}]. diff --git a/src/ndto.erl b/src/ndto.erl index 3979097..c52840f 100644 --- a/src/ndto.erl +++ b/src/ndto.erl @@ -45,13 +45,16 @@ -type empty_schema() :: false. -type universal_schema() :: true | #{} | union_schema(). -type ref_schema() :: #{ - ref := binary() + ref := binary(), + nullable => boolean() }. -type enum_schema() :: #{ - enum := [value()] + enum := [value()], + nullable => boolean() }. -type boolean_schema() :: #{ - type := boolean + type := boolean, + nullable => boolean() }. -type integer_schema() :: #{ type := integer, @@ -59,28 +62,32 @@ exclusive_minimum => boolean(), maximum => integer(), exclusive_maximum => boolean(), - multiple_of => integer() + multiple_of => integer(), + nullable => boolean() }. -type float_schema() :: #{ type := float, minimum => float(), exclusive_minimum => boolean(), maximum => float(), - exclusive_maximum => boolean() + exclusive_maximum => boolean(), + nullable => boolean() }. -type string_schema() :: #{ type := string, min_length => non_neg_integer(), max_length => non_neg_integer(), format => format(), - pattern => pattern() + pattern => pattern(), + nullable => boolean() }. -type array_schema() :: #{ type := array, items => schema(), min_items => non_neg_integer(), max_items => non_neg_integer(), - unique_items => boolean() + unique_items => boolean(), + nullable => boolean() }. -type object_schema() :: #{ type := object, @@ -89,29 +96,36 @@ min_properties => non_neg_integer(), max_properties => non_neg_integer(), pattern_properties => #{pattern() => schema()}, - additional_properties => schema() + additional_properties => schema(), + nullable => boolean() }. -type union_schema() :: #{ - any_of := [schema()] + any_of := [schema()], + nullable => boolean() }. -type intersection_schema() :: #{ - all_of := [schema()] + all_of := [schema()], + nullable => boolean() }. -type complement_schema() :: #{ - 'not' := schema() + 'not' := schema(), + nullable => boolean() }. -type symmetric_difference_schema() :: #{ - one_of := [schema()] + one_of := [schema()], + nullable => boolean() }. -type value() :: - boolean() + null() + | boolean() | integer() | float() | binary() | array() | object(). -type array() :: [value()]. +-type null() :: null. -type object() :: #{binary() => value()}. -type format() :: iso8601 | base64. % TODO: use openapi defined formats diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index 8545f0f..0b5b05b 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -95,7 +95,8 @@ generate(Name, Schema) -> is_valid(Prefix, #{ref := Ref} = Schema) -> FunName = <>, DTO = erlang:binary_to_atom(Ref), - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -110,14 +111,16 @@ is_valid(Prefix, #{ref := Ref} = Schema) -> ) ] ), + Clauses = clauses([OptionalClause, NullClause, TrueClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause] + Clauses ), {Fun, []}; is_valid(Prefix, #{enum := Enum} = Schema) -> FunName = <>, - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClauses = lists:map( fun(EnumVal) -> erl_syntax:clause( @@ -129,7 +132,7 @@ is_valid(Prefix, #{enum := Enum} = Schema) -> Enum ), FalseClause = false_clause(), - Clauses = OptionalClause ++ TrueClauses ++ [FalseClause], + Clauses = clauses(lists:flatten([OptionalClause, NullClause, TrueClauses, FalseClause])), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), Clauses @@ -167,7 +170,8 @@ is_valid(Prefix, #{type := string} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -175,9 +179,10 @@ is_valid(Prefix, #{type := string} = Schema) -> [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, ExtraFuns}; is_valid(Prefix, #{type := integer} = Schema) -> @@ -212,7 +217,8 @@ is_valid(Prefix, #{type := integer} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -220,9 +226,10 @@ is_valid(Prefix, #{type := integer} = Schema) -> [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, ExtraFuns}; is_valid(Prefix, #{type := float} = Schema) -> @@ -255,7 +262,8 @@ is_valid(Prefix, #{type := float} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -263,14 +271,16 @@ is_valid(Prefix, #{type := float} = Schema) -> [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, ExtraFuns}; is_valid(Prefix, #{type := boolean} = Schema) -> FunName = <>, - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -278,9 +288,10 @@ is_valid(Prefix, #{type := boolean} = Schema) -> [erl_syntax:atom(true)] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, []}; is_valid(Prefix, #{type := array} = Schema) -> @@ -318,7 +329,8 @@ is_valid(Prefix, #{type := array} = Schema) -> ) || Fun <- IsValidFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -326,9 +338,10 @@ is_valid(Prefix, #{type := array} = Schema) -> [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, IsValidFuns ++ ExtraFuns}; is_valid(Prefix, #{type := object} = Schema) -> @@ -368,7 +381,8 @@ is_valid(Prefix, #{type := object} = Schema) -> ) || Fun <- IsValidFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -376,9 +390,10 @@ is_valid(Prefix, #{type := object} = Schema) -> [chain_conditions(BodyFunCalls, 'andalso')] ), FalseClause = false_clause(), + Clauses = clauses([OptionalClause, NullClause, TrueClause, FalseClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause, FalseClause] + Clauses ), {Fun, IsValidFuns ++ ExtraFuns}; is_valid(Prefix, #{one_of := Subschemas} = Schema) when is_list(Subschemas) -> @@ -404,16 +419,18 @@ is_valid(Prefix, #{one_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, [chain_conditions(BodyFunCalls, 'xor')] ), + Clauses = clauses([OptionalClause, NullClause, TrueClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause] + Clauses ), {Fun, IsValidFuns ++ ExtraFuns}; is_valid(Prefix, #{any_of := Subschemas} = Schema) when is_list(Subschemas) -> @@ -440,16 +457,18 @@ is_valid(Prefix, #{any_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, [chain_conditions(BodyFunCalls, 'orelse')] ), + Clauses = clauses([OptionalClause, NullClause, TrueClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause] + Clauses ), {Fun, IsValidFuns ++ ExtraFuns}; is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> @@ -476,22 +495,25 @@ is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, [chain_conditions(BodyFunCalls, 'andalso')] ), + Clauses = clauses([OptionalClause, NullClause, TrueClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause] + Clauses ), {Fun, IsValidFuns ++ ExtraFuns}; is_valid(Prefix, #{'not' := Subschema} = Schema) -> FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, Subschema), - OptionalClause = optional_clause(Schema), + OptionalClause = optional_clause(), + NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], none, @@ -505,9 +527,10 @@ is_valid(Prefix, #{'not' := Subschema} = Schema) -> ) ] ), + Clauses = clauses([OptionalClause, NullClause, TrueClause]), Fun = erl_syntax:function( erl_syntax:atom(erlang:binary_to_atom(FunName)), - OptionalClause ++ [TrueClause] + Clauses ), {Fun, [IsValidFun | ExtraFuns]}; is_valid(Prefix, _Schema) -> @@ -1538,6 +1561,9 @@ chain_conditions([FunCall | Rest], Operator, Acc) -> ), chain_conditions(Rest, Operator, NewAcc). +clauses(Clauses) -> + lists:filter(fun(Clause) -> Clause =/= undefined end, Clauses). + false_clause() -> erl_syntax:clause( [erl_syntax:variable('_Val')], @@ -1566,16 +1592,21 @@ literal(Val) when is_map(Val) -> || {K, V} <- maps:to_list(Val) ]). -optional_clause(#{nullable := true}) -> - [ - erl_syntax:clause( - [erl_syntax:atom('undefined')], - none, - [erl_syntax:atom(true)] - ) - ]; -optional_clause(_Schema) -> - []. +null_clause(#{nullable := true}) -> + erl_syntax:clause( + [erl_syntax:variable('null')], + none, + [erl_syntax:atom(true)] + ); +null_clause(_Schema) -> + undefined. + +optional_clause() -> + erl_syntax:clause( + [erl_syntax:atom('undefined')], + none, + [erl_syntax:atom(true)] + ). type_guard(Type) -> type_guard(Type, 'Val'). diff --git a/src/ndto_parser_json_schema_draft_04.erl b/src/ndto_parser_json_schema_draft_04.erl index 718752b..7d6f9b1 100644 --- a/src/ndto_parser_json_schema_draft_04.erl +++ b/src/ndto_parser_json_schema_draft_04.erl @@ -97,11 +97,10 @@ clean_schema(Schema) -> Result :: {ok, json_schema()} | {error, Reason}, Reason :: term(). deserialize_spec(Bin) -> - try - Data = njson:decode(Bin), - {ok, Data} - catch - _Error:Reason -> + case njson:decode(Bin) of + {ok, Spec} -> + {ok, Spec}; + {error, Reason} -> {error, {invalid_json, Reason}} end. diff --git a/test/ndto_SUITE.erl b/test/ndto_SUITE.erl index 1aed82c..48c1690 100644 --- a/test/ndto_SUITE.erl +++ b/test/ndto_SUITE.erl @@ -31,6 +31,7 @@ all() -> unique_items, pattern_properties, additional_properties, + required, {group, string_formats}, {group, examples} ]. @@ -159,7 +160,7 @@ nullable(_Conf) -> DTO1 = ndto:generate(test_nullable1, Schema1), ok = ndto:load(DTO1), - ?assertEqual(true, test_nullable1:is_valid(undefined)), + ?assertEqual(true, test_nullable1:is_valid(null)), Schema2 = #{ type => Type @@ -167,7 +168,7 @@ nullable(_Conf) -> DTO2 = ndto:generate(test_nullable2, Schema2), ok = ndto:load(DTO2), - ?assertEqual(false, test_nullable2:is_valid(undefined)) + ?assertEqual(false, test_nullable2:is_valid(null)) end, ndto_dom:types() ). @@ -325,6 +326,25 @@ additional_properties(_Conf) -> false, test_additional_properties4:is_valid(#{<<"Foo">> => true, <<"BAR">> => 1}) ). +required(_Conf) -> + Schema = #{ + type => object, + properties => #{ + <<"foo">> => #{ + type => string + }, + <<"bar">> => #{ + type => integer + } + }, + required => [<<"foo">>] + }, + DTO = ndto:generate(test_required, Schema), + ok = ndto:load(DTO), + + ?assertEqual(true, test_required:is_valid(#{<<"foo">> => <<"foobar">>})), + ok. + unique_items(_Conf) -> Schema = #{ type => array, @@ -372,7 +392,7 @@ petstore(_Conf) -> {ok, PetstoreBin} = file:read_file( erlang:list_to_binary(code:lib_dir(ndto, priv) ++ "/oas/3.0/examples/petstore.json") ), - Petstore = njson:decode(PetstoreBin), + {ok, Petstore} = njson:decode(PetstoreBin), ?assertEqual( true, From d8dd35756c48a6526e1075c96f2b2540afdefd0c Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Tue, 5 Dec 2023 12:20:32 +0100 Subject: [PATCH 4/6] fix(#97): handle optionality as false by default (#98) closes #97 --- .github/workflows/ci.yml | 1 - rebar.config | 2 +- src/ndto.erl | 11 +++++++++++ src/ndto_generator.erl | 34 +++++++++++++++++++--------------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 394cf01..df9400e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] env: OTP-VERSION: 25.2.3 diff --git a/rebar.config b/rebar.config index cafd9eb..a369a06 100644 --- a/rebar.config +++ b/rebar.config @@ -6,7 +6,7 @@ ]}. {project_plugins, [ - {erlfmt, {git, "git@github.com:WhatsApp/erlfmt.git", {branch, "main"}}}, + erlfmt, {gradualizer, {git, "git@github.com:josefs/Gradualizer.git", {branch, "master"}}}, rebar3_ex_doc ]}. diff --git a/src/ndto.erl b/src/ndto.erl index c52840f..24492c6 100644 --- a/src/ndto.erl +++ b/src/ndto.erl @@ -46,14 +46,17 @@ -type universal_schema() :: true | #{} | union_schema(). -type ref_schema() :: #{ ref := binary(), + optional => boolean(), nullable => boolean() }. -type enum_schema() :: #{ enum := [value()], + optional => boolean(), nullable => boolean() }. -type boolean_schema() :: #{ type := boolean, + optional => boolean(), nullable => boolean() }. -type integer_schema() :: #{ @@ -63,6 +66,7 @@ maximum => integer(), exclusive_maximum => boolean(), multiple_of => integer(), + optional => boolean(), nullable => boolean() }. -type float_schema() :: #{ @@ -71,6 +75,7 @@ exclusive_minimum => boolean(), maximum => float(), exclusive_maximum => boolean(), + optional => boolean(), nullable => boolean() }. -type string_schema() :: #{ @@ -79,6 +84,7 @@ max_length => non_neg_integer(), format => format(), pattern => pattern(), + optional => boolean(), nullable => boolean() }. -type array_schema() :: #{ @@ -87,6 +93,7 @@ min_items => non_neg_integer(), max_items => non_neg_integer(), unique_items => boolean(), + optional => boolean(), nullable => boolean() }. -type object_schema() :: #{ @@ -97,18 +104,22 @@ max_properties => non_neg_integer(), pattern_properties => #{pattern() => schema()}, additional_properties => schema(), + optional => boolean(), nullable => boolean() }. -type union_schema() :: #{ any_of := [schema()], + optional => boolean(), nullable => boolean() }. -type intersection_schema() :: #{ all_of := [schema()], + optional => boolean(), nullable => boolean() }. -type complement_schema() :: #{ 'not' := schema(), + optional => boolean(), nullable => boolean() }. -type symmetric_difference_schema() :: #{ diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index 0b5b05b..0be4464 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -95,7 +95,7 @@ generate(Name, Schema) -> is_valid(Prefix, #{ref := Ref} = Schema) -> FunName = <>, DTO = erlang:binary_to_atom(Ref), - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -119,7 +119,7 @@ is_valid(Prefix, #{ref := Ref} = Schema) -> {Fun, []}; is_valid(Prefix, #{enum := Enum} = Schema) -> FunName = <>, - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClauses = lists:map( fun(EnumVal) -> @@ -170,7 +170,7 @@ is_valid(Prefix, #{type := string} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -217,7 +217,7 @@ is_valid(Prefix, #{type := integer} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -262,7 +262,7 @@ is_valid(Prefix, #{type := float} = Schema) -> ) || Fun <- ExtraFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -279,7 +279,7 @@ is_valid(Prefix, #{type := float} = Schema) -> {Fun, ExtraFuns}; is_valid(Prefix, #{type := boolean} = Schema) -> FunName = <>, - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -329,7 +329,7 @@ is_valid(Prefix, #{type := array} = Schema) -> ) || Fun <- IsValidFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -381,7 +381,7 @@ is_valid(Prefix, #{type := object} = Schema) -> ) || Fun <- IsValidFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -419,7 +419,7 @@ is_valid(Prefix, #{one_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -457,7 +457,7 @@ is_valid(Prefix, #{any_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -495,7 +495,7 @@ is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> ) || IsValidFun <- IsValidFuns ], - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( @@ -512,7 +512,7 @@ is_valid(Prefix, #{all_of := Subschemas} = Schema) when is_list(Subschemas) -> is_valid(Prefix, #{'not' := Subschema} = Schema) -> FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, Subschema), - OptionalClause = optional_clause(), + OptionalClause = optional_clause(Schema), NullClause = null_clause(Schema), TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -756,7 +756,9 @@ is_valid_object(Prefix, properties, #{properties := Properties}) -> {PropertiesFuns, ExtraFuns} = maps:fold( fun(PropertyName, Property, {IsValidFunsAcc, ExtraFunsAcc}) -> {IsValidPropertyFun, ExtraPropertyFuns} = - is_valid(<>, Property), + is_valid(<>, Property#{ + optional => true + }), { [{PropertyName, IsValidPropertyFun} | IsValidFunsAcc], ExtraFunsAcc ++ ExtraPropertyFuns @@ -1601,12 +1603,14 @@ null_clause(#{nullable := true}) -> null_clause(_Schema) -> undefined. -optional_clause() -> +optional_clause(#{optional := true}) -> erl_syntax:clause( [erl_syntax:atom('undefined')], none, [erl_syntax:atom(true)] - ). + ); +optional_clause(_Schema) -> + undefined. type_guard(Type) -> type_guard(Type, 'Val'). From ecb52baafa44eba1d58661e4658e34b011aeb58c Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Tue, 9 Jan 2024 15:24:58 +0100 Subject: [PATCH 5/6] refactor: enhance ndto_parser interface and json_schema_draft_04 parser (#100) --- rebar.config | 16 +- rebar.lock | 2 +- src/ndto.erl | 6 +- src/ndto_generator.erl | 142 ++++++- src/ndto_parser.erl | 41 +- src/ndto_parser/ndto_parser_json_schema.erl | 213 ++++++++++ .../ndto_parser_json_schema_draft_04.erl | 398 +++++++++++++++++ src/ndto_parser_json_schema_draft_04.erl | 401 ------------------ test/ndto_SUITE.erl | 31 +- ....erl => ndto_parser_json_schema_SUITE.erl} | 18 +- 10 files changed, 822 insertions(+), 446 deletions(-) create mode 100644 src/ndto_parser/ndto_parser_json_schema.erl create mode 100644 src/ndto_parser/ndto_parser_json_schema/ndto_parser_json_schema_draft_04.erl delete mode 100644 src/ndto_parser_json_schema_draft_04.erl rename test/{ndto_parser_json_schema_draft_04_SUITE.erl => ndto_parser_json_schema_SUITE.erl} (89%) diff --git a/rebar.config b/rebar.config index a369a06..d371b23 100644 --- a/rebar.config +++ b/rebar.config @@ -7,6 +7,9 @@ {project_plugins, [ erlfmt, + {eqwalizer_rebar3, + {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, + "eqwalizer_rebar3"}}, {gradualizer, {git, "git@github.com:josefs/Gradualizer.git", {branch, "master"}}}, rebar3_ex_doc ]}. @@ -16,6 +19,9 @@ {test, [ {erl_opts, [nowarn_export_all]}, {deps, [ + {eqwalizer_support, + {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, + "eqwalizer_support"}}, {nct_util, {git, "git@github.com:nomasystems/nct_util.git", {branch, "main"}}}, {triq, {git, "git@github.com:nomasystems/triq.git", {branch, "master"}}} ]} @@ -54,12 +60,6 @@ {xref_ignores, [ ndto, - ndto_parser -]}. - -%% TODO: address this -{gradualizer_opts, [ - {exclude, [ - "src/ndto_parser_json_schema_draft_04.erl" - ]} + ndto_parser, + ndto_parser_json_schema ]}. diff --git a/rebar.lock b/rebar.lock index 091bc8c..f9cc7f4 100644 --- a/rebar.lock +++ b/rebar.lock @@ -4,5 +4,5 @@ 0}, {<<"njson">>, {git,"git@github.com:nomasystems/njson.git", - {ref,"338ecbac922343874765abebfbcc8af131724732"}}, + {ref,"b230b3e6fb5e35320aeaa203762f3f12277c9970"}}, 0}]. diff --git a/src/ndto.erl b/src/ndto.erl index 24492c6..a9efe72 100644 --- a/src/ndto.erl +++ b/src/ndto.erl @@ -89,7 +89,8 @@ }. -type array_schema() :: #{ type := array, - items => schema(), + items => schema() | [schema()], + additional_items => schema(), min_items => non_neg_integer(), max_items => non_neg_integer(), unique_items => boolean(), @@ -124,6 +125,7 @@ }. -type symmetric_difference_schema() :: #{ one_of := [schema()], + optional => boolean(), nullable => boolean() }. @@ -139,7 +141,7 @@ -type null() :: null. -type object() :: #{binary() => value()}. -type format() :: iso8601 | base64. -% TODO: use openapi defined formats +% TODO: support json_schema and openapi defined formats -type pattern() :: binary(). %%% TYPE EXPORTS diff --git a/src/ndto_generator.erl b/src/ndto_generator.erl index 0be4464..e396c15 100644 --- a/src/ndto_generator.erl +++ b/src/ndto_generator.erl @@ -92,6 +92,14 @@ generate(Name, Schema) -> Result :: {IsValidFun, ExtraFuns}, IsValidFun :: erl_syntax:syntaxTree(), ExtraFuns :: [erl_syntax:syntaxTree()]. +is_valid(Prefix, false) -> + FunName = <>, + FalseClause = false_clause(), + 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), @@ -302,8 +310,8 @@ is_valid(Prefix, #{type := array} = Schema) -> case maps:get(Keyword, Schema, undefined) of undefined -> Acc; - Value -> - case is_valid_array(<>, Keyword, Value) of + _Value -> + case is_valid_array(<>, Keyword, Schema) of {undefined, _EmptyList} -> Acc; {NewIsValidFun, NewExtraFuns} -> @@ -554,7 +562,7 @@ is_valid(Prefix, _Schema) -> Result :: {Fun, ExtraFuns}, Fun :: erl_syntax:syntaxTree() | undefined, ExtraFuns :: [erl_syntax:syntaxTree()]. -is_valid_array(Prefix, items, Items) -> +is_valid_array(Prefix, items, #{items := Items}) when is_map(Items) -> FunName = <>, {IsValidFun, ExtraFuns} = is_valid(<>, Items), TrueClause = erl_syntax:clause( @@ -587,7 +595,125 @@ is_valid_array(Prefix, items, Items) -> [TrueClause] ), {Fun, [IsValidFun | ExtraFuns]}; -is_valid_array(Prefix, min_items, MinItems) -> +is_valid_array(Prefix, items, #{items := Items} = Schema) when is_list(Items) -> + {_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 + ), + FunName = <>, + 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:application( + erl_syntax:atom(lists), + erl_syntax:atom(all), + [ + erl_syntax:fun_expr([ + erl_syntax:clause( + [ + erl_syntax:tuple([ + erl_syntax:variable('Item'), + erl_syntax:variable('FunKey') + ]) + ], + none, + [ + 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:application( + erl_syntax:function_name( + IsValidAdditionalItemsFun + ), + [erl_syntax:variable('Item')] + ) + ] + ), + erl_syntax:clause( + [erl_syntax:variable('IsValidItemFun')], + none, + [ + erl_syntax:application( + erl_syntax:variable( + 'IsValidItemFun' + ), + [ + erl_syntax:variable( + 'Item' + ) + ] + ) + ] + ) + ] + ) + ] + ) + ]), + 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')] + ) + ] + ) + ] + ) + ] + ) + ] + ), + 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')], @@ -604,7 +730,7 @@ is_valid_array(Prefix, min_items, MinItems) -> [TrueClause, FalseClause] ), {Fun, []}; -is_valid_array(Prefix, max_items, MaxItems) -> +is_valid_array(Prefix, max_items, #{max_items := MaxItems}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -621,7 +747,7 @@ is_valid_array(Prefix, max_items, MaxItems) -> [TrueClause, FalseClause] ), {Fun, []}; -is_valid_array(Prefix, unique_items, true) -> +is_valid_array(Prefix, unique_items, #{unique_items := true}) -> FunName = <>, TrueClause = erl_syntax:clause( [erl_syntax:variable('Val')], @@ -649,7 +775,7 @@ is_valid_array(Prefix, unique_items, true) -> [TrueClause] ), {Fun, []}; -is_valid_array(_Prefix, unique_items, false) -> +is_valid_array(_Prefix, unique_items, #{unique_items := false}) -> {undefined, []}. -spec is_valid_number(Type, Prefix, Keyword, Value, Schema) -> Result when @@ -1576,6 +1702,8 @@ false_clause() -> guard(Pred, Var) -> erl_syntax:application(erl_syntax:atom(Pred), [erl_syntax:variable(Var)]). +literal(null) -> + erl_syntax:atom(null); literal(Val) when is_boolean(Val) -> erl_syntax:atom(Val); literal(Val) when is_integer(Val) -> diff --git a/src/ndto_parser.erl b/src/ndto_parser.erl index bd6be40..25b95f0 100644 --- a/src/ndto_parser.erl +++ b/src/ndto_parser.erl @@ -12,46 +12,59 @@ %% See the License for the specific language governing permissions and %% limitations under the License -%% @doc An ndto behaviour for schema parsers. +%% @doc An ndto interface and behaviour for schema parsers. -module(ndto_parser). %%% EXTERNAL EXPORTS -export([ + parse/2, parse/3 ]). %%% TYPES +-type ctx() :: term(). +-type spec() :: term(). -opaque t() :: module(). % A parser is a module that implements the ndto_parser behaviour. %%% EXPORT TYPES -export_type([ + ctx/0, + spec/0, t/0 ]). %%%----------------------------------------------------------------------------- %%% BEHAVIOUR CALLBACKS %%%----------------------------------------------------------------------------- --callback parse(Namespace, SpecPath) -> Result when - Namespace :: atom(), - SpecPath :: binary(), +-callback parse(SpecPath, Opts) -> Result when + SpecPath :: file:filename_all(), + Opts :: map(), Result :: {ok, Schemas} | {error, Reason}, - Schemas :: [{SchemaName, ndto:schema()}], - SchemaName :: ndto:name(), + Schemas :: [{ndto:name(), ndto:schema()}], Reason :: term(). -% Parses a specification into a ndto:schema() +% Parses a specification into a list of ndto:schema() values. %%%----------------------------------------------------------------------------- %%% EXTERNAL EXPORTS %%%----------------------------------------------------------------------------- --spec parse(Parser, Namespace, SpecPath) -> Result when +-spec parse(Parser, SpecPath) -> Result when Parser :: t(), - Namespace :: atom(), - SpecPath :: binary(), + SpecPath :: file:filename_all(), Result :: {ok, Schemas} | {error, Reason}, - Schemas :: [{SchemaName, ndto:schema()}], - SchemaName :: ndto:name(), + Schemas :: [{ndto:name(), ndto:schema()}], + Reason :: term(). +%% @equiv parse(Parser, SpecPath, #{}) +parse(Parser, SpecPath) -> + parse(Parser, SpecPath, #{}). + +-spec parse(Parser, SpecPath, Opts) -> Result when + Parser :: t(), + SpecPath :: file:filename_all(), + Opts :: map(), + Result :: {ok, Schemas} | {error, Reason}, + Schemas :: [{ndto:name(), ndto:schema()}], Reason :: term(). %% @doc Parses a schema specification into a ndto:schema() using the given parser. -parse(Parser, Namespace, SpecPath) -> - Parser:parse(Namespace, SpecPath). +parse(Parser, SpecPath, Opts) -> + Parser:parse(SpecPath, Opts). diff --git a/src/ndto_parser/ndto_parser_json_schema.erl b/src/ndto_parser/ndto_parser_json_schema.erl new file mode 100644 index 0000000..c32ac04 --- /dev/null +++ b/src/ndto_parser/ndto_parser_json_schema.erl @@ -0,0 +1,213 @@ +%%% 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 + +%% @doc An ndto interface for parsing JSON Schemas. +-module(ndto_parser_json_schema). + +%%% EXTERNAL EXPORTS +-export([ + parse/2 +]). + +%%% UTIL EXPORTS +-export([ + clean_optionals/1, + get/2, + parse_spec/1, + resolve_ref/2 +]). + +%%% TYPES +-type ctx() :: #{ + base_path := binary(), + base_name := binary(), + resolved := [binary()], + spec := spec() +}. +-type opts() :: #{ + name => atom() +}. +-type spec() :: njson:t(). +-opaque t() :: module(). +% A parser is a module that implements the ndto_parser_json_schema behaviour. + +%%% EXPORT TYPES +-export_type([ + ctx/0, + spec/0, + t/0 +]). + +%%%----------------------------------------------------------------------------- +%%% BEHAVIOUR CALLBACKS +%%%----------------------------------------------------------------------------- +-callback parse(Spec, CTX) -> Result when + Spec :: spec(), + CTX :: ctx(), + Result :: {Schema, ExtraSchemas, NewCTX}, + Schema :: ndto:schema(), + ExtraSchemas :: [{ndto:name(), ndto:schema()}], + NewCTX :: ctx(). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +-spec parse(SpecPath, Opts) -> Result when + SpecPath :: file:filename_all(), + Opts :: opts(), + Result :: {ok, Schemas} | {error, Reason}, + Schemas :: [{ndto:name(), ndto:schema()}], + Reason :: term(). +%% @doc Parses a JSONSchema specification into a list of ndto:schema() values. +parse(SpecPath, Opts) -> + case parse_spec(SpecPath) of + {ok, Spec} -> + case parser(Spec) of + {ok, Parser} -> + BasePath = filename:dirname(SpecPath), + BaseName = filename:rootname(filename:basename(SpecPath)), + CTX = #{ + base_path => BasePath, + base_name => BaseName, + resolved => [], + spec => Spec + }, + {Schema, ExtraSchemas, _NewCTX} = Parser:parse(Spec, CTX), + Name = maps:get(name, Opts, erlang:binary_to_atom(BaseName)), + RawSchemas = [{Name, Schema} | ExtraSchemas], + Schemas = [ + {SchemaName, clean_optionals(RawSchema)} + || {SchemaName, RawSchema} <- RawSchemas + ], + {ok, Schemas}; + {error, Reason} -> + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end. + +%%%----------------------------------------------------------------------------- +%%% UTIL EXPORTS +%%%----------------------------------------------------------------------------- +-spec clean_optionals(RawValue) -> Value when + RawValue :: term(), + Value :: term(). +clean_optionals(RawMap) when is_map(RawMap) -> + maps:fold( + fun + (_Key, undefined, Acc) -> + Acc; + (Key, Value, Acc) -> + maps:put(Key, clean_optionals(Value), Acc) + end, + #{}, + RawMap + ); +clean_optionals(RawList) when is_list(RawList) -> + [clean_optionals(Value) || Value <- RawList, Value =/= undefined]; +clean_optionals(Value) -> + Value. + +-spec get(Keys, Spec) -> Result when + Keys :: [binary()], + Spec :: map(), + Result :: term(). +get([], Spec) -> + Spec; +get([Key | Keys], Spec) -> + get(Keys, maps:get(Key, Spec)). + +-spec parse_spec(SpecPath) -> Result when + SpecPath :: binary(), + Result :: {ok, Spec} | {error, Reason}, + Spec :: ndto_parser:spec(), + Reason :: term(). +parse_spec(SpecPath) -> + case file:read_file(SpecPath) of + {ok, BinSpec} -> + case filename:extension(SpecPath) of + JSON when JSON =:= <<".json">> orelse JSON =:= ".json" -> + case njson:decode(BinSpec) of + {ok, Spec} -> + {ok, Spec}; + {error, Reason} -> + {error, {invalid_json, Reason}} + end; + Extension -> + {error, {unsupported_extension, Extension}} + end; + {error, Reason} -> + {error, {invalid_spec, Reason}} + end. + +-spec resolve_ref(Ref, CTX) -> Result when + Ref :: binary(), + CTX :: ctx(), + Result :: {NewResolved, NewSchema, NewCTX}, + NewResolved :: binary(), + NewSchema :: ndto:schema(), + NewCTX :: ctx(). +resolve_ref(Ref, CTX) -> + BasePath = maps:get(base_path, CTX), + BaseName = maps:get(base_name, CTX), + Resolved = maps:get(resolved, CTX), + Spec = maps:get(spec, CTX), + + [FilePath, ElementPath] = binary:split(Ref, <<"#">>, [global]), + LocalPath = binary:split(ElementPath, <<"/">>, [global, trim_all]), + {NewSpec, NewBasePath, NewBaseName} = + case FilePath of + <<>> -> + {Spec, BasePath, BaseName}; + _FilePath -> + AbsPath = filename:join(BasePath, FilePath), + case parse_spec(AbsPath) of + {ok, RefSpec} -> + RefBasePath = filename:dirname(AbsPath), + RefBaseName = filename:rootname(filename:basename(AbsPath)), + {RefSpec, RefBasePath, RefBaseName}; + {error, Reason} -> + % TODO: Handle error + erlang:error({invalid_ref, Reason}) + end + end, + NewResolved = + case LocalPath of + [] -> + NewBaseName; + _LocalPath -> + <> + end, + NewSchema = get(LocalPath, NewSpec), + NewCTX = #{ + base_path => NewBasePath, + base_name => NewBaseName, + resolved => [NewResolved | Resolved], + spec => NewSpec + }, + {NewResolved, NewSchema, NewCTX}. + +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- +-spec parser(Spec) -> Result when + Spec :: spec(), + Result :: {ok, Parser} | {error, Reason}, + Parser :: t(), + Reason :: term(). +parser(#{<<"$schema">> := <<"http://json-schema.org/draft-04/schema#">>}) -> + {ok, ndto_parser_json_schema_draft_04}; +parser(_Schema) -> + {error, unsupported_draft}. diff --git a/src/ndto_parser/ndto_parser_json_schema/ndto_parser_json_schema_draft_04.erl b/src/ndto_parser/ndto_parser_json_schema/ndto_parser_json_schema_draft_04.erl new file mode 100644 index 0000000..7706cd4 --- /dev/null +++ b/src/ndto_parser/ndto_parser_json_schema/ndto_parser_json_schema_draft_04.erl @@ -0,0 +1,398 @@ +%%% 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 + +%% @doc A ndto parser for draft-04 JSON Schema specifications. +-module(ndto_parser_json_schema_draft_04). + +%%% BEHAVIOURS +-behaviour(ndto_parser_json_schema). + +%%% EXTERNAL EXPORTS +-export([ + parse/2 +]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +-spec parse(Spec, CTX) -> Result when + Spec :: ndto_parser_json_schema:spec(), + CTX :: ndto_parser_json_schema:ctx(), + Result :: {Schema, ExtraSchemas, NewCTX}, + Schema :: ndto:schema(), + ExtraSchemas :: [{ndto:name(), ndto:schema()}], + NewCTX :: ndto_parser_json_schema:ctx(). +%% @doc Parses a JSONSchema draft-04 specification into a list of ndto:schema() values. +parse(false, CTX) -> + Schema = false, + {Schema, [], CTX}; +parse(true, CTX) -> + Schema = #{}, + {Schema, [], CTX}; +parse(#{<<"$ref">> := Ref}, CTX) -> + {RefName, RefSchema, RefCTX} = ndto_parser_json_schema:resolve_ref(Ref, CTX), + Schema = #{ref => RefName}, + case lists:member(RefName, maps:get(resolved, CTX)) of + true -> + {Schema, [], CTX}; + false -> + {NewSchema, NewExtraSchemas, NewCTX} = parse(RefSchema, RefCTX), + { + Schema, + [{erlang:binary_to_atom(RefName), NewSchema} | NewExtraSchemas], + CTX#{resolved => maps:get(resolved, NewCTX)} + } + end; +parse(#{<<"enum">> := Enum}, CTX) -> + Schema = #{enum => Enum}, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"null">>}, CTX) -> + Schema = #{enum => [<<"null">>]}, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"boolean">>}, CTX) -> + Schema = #{type => boolean}, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"integer">>} = RawSchema, CTX) -> + Minimum = maps:get(<<"minimum">>, RawSchema, undefined), + ExclusiveMinimum = maps:get(<<"exclusiveMinimum">>, RawSchema, undefined), + Maximum = maps:get(<<"maximum">>, RawSchema, undefined), + ExclusiveMaximum = maps:get(<<"exclusiveMaximum">>, RawSchema, undefined), + MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), + Schema = + #{ + type => integer, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf + }, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"number">>} = RawSchema, CTX) -> + Minimum = maps:get(<<"minimum">>, RawSchema, undefined), + ExclusiveMinimum = maps:get(<<"exclusiveMinimum">>, RawSchema, undefined), + Maximum = maps:get(<<"maximum">>, RawSchema, undefined), + ExclusiveMaximum = maps:get(<<"exclusiveMaximum">>, RawSchema, undefined), + MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), + Schema = + #{ + any_of => [ + #{ + type => integer, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf + }, + #{ + type => float, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum + } + ] + }, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"string">>} = RawSchema, CTX) -> + MinLength = maps:get(<<"minLength">>, RawSchema, undefined), + MaxLength = maps:get(<<"maxLength">>, RawSchema, undefined), + Format = + case maps:get(<<"format">>, RawSchema, undefined) of + <<"iso8601">> -> + iso8601; + <<"byte">> -> + base64; + _Otherwise -> + undefined + end, + Pattern = maps:get(<<"pattern">>, RawSchema, undefined), + Schema = + #{ + type => string, + min_length => MinLength, + max_length => MaxLength, + format => Format, + pattern => Pattern + }, + {Schema, [], CTX}; +parse(#{<<"type">> := <<"array">>} = RawSchema, CTX) -> + {Items, ItemsExtraSchemas, ItemsCTX} = + case maps:get(<<"items">>, RawSchema, undefined) of + undefined -> + {undefined, [], CTX}; + RawItems when is_list(RawItems) -> + {IS, ES, CT} = lists:foldl( + fun(RawItemSchema, {ItemsAcc, ExtraSchemasAcc, CTXAcc}) -> + {ItemSchema, ExtraSchemas, NewCTX} = parse( + RawItemSchema, CTXAcc + ), + { + [ItemSchema | ItemsAcc], + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{resolved => maps:get(resolved, NewCTX)} + } + end, + {[], [], CTX}, + RawItems + ), + {lists:reverse(IS), ES, CT}; + RawItems -> + parse(RawItems, CTX) + end, + {AdditionalItems, AdditionalItemsExtraSchemas, AdditionalItemsCTX} = + case maps:get(<<"additionalItems">>, RawSchema, undefined) of + undefined -> + {undefined, [], ItemsCTX}; + RawAdditionalItems -> + parse(RawAdditionalItems, ItemsCTX) + end, + MinItems = maps:get(<<"minItems">>, RawSchema, undefined), + MaxItems = maps:get(<<"maxItems">>, RawSchema, undefined), + UniqueItems = maps:get(<<"uniqueItems">>, RawSchema, undefined), + Schema = + #{ + type => array, + items => Items, + additional_items => AdditionalItems, + min_items => MinItems, + max_items => MaxItems, + unique_items => UniqueItems + }, + {Schema, ItemsExtraSchemas ++ AdditionalItemsExtraSchemas, AdditionalItemsCTX}; +parse(#{<<"type">> := <<"object">>} = RawSchema, CTX) -> + {Properties, PropertiesExtraSchemas, PropertiesCTX} = + case maps:get(<<"properties">>, RawSchema, undefined) of + undefined -> + {undefined, [], CTX}; + RawProperties -> + lists:foldl( + fun({Property, RawPropertySchema}, {PropertiesAcc, ExtraSchemasAcc, CTXAcc}) -> + {PropertySchema, ExtraSchemas, NewCTX} = parse( + RawPropertySchema, CTXAcc + ), + { + PropertiesAcc#{Property => PropertySchema}, + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{resolved => maps:get(resolved, NewCTX)} + } + end, + {#{}, [], CTX}, + maps:to_list(RawProperties) + ) + end, + Required = maps:get(<<"required">>, RawSchema, undefined), + MinProperties = maps:get(<<"minProperties">>, RawSchema, undefined), + MaxProperties = maps:get(<<"maxProperties">>, RawSchema, undefined), + {AdditionalProperties, AdditionalPropertiesExtraSchemas, AdditionalPropertiesCTX} = + case maps:get(<<"additionalProperties">>, RawSchema, undefined) of + undefined -> + {undefined, [], PropertiesCTX}; + RawAdditionalProperties -> + parse(RawAdditionalProperties, PropertiesCTX) + end, + {PatternProperties, PatternPropertiesExtraSchemas, PatternPropertiesCTX} = + case maps:get(<<"patternProperties">>, RawSchema, undefined) of + undefined -> + {undefined, [], AdditionalPropertiesCTX}; + RawPatternProperties -> + lists:foldl( + fun( + {Pattern, RawPatternSchema}, {PatternPropertiesAcc, ExtraSchemasAcc, CTXAcc} + ) -> + {PatternSchema, ExtraSchemas, NewCTX} = parse( + RawPatternSchema, CTXAcc + ), + { + PatternPropertiesAcc#{Pattern => PatternSchema}, + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{resolved => maps:get(resolved, NewCTX)} + } + end, + {#{}, [], AdditionalPropertiesCTX}, + maps:to_list(RawPatternProperties) + ) + end, + Schema = + #{ + type => object, + properties => Properties, + required => Required, + min_properties => MinProperties, + max_properties => MaxProperties, + additional_properties => AdditionalProperties, + pattern_properties => PatternProperties + }, + {Schema, + PropertiesExtraSchemas ++ AdditionalPropertiesExtraSchemas ++ PatternPropertiesExtraSchemas, + PatternPropertiesCTX}; +parse(#{<<"anyOf">> := RawAnyOf}, CTX) -> + {AnyOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSchema, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, ExtraSchemas, NewCTX} = parse(RawSchema, CTXAcc), + {[Schema | AnyOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + }} + end, + {[], [], CTX}, + RawAnyOf + ), + Schema = #{any_of => AnyOf}, + {Schema, ExtraSchemas, NewCTX}; +parse(#{<<"allOf">> := RawAllOf}, CTX) -> + {AllOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSchema, {AllOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, ExtraSchemas, NewCTX} = parse(RawSchema, CTXAcc), + {[Schema | AllOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + }} + end, + {[], [], CTX}, + RawAllOf + ), + Schema = #{all_of => AllOf}, + {Schema, ExtraSchemas, NewCTX}; +parse(#{<<"not">> := RawNot}, CTX) -> + {Not, ExtraSchemas, NewCTX} = parse(RawNot, CTX), + Schema = #{'not' => Not}, + {Schema, ExtraSchemas, NewCTX}; +parse(#{<<"oneOf">> := RawOneOf}, CTX) -> + {OneOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSchema, {OneOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, ExtraSchemas, NewCTX} = parse(RawSchema, CTXAcc), + {[Schema | OneOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + }} + end, + {[], [], CTX}, + RawOneOf + ), + Schema = #{one_of => OneOf}, + {Schema, ExtraSchemas, NewCTX}; +parse(UniversalSchema, CTX) -> + RawSchema = attempt_type(UniversalSchema), + parse(RawSchema, CTX). + +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- +-spec attempt_type(Schema) -> Result when + Schema :: ndto_parser_json_schema:spec(), + Result :: ndto_parser_json_schema:spec(). +attempt_type(Schema) -> + Base = #{ + boolean => #{<<"type">> => <<"boolean">>}, + number => #{<<"type">> => <<"number">>}, + string => #{<<"type">> => <<"string">>}, + array => #{<<"type">> => <<"array">>}, + object => #{<<"type">> => <<"object">>} + }, + Keywords = attempt_type(maps:to_list(Schema), Base), + Boolean = maps:get(boolean, Keywords, #{}), + Number = maps:get(number, Keywords, #{}), + String = maps:get(string, Keywords, #{}), + Array = maps:get(array, Keywords, #{}), + Object = maps:get(object, Keywords, #{}), + #{ + <<"anyOf">> => [ + Boolean, + Number, + String, + Array, + Object + ] + }. + +attempt_type([], Acc) -> + Acc; +attempt_type([{<<"minimum">>, Minimum} | Rest], #{number := OldNumber} = OldAcc) -> + Acc = OldAcc#{number => OldNumber#{<<"minimum">> => Minimum}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"exclusiveMinimum">>, ExclusiveMinimum} | Rest], #{number := OldNumber} = OldAcc +) -> + Acc = OldAcc#{number => OldNumber#{<<"exclusiveMinimum">> => ExclusiveMinimum}}, + attempt_type(Rest, Acc); +attempt_type([{<<"maximum">>, Maximum} | Rest], #{number := OldNumber} = OldAcc) -> + Acc = OldAcc#{number => OldNumber#{<<"maximum">> => Maximum}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"exclusiveMaximum">>, ExclusiveMaximum} | Rest], #{number := OldNumber} = OldAcc +) -> + Acc = OldAcc#{number => OldNumber#{<<"exclusiveMaximum">> => ExclusiveMaximum}}, + attempt_type(Rest, Acc); +attempt_type([{<<"multipleOf">>, MultipleOf} | Rest], #{number := OldNumber} = OldAcc) -> + Acc = OldAcc#{number => OldNumber#{<<"multipleOf">> => MultipleOf}}, + attempt_type(Rest, Acc); +attempt_type([{<<"minLength">>, MinLength} | Rest], #{string := OldString} = OldAcc) -> + Acc = OldAcc#{string => OldString#{<<"minLength">> => MinLength}}, + attempt_type(Rest, Acc); +attempt_type([{<<"maxLength">>, MaxLength} | Rest], #{string := OldString} = OldAcc) -> + Acc = OldAcc#{string => OldString#{<<"maxLength">> => MaxLength}}, + attempt_type(Rest, Acc); +attempt_type([{<<"format">>, Format} | Rest], #{string := OldString} = OldAcc) -> + Acc = OldAcc#{string => OldString#{<<"format">> => Format}}, + attempt_type(Rest, Acc); +attempt_type([{<<"pattern">>, Pattern} | Rest], #{string := OldString} = OldAcc) -> + Acc = OldAcc#{string => OldString#{<<"pattern">> => Pattern}}, + attempt_type(Rest, Acc); +attempt_type([{<<"minItems">>, MinItems} | Rest], #{array := OldArray} = OldAcc) -> + Acc = OldAcc#{array => OldArray#{<<"minItems">> => MinItems}}, + attempt_type(Rest, Acc); +attempt_type([{<<"maxItems">>, MaxItems} | Rest], #{array := OldArray} = OldAcc) -> + Acc = OldAcc#{array => OldArray#{<<"maxItems">> => MaxItems}}, + attempt_type(Rest, Acc); +attempt_type([{<<"uniqueItems">>, UniqueItems} | Rest], #{array := OldArray} = OldAcc) -> + Acc = OldAcc#{array => OldArray#{<<"uniqueItems">> => UniqueItems}}, + attempt_type(Rest, Acc); +attempt_type([{<<"items">>, Items} | Rest], #{array := OldArray} = OldAcc) -> + Acc = OldAcc#{array => OldArray#{<<"items">> => Items}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"additionalItems">>, AdditionalItems} | Rest], #{array := OldArray} = OldAcc +) -> + Acc = OldAcc#{array => OldArray#{<<"additionalItems">> => AdditionalItems}}, + attempt_type(Rest, Acc); +attempt_type([{<<"properties">>, Properties} | Rest], #{object := OldObject} = OldAcc) -> + Acc = OldAcc#{object => OldObject#{<<"properties">> => Properties}}, + attempt_type(Rest, Acc); +attempt_type([{<<"required">>, Required} | Rest], #{object := OldObject} = OldAcc) -> + Acc = OldAcc#{object => OldObject#{<<"required">> => Required}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"minProperties">>, MinProperties} | Rest], #{object := OldObject} = OldAcc +) -> + Acc = OldAcc#{object => OldObject#{<<"minProperties">> => MinProperties}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"maxProperties">>, MaxProperties} | Rest], #{object := OldObject} = OldAcc +) -> + Acc = OldAcc#{object => OldObject#{<<"maxProperties">> => MaxProperties}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"additionalProperties">>, AdditionalProperties} | Rest], #{object := OldObject} = OldAcc +) -> + Acc = OldAcc#{object => OldObject#{<<"additionalProperties">> => AdditionalProperties}}, + attempt_type(Rest, Acc); +attempt_type( + [{<<"patternProperties">>, PatternProperties} | Rest], #{object := OldObject} = OldAcc +) -> + Acc = OldAcc#{object => OldObject#{<<"patternProperties">> => PatternProperties}}, + attempt_type(Rest, Acc); +attempt_type([_UnknownKeyword | Rest], Acc) -> + attempt_type(Rest, Acc). diff --git a/src/ndto_parser_json_schema_draft_04.erl b/src/ndto_parser_json_schema_draft_04.erl deleted file mode 100644 index 7d6f9b1..0000000 --- a/src/ndto_parser_json_schema_draft_04.erl +++ /dev/null @@ -1,401 +0,0 @@ -%%% 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 - -%% @doc An ndto_parser for draft-04 JSON Schema specifications. --module(ndto_parser_json_schema_draft_04). - -%%% BEHAVIOURS --behaviour(ndto_parser). - -%%% EXTERNAL EXPORTS --export([ - parse/2 -]). - -%%% TYPES --type json_schema() :: njson:t(). - -%%% RECORDS --record(ctx, { - base_path :: binary(), - namespace :: binary(), - resolved :: [binary()], - spec :: json_schema() -}). - -%%%----------------------------------------------------------------------------- -%%% EXTERNAL EXPORTS -%%%----------------------------------------------------------------------------- --spec parse(Namespace, SpecPath) -> Result when - Namespace :: atom(), - SpecPath :: binary(), - Result :: {ok, Schemas} | {error, Reason}, - Schemas :: [{ndto:name(), ndto:schema()}], - Reason :: term(). -%% @doc Parses a draft-04 JSON Schema specification into a list of ndto:schema() values. -parse(RawNamespace, SpecPath) -> - Namespace = erlang:atom_to_binary(RawNamespace), - case read_spec(SpecPath) of - {ok, BinSpec} -> - case deserialize_spec(BinSpec) of - {ok, Spec} -> - CTX = #ctx{ - base_path = filename:dirname(SpecPath), - namespace = Namespace, - resolved = [], - spec = Spec - }, - {Schema, ExtraSchemas, _CTX} = parse_schemas(CTX, Spec), - RawSchemas = [{erlang:binary_to_atom(Namespace), Schema} | ExtraSchemas], - Schemas = [ - {Name, clean_schema(RawSchema)} - || {Name, RawSchema} <- RawSchemas - ], - {ok, Schemas}; - {error, Reason} -> - {error, Reason} - end; - {error, Reason} -> - {error, Reason} - end. - -%%%----------------------------------------------------------------------------- -%%% INTERNAL FUNCTIONS -%%%----------------------------------------------------------------------------- --spec clean_schema(RawSchema) -> Schema when - RawSchema :: ndto:schema(), - Schema :: ndto:schema(). -clean_schema(RawSchema) when is_map(RawSchema) -> - maps:fold( - fun - (_Key, undefined, Acc) -> - Acc; - (Key, List, Acc) when is_list(List) -> - maps:put(Key, [clean_schema(Value) || Value <- List, Value =/= undefined], Acc); - (Key, Value, Acc) -> - maps:put(Key, clean_schema(Value), Acc) - end, - #{}, - RawSchema - ); -clean_schema(Schema) -> - Schema. - --spec deserialize_spec(Bin) -> Result when - Bin :: binary(), - Result :: {ok, json_schema()} | {error, Reason}, - Reason :: term(). -deserialize_spec(Bin) -> - case njson:decode(Bin) of - {ok, Spec} -> - {ok, Spec}; - {error, Reason} -> - {error, {invalid_json, Reason}} - end. - --spec get(Keys, Spec) -> Result when - Keys :: [binary()], - Spec :: map(), - Result :: term(). -get([], Spec) -> - Spec; -get([Key | Keys], Spec) -> - get(Keys, maps:get(Key, Spec)). - --spec parse_schemas(CTX, Spec) -> Result when - CTX :: #ctx{}, - Spec :: json_schema(), - Result :: {Schema, ExtraSchemas, NewCTX}, - Schema :: ndto:schema(), - ExtraSchemas :: [{ndto:name(), ndto:schema()}], - NewCTX :: #ctx{}. -parse_schemas(CTX, false) -> - Schema = false, - {Schema, [], CTX}; -parse_schemas(CTX, true) -> - Schema = #{}, - {Schema, [], CTX}; -parse_schemas(CTX, UniversalSchema) when is_map(UniversalSchema), map_size(UniversalSchema) =:= 0 -> - Schema = #{}, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"$ref">> := Ref}) -> - {RefName, RefSchema, RefCTX} = resolve_ref(Ref, CTX), - Schema = #{ref => RefName}, - case lists:member(RefName, CTX#ctx.resolved) of - true -> - {Schema, [], CTX}; - false -> - {NewSchema, NewExtraSchemas, NewCTX} = parse_schemas(RefCTX, RefSchema), - {Schema, [{erlang:binary_to_atom(RefName), NewSchema} | NewExtraSchemas], CTX#ctx{ - resolved = NewCTX#ctx.resolved - }} - end; -parse_schemas(CTX, #{<<"enum">> := Enum}) -> - Schema = #{enum => Enum}, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"type">> := <<"boolean">>}) -> - Schema = #{type => boolean}, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"type">> := <<"integer">>} = RawSchema) -> - Minimum = maps:get(<<"minimum">>, RawSchema, undefined), - ExclusiveMinimum = maps:get(<<"exclusiveMinimum">>, RawSchema, undefined), - Maximum = maps:get(<<"maximum">>, RawSchema, undefined), - ExclusiveMaximum = maps:get(<<"exclusiveMaximum">>, RawSchema, undefined), - MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), - Schema = - #{ - type => integer, - minimum => Minimum, - exclusive_minimum => ExclusiveMinimum, - maximum => Maximum, - exclusive_maximum => ExclusiveMaximum, - multiple_of => MultipleOf - }, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"type">> := <<"number">>} = RawSchema) -> - Minimum = maps:get(<<"minimum">>, RawSchema, undefined), - ExclusiveMinimum = maps:get(<<"exclusiveMinimum">>, RawSchema, undefined), - Maximum = maps:get(<<"maximum">>, RawSchema, undefined), - ExclusiveMaximum = maps:get(<<"exclusiveMaximum">>, RawSchema, undefined), - MultipleOf = maps:get(<<"multipleOf">>, RawSchema, undefined), - Schema = - #{ - any_of => [ - #{ - type => integer, - minimum => Minimum, - exclusive_minimum => ExclusiveMinimum, - maximum => Maximum, - exclusive_maximum => ExclusiveMaximum, - multiple_of => MultipleOf - }, - #{ - type => float, - minimum => Minimum, - exclusive_minimum => ExclusiveMinimum, - maximum => Maximum, - exclusive_maximum => ExclusiveMaximum - } - ] - }, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"type">> := <<"string">>} = RawSchema) -> - MinLength = maps:get(<<"minLength">>, RawSchema, undefined), - MaxLength = maps:get(<<"maxLength">>, RawSchema, undefined), - Format = - case maps:get(<<"format">>, RawSchema, undefined) of - <<"iso8601">> -> - iso8601; - <<"byte">> -> - base64; - _Otherwise -> - undefined - end, - Pattern = maps:get(<<"pattern">>, RawSchema, undefined), - Schema = - #{ - type => string, - min_length => MinLength, - max_length => MaxLength, - format => Format, - pattern => Pattern - }, - {Schema, [], CTX}; -parse_schemas(CTX, #{<<"type">> := <<"array">>} = RawSchema) -> - {Items, ItemsExtraSchemas, ItemsCTX} = - case maps:get(<<"items">>, RawSchema, undefined) of - undefined -> - {undefined, []}; - RawItems -> - parse_schemas(CTX, RawItems) - end, - MinItems = maps:get(<<"minItems">>, RawSchema, undefined), - MaxItems = maps:get(<<"maxItems">>, RawSchema, undefined), - UniqueItems = maps:get(<<"uniqueItems">>, RawSchema, undefined), - Schema = - #{ - type => array, - items => Items, - min_items => MinItems, - max_items => MaxItems, - unique_items => UniqueItems - }, - {Schema, ItemsExtraSchemas, ItemsCTX}; -parse_schemas(CTX, #{<<"type">> := <<"object">>} = RawSchema) -> - {Properties, PropertiesExtraSchemas, PropertiesCTX} = - case maps:get(<<"properties">>, RawSchema, undefined) of - undefined -> - {undefined, [], CTX}; - RawProperties -> - lists:foldl( - fun({Property, RawPropertySchema}, {PropertiesAcc, ExtraSchemasAcc, CTXAcc}) -> - {PropertySchema, ExtraSchemas, NewCTX} = parse_schemas( - CTXAcc, RawPropertySchema - ), - { - PropertiesAcc#{Property => PropertySchema}, - ExtraSchemasAcc ++ ExtraSchemas, - CTXAcc#ctx{resolved = NewCTX#ctx.resolved} - } - end, - {#{}, [], CTX}, - maps:to_list(RawProperties) - ) - end, - Required = maps:get(<<"required">>, RawSchema, undefined), - MinProperties = maps:get(<<"minProperties">>, RawSchema, undefined), - MaxProperties = maps:get(<<"maxProperties">>, RawSchema, undefined), - {AdditionalProperties, AdditionalPropertiesExtraSchemas, AdditionalPropertiesCTX} = - case maps:get(<<"additionalProperties">>, RawSchema, undefined) of - undefined -> - {undefined, [], PropertiesCTX}; - RawAdditionalProperties -> - parse_schemas(PropertiesCTX, RawAdditionalProperties) - end, - {PatternProperties, PatternPropertiesExtraSchemas, PatternPropertiesCTX} = - case maps:get(<<"patternProperties">>, RawSchema, undefined) of - undefined -> - {undefined, [], AdditionalPropertiesCTX}; - RawPatternProperties -> - lists:foldl( - fun( - {Pattern, RawPatternSchema}, {PatternPropertiesAcc, ExtraSchemasAcc, CTXAcc} - ) -> - {PatternSchema, ExtraSchemas, NewCTX} = parse_schemas( - CTXAcc, RawPatternSchema - ), - { - PatternPropertiesAcc#{Pattern => PatternSchema}, - ExtraSchemasAcc ++ ExtraSchemas, - CTXAcc#ctx{resolved = NewCTX#ctx.resolved} - } - end, - {#{}, [], AdditionalPropertiesCTX}, - maps:to_list(RawPatternProperties) - ) - end, - Schema = - #{ - type => object, - properties => Properties, - required => Required, - minProperties => MinProperties, - maxProperties => MaxProperties, - additionalProperties => AdditionalProperties, - patternProperties => PatternProperties - }, - {Schema, - PropertiesExtraSchemas ++ AdditionalPropertiesExtraSchemas ++ PatternPropertiesExtraSchemas, - PatternPropertiesCTX}; -parse_schemas(CTX, #{<<"anyOf">> := RawAnyOf}) -> - {AnyOf, ExtraSchemas, NewCTX} = - lists:foldl( - fun(RawSchema, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, NewCTX} = parse_schemas(CTXAcc, RawSchema), - {[Schema | AnyOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#ctx{ - resolved = NewCTX#ctx.resolved - }} - end, - {[], [], CTX}, - RawAnyOf - ), - Schema = #{any_of => AnyOf}, - {Schema, ExtraSchemas, NewCTX}; -parse_schemas(CTX, #{<<"allOf">> := RawAllOf}) -> - {AllOf, ExtraSchemas, NewCTX} = - lists:foldl( - fun(RawSchema, {AllOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, NewCTX} = parse_schemas(CTXAcc, RawSchema), - {[Schema | AllOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#ctx{ - resolved = NewCTX#ctx.resolved - }} - end, - {[], [], CTX}, - RawAllOf - ), - Schema = #{all_of => AllOf}, - {Schema, ExtraSchemas, NewCTX}; -parse_schemas(CTX, #{<<"not">> := RawNot}) -> - {Not, ExtraSchemas, NewCTX} = parse_schemas(CTX, RawNot), - Schema = #{'not' => Not}, - {Schema, ExtraSchemas, NewCTX}; -parse_schemas(CTX, #{<<"oneOf">> := RawOneOf}) -> - {OneOf, ExtraSchemas, NewCTX} = - lists:foldl( - fun(RawSchema, {OneOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, NewCTX} = parse_schemas(CTXAcc, RawSchema), - {[Schema | OneOfAcc], ExtraSchemasAcc ++ ExtraSchemas, CTXAcc#ctx{ - resolved = NewCTX#ctx.resolved - }} - end, - {[], [], CTX}, - RawOneOf - ), - Schema = #{one_of => OneOf}, - {Schema, ExtraSchemas, NewCTX}. - --spec read_spec(SpecPath) -> Result when - SpecPath :: binary(), - Result :: {ok, BinSpec} | {error, Reason}, - BinSpec :: binary(), - Reason :: term(). -read_spec(SpecPath) -> - case file:read_file(SpecPath) of - {ok, BinSpec} -> - {ok, BinSpec}; - {error, Reason} -> - {error, {invalid_spec, Reason}} - end. - --spec resolve_ref(Ref, CTX) -> Result when - Ref :: binary(), - CTX :: #ctx{}, - Result :: {NewResolved, NewSchema, NewCTX}, - NewResolved :: binary(), - NewSchema :: ndto:schema(), - NewCTX :: #ctx{}. -resolve_ref(Ref, #ctx{base_path = BasePath, namespace = Namespace, resolved = Resolved, spec = Spec}) -> - [FilePath, ElementPath] = binary:split(Ref, <<"#">>, [global]), - [<<>> | LocalPath] = binary:split(ElementPath, <<"/">>, [global]), - {NewSpec, NewBasePath, NewNamespace} = - case FilePath of - <<>> -> - {Spec, BasePath, Namespace}; - _FilePath -> - AbsPath = filename:join(BasePath, FilePath), - case read_spec(AbsPath) of - {ok, Bin} -> - case deserialize_spec(Bin) of - {ok, RefSpec} -> - RefBasePath = filename:dirname(AbsPath), - RefNamespace = lists:last( - binary:split(FilePath, <<"/">>, [global]) - ), - {RefSpec, RefBasePath, RefNamespace}; - {error, Reason} -> - erlang:error({invalid_ref, Reason}) - end; - {error, Reason} -> - erlang:error({invalid_ref, Reason}) - end - end, - NewResolved = <>, - NewSchema = get(LocalPath, NewSpec), - NewCTX = #ctx{ - base_path = NewBasePath, - namespace = NewNamespace, - resolved = [NewResolved | Resolved], - spec = NewSpec - }, - {NewResolved, NewSchema, NewCTX}. diff --git a/test/ndto_SUITE.erl b/test/ndto_SUITE.erl index 48c1690..70a8f1b 100644 --- a/test/ndto_SUITE.erl +++ b/test/ndto_SUITE.erl @@ -381,20 +381,35 @@ base64(_Conf) -> ?assertEqual(true, test_base64:is_valid(String)). petstore(_Conf) -> - Schema = ndto_parser:parse( - ndto_parser_json_schema_draft_04, - test_oas_3_0, - erlang:list_to_binary(code:lib_dir(ndto, priv) ++ "/oas/3.0/specs/oas_3_0.json") + SpecPath = erlang:list_to_binary( + filename:join( + code:lib_dir(ndto, priv), + "oas/3.0/specs/oas_3_0.json" + ) + ), + {ok, [{PetstoreDTO, _Schema} | _Rest] = Schemas} = ndto_parser:parse( + ndto_parser_json_schema, + SpecPath + ), + lists:foreach( + fun({SchemaName, Schema}) -> + DTO = ndto:generate(SchemaName, Schema), + ok = ndto:load(DTO) + end, + Schemas ), - DTO = ndto:generate(test_oas_3_0, Schema), - ok = ndto:load(DTO), {ok, PetstoreBin} = file:read_file( - erlang:list_to_binary(code:lib_dir(ndto, priv) ++ "/oas/3.0/examples/petstore.json") + erlang:list_to_binary( + filename:join( + code:lib_dir(ndto, priv), + "oas/3.0/examples/petstore.json" + ) + ) ), {ok, Petstore} = njson:decode(PetstoreBin), ?assertEqual( true, - test_oas_3_0:is_valid(Petstore) + PetstoreDTO:is_valid(Petstore) ). diff --git a/test/ndto_parser_json_schema_draft_04_SUITE.erl b/test/ndto_parser_json_schema_SUITE.erl similarity index 89% rename from test/ndto_parser_json_schema_draft_04_SUITE.erl rename to test/ndto_parser_json_schema_SUITE.erl index 6a0fa29..727b429 100644 --- a/test/ndto_parser_json_schema_draft_04_SUITE.erl +++ b/test/ndto_parser_json_schema_SUITE.erl @@ -11,7 +11,7 @@ %% 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 --module(ndto_parser_json_schema_draft_04_SUITE). +-module(ndto_parser_json_schema_SUITE). %%% INCLUDE FILES -include_lib("stdlib/include/assert.hrl"). @@ -24,7 +24,7 @@ %%%----------------------------------------------------------------------------- all() -> [ - oas_3_0 + draft_04 ]. %%%----------------------------------------------------------------------------- @@ -58,9 +58,17 @@ end_per_testcase(Case, Conf) -> %%%----------------------------------------------------------------------------- %%% TEST CASES %%%----------------------------------------------------------------------------- -oas_3_0(_Conf) -> - SpecPath = filename:join(code:lib_dir(ndto, priv), "oas/3.0/specs/oas_3_0.json"), - {ok, Schemas} = ndto_parser:parse(ndto_parser_json_schema_draft_04, oas_3_0, SpecPath), +draft_04(_Conf) -> + SpecPath = erlang:list_to_binary( + filename:join( + code:lib_dir(ndto, priv), + "oas/3.0/specs/oas_3_0.json" + ) + ), + {ok, Schemas} = ndto_parser:parse( + ndto_parser_json_schema, + SpecPath + ), #{ type := object, properties := #{ From f27b00c88f5009cc889180b302ae32529bfe56ff Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Tue, 9 Jan 2024 15:54:35 +0100 Subject: [PATCH 6/6] build: 0.2.0 --- src/ndto.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ndto.app.src b/src/ndto.app.src index d2aafa5..1827ac6 100644 --- a/src/ndto.app.src +++ b/src/ndto.app.src @@ -1,6 +1,6 @@ {application, ndto, [ {description, "Erlang library for DTOs validation"}, - {vsn, "0.1.0"}, + {vsn, "0.2.0"}, {registered, []}, {applications, [kernel, stdlib, compiler, syntax_tools]}, {env, []}