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..303fa57 --- /dev/null +++ b/src/ndto_parser/ndto_parser_json_schema.erl @@ -0,0 +1,212 @@ +%%% 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} -> + 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 := #{