From 11b16b19a039088583f507eae5e6510e61cd887f Mon Sep 17 00:00:00 2001 From: Javier Garea Cidre Date: Wed, 13 Dec 2023 14:11:44 +0100 Subject: [PATCH] fix: handle oas 3.0 optionality --- .github/workflows/ci.yml | 2 +- README.md | 2 +- rebar.config | 12 +- rebar.lock | 4 +- src/erf.erl | 4 +- src/erf_oas_3_0.erl | 577 ------------- src/erf_parser.erl | 26 +- src/erf_parser/erf_parser_oas_3_0.erl | 813 ++++++++++++++++++ src/erf_router.erl | 323 ++++--- ...SUITE.erl => erf_parser_oas_3_0_SUITE.erl} | 140 ++- test/erf_router_SUITE.erl | 62 +- test/fixtures/common_oas_3_0_spec.json | 2 +- test/fixtures/with_refs_oas_3_0_spec.json | 2 +- 13 files changed, 1186 insertions(+), 783 deletions(-) delete mode 100644 src/erf_oas_3_0.erl create mode 100644 src/erf_parser/erf_parser_oas_3_0.erl rename test/{erf_oas_3_0_SUITE.erl => erf_parser_oas_3_0_SUITE.erl} (55%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec467bd..1c852e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: erf ci +name: CI on: push: diff --git a/README.md b/README.md index 0c517d3..b62abdd 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ A detailed description of each parameter can be found in the following list: - `callback`: Name of the callback module. - `port`: Port the server will listen to. Defaults to `8080`. - `name`: Name under which the server is registered. Defaults to `erf`. -- `spec_parser`: Name of the specification parser module. Defaults to `erf_oas_3_0`. +- `spec_parser`: Name of the specification parser module. Defaults to `erf_parser_oas_3_0`. - `preprocess_middlewares`: List of names of middlewares to be invoked before the request is forwarded to the callback. Defaults to `[]`. - `postprocess_middlewares`: List of names of middlewares to be invoked after the response is returned by the callback. Defaults to `[]`. - `ssl`: Boolean flag that enables/disables SSL. Defaults to `false`. diff --git a/rebar.config b/rebar.config index 4eb8ef8..7ee96ef 100644 --- a/rebar.config +++ b/rebar.config @@ -5,12 +5,12 @@ {deps, [ {elli, {git, "git@github.com:elli-lib/elli.git", {branch, "main"}}}, - {ndto, {git, "git@github.com:nomasystems/ndto.git", {branch, "main"}}}, + {ndto, {git, "git@github.com:nomasystems/ndto.git", {tag, "0.2.0"}}}, {njson, {git, "git@github.com:nomasystems/njson.git", {branch, "main"}}} ]}. {plugins, [ - {rebar3_ndto, {git, "git@github.com:nomasystems/rebar3_ndto.git", {branch, "main"}}} + {rebar3_ndto, {git, "git@github.com:nomasystems/rebar3_ndto.git", {tag, "0.2.0"}}} ]}. {ndto, [ {specs, [ @@ -27,6 +27,9 @@ {project_plugins, [ {erlfmt, {git, "git@github.com:WhatsApp/erlfmt.git", {branch, "main"}}}, + {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 ]}. @@ -42,6 +45,9 @@ {test, [ {erl_opts, [nowarn_export_all]}, {deps, [ + {eqwalizer_support, + {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, + "eqwalizer_support"}}, {meck, {git, "git@github.com:eproxus/meck.git", {branch, "master"}}}, {nct_util, {git, "git@github.com:nomasystems/nct_util.git", {branch, "main"}}} ]} @@ -91,5 +97,5 @@ {gradualizer_opts, [ %% TODO: address - {exclude, ["src/erf_oas_3_0.erl", "src/erf_router.erl"]} + {exclude, ["src/erf_parser_oas_3_0.erl", "src/erf_router.erl"]} ]}. diff --git a/rebar.lock b/rebar.lock index 5725a88..64835ad 100644 --- a/rebar.lock +++ b/rebar.lock @@ -8,9 +8,9 @@ 1}, {<<"ndto">>, {git,"git@github.com:nomasystems/ndto.git", - {ref,"491a2441e43afa2fb037c6e7e826c45a383e3bd9"}}, + {ref,"ecb52baafa44eba1d58661e4658e34b011aeb58c"}}, 0}, {<<"njson">>, {git,"git@github.com:nomasystems/njson.git", - {ref,"76ab40033ee977f876e7b3addca5de981ff4a9ef"}}, + {ref,"b230b3e6fb5e35320aeaa203762f3f12277c9970"}}, 0}]. diff --git a/src/erf.erl b/src/erf.erl index b6d0a95..88a8a37 100644 --- a/src/erf.erl +++ b/src/erf.erl @@ -41,7 +41,7 @@ %%% TYPES -type api() :: erf_parser:api(). --type body() :: njson:t(). +-type body() :: undefined | njson:t(). -type conf() :: #{ spec_path := binary(), callback := module(), @@ -207,7 +207,7 @@ reload_conf(Name, NewConf) -> init([Name, RawConf]) -> RawErfConf = #{ spec_path => maps:get(spec_path, RawConf), - spec_parser => maps:get(spec_parser, RawConf, erf_oas_3_0), + spec_parser => maps:get(spec_parser, RawConf, erf_parser_oas_3_0), callback => maps:get(callback, RawConf), static_routes => maps:get(static_routes, RawConf, []), swagger_ui => maps:get(swagger_ui, RawConf, false), diff --git a/src/erf_oas_3_0.erl b/src/erf_oas_3_0.erl deleted file mode 100644 index b80cd77..0000000 --- a/src/erf_oas_3_0.erl +++ /dev/null @@ -1,577 +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 OpenAPI Specification 3.0 erf_parser. --module(erf_oas_3_0). - -%%% BEHAVIOURS --behaviour(erf_parser). - -%%% EXTERNAL EXPORTS --export([ - parse/1 -]). - -%%% RECORDS --record(ctx, { - base_path :: binary(), - namespace :: binary(), - resolved :: [erf_parser:ref()], - spec :: oas() -}). - -%%% TYPES --type ctx() :: #ctx{}. --type oas() :: njson:t(). - -%%% MACROS --define(METHODS, [ - <<"get">>, - <<"put">>, - <<"post">>, - <<"delete">>, - <<"options">>, - <<"head">>, - <<"patch">>, - <<"trace">> -]). - -%%%----------------------------------------------------------------------------- -%%% EXTERNAL EXPORTS -%%%----------------------------------------------------------------------------- --spec parse(SpecPath) -> Result when - SpecPath :: binary(), - Result :: {ok, API} | {error, Reason}, - API :: erf:api(), - Reason :: term(). -%% @doc Parses an OpenAPI Specification 3.0 file into an API AST. -parse(SpecPath) -> - case read_spec(SpecPath) of - {ok, BinSpec} -> - case deserialize_spec(BinSpec) of - {ok, OAS} -> - case oas_3_0:is_valid(OAS) of - true -> - CTX = #ctx{ - base_path = SpecPath, - namespace = filename:rootname(filename:basename(SpecPath)), - resolved = [], - spec = OAS - }, - {ok, parse_api(OAS, CTX)}; - false -> - {error, {invalid_spec, <<"Invalid OpenAPI Specification 3.0">>}} - end; - {error, Reason} -> - {error, {invalid_spec, Reason}} - end; - {error, Reason} -> - {error, {invalid_spec, Reason}} - end. - -%%%----------------------------------------------------------------------------- -%%% INTERNAL FUNCTIONS -%%%----------------------------------------------------------------------------- --spec deserialize_spec(Bin) -> Result when - Bin :: binary(), - Result :: {ok, map()} | {error, Reason}, - Reason :: term(). -deserialize_spec(Bin) -> - try - Data = njson:decode(Bin), - {ok, Data} - catch - _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_api(OAS, CTX) -> Result when - OAS :: oas(), - CTX :: ctx(), - Result :: erf:api(). -parse_api(OAS, CTX) -> - Name = parse_name(OAS), - Version = parse_version(OAS), - {RawEndpoints, RawSchemas, _NewCTX} = lists:foldl( - fun({Path, RawEndpoint}, {EndpointsAcc, SchemasAcc, CTXAcc}) -> - {Endpoint, EndpointSchemas, NewCTX} = parse_endpoint(Path, RawEndpoint, CTXAcc), - {[Endpoint | EndpointsAcc], SchemasAcc ++ EndpointSchemas, NewCTX} - end, - {[], [], CTX}, - maps:to_list(maps:get(<<"paths">>, OAS)) - ), - Endpoints = lists:reverse(RawEndpoints), - Schemas = maps:from_list(RawSchemas), - #{ - name => Name, - version => Version, - endpoints => Endpoints, - schemas => Schemas - }. - --spec parse_endpoint(Path, RawEndpoint, CTX) -> Result when - Path :: binary(), - RawEndpoint :: oas(), - CTX :: ctx(), - Result :: {Endpoint, Schemas, NewCTX}, - Endpoint :: erf_parser:endpoint(), - Schemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_endpoint(Path, RawEndpoint, #ctx{namespace = Namespace} = CTX) -> - EndpointNamespace = erf_util:to_snake_case(<>), - {Parameters, ParametersSchemas, ParametersCTX} = - lists:foldl( - fun(RawParameter, {ParametersAcc, ParametersExtraSchemasAcc, ParametersCTXAcc}) -> - {Parameter, ParameterExtraSchemas, ParameterCTX} = parse_parameter( - RawParameter, ParametersCTXAcc - ), - { - [Parameter | ParametersAcc], - ParameterExtraSchemas ++ ParametersExtraSchemasAcc, - ParameterCTX - } - end, - {[], [], CTX#ctx{namespace = EndpointNamespace}}, - maps:get(<<"parameters">>, RawEndpoint, []) - ), - - RawOperations = lists:reverse( - lists:filtermap( - fun(Method) -> - case maps:get(Method, RawEndpoint, undefined) of - undefined -> - false; - Operation -> - {true, {Method, Operation}} - end - end, - ?METHODS - ) - ), - {Operations, OperationsSchemas, OperationsCTX} = - lists:foldl( - fun( - {Method, RawOperation}, {OperationsAcc, OperationsExtraSchemasAcc, OperationsCTXAcc} - ) -> - {Operation, Schemas, OperationCTX} = parse_operation( - Path, Method, RawOperation, OperationsCTXAcc - ), - {[Operation | OperationsAcc], Schemas ++ OperationsExtraSchemasAcc, OperationCTX} - end, - {[], [], ParametersCTX#ctx{namespace = Namespace}}, - RawOperations - ), - - Endpoint = #{ - path => Path, - parameters => Parameters, - operations => Operations - }, - Schemas = ParametersSchemas ++ OperationsSchemas, - {Endpoint, Schemas, OperationsCTX#ctx{namespace = Namespace}}. - --spec parse_method(Method) -> Result when - Method :: binary(), - Result :: erf_parser:method(). -parse_method(<<"get">>) -> - get; -parse_method(<<"post">>) -> - post; -parse_method(<<"put">>) -> - put; -parse_method(<<"delete">>) -> - delete; -parse_method(<<"patch">>) -> - patch; -parse_method(<<"head">>) -> - head; -parse_method(<<"options">>) -> - options; -parse_method(<<"trace">>) -> - trace; -parse_method(<<"connect">>) -> - connect. - --spec parse_name(Val) -> Result when - Val :: oas(), - Result :: binary(). -parse_name(#{<<"info">> := #{<<"title">> := Name}}) -> - Name. - --spec parse_operation(Path, Method, RawOperation, CTX) -> Result when - Path :: binary(), - Method :: binary(), - RawOperation :: oas(), - CTX :: ctx(), - Result :: {Operation, Schemas, NewCTX}, - Operation :: erf_parser:operation(), - Schemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_operation( - Path, - RawMethod, - #{<<"responses">> := RawResponses} = RawOperation, - #ctx{namespace = Namespace} = CTX -) -> - OperationId = - case maps:get(<<"operationId">>, RawOperation, undefined) of - undefined -> - erf_util:to_snake_case(<>); - RawOperationId -> - erf_util:to_snake_case(RawOperationId) - end, - NewCTX = CTX#ctx{namespace = <>}, - Method = parse_method(RawMethod), - - {Parameters, ParametersSchemas, ParametersCTX} = - lists:foldl( - fun(RawParameter, {ParametersAcc, ParametersExtraSchemasAcc, ParametersCTXAcc}) -> - {Parameter, ParameterExtraSchemas, ParameterCTX} = parse_parameter( - RawParameter, ParametersCTXAcc - ), - { - [Parameter | ParametersAcc], - ParameterExtraSchemas ++ ParametersExtraSchemasAcc, - ParameterCTX - } - end, - {[], [], NewCTX}, - maps:get(<<"parameters">>, RawOperation, []) - ), - - RequestBodyRef = erf_util:to_snake_case(<<(NewCTX#ctx.namespace)/binary, "_request_body">>), - RawRequestBody = maps:get(<<"requestBody">>, RawOperation, undefined), - {RequestBodySchema, RequestBodyExtraSchemas, RequestBodyCTX} = parse_request_body( - RawRequestBody, ParametersCTX - ), - RequestBodySchemas = [{RequestBodyRef, RequestBodySchema} | RequestBodyExtraSchemas], - - {Responses, ResponsesSchemas, ResponsesCTX} = - lists:foldl( - fun( - {RawStatusCode, RawResponse}, - {ResponsesAcc, ResponsesExtraSchemasAcc, ResponsesCTXAcc} - ) -> - StatusCode = - case RawStatusCode of - <<"default">> -> - '*'; - _RawStatusCode -> - erlang:binary_to_integer(RawStatusCode) - end, - {ResponseBody, ResponseExtraSchemas, ResponseCTX} = parse_response_body( - RawResponse, ResponsesCTXAcc - ), - RawRef = - case StatusCode of - '*' -> - <<"default">>; - _StatusCode -> - erlang:integer_to_binary(StatusCode) - end, - Ref = erf_util:to_snake_case(<< - (NewCTX#ctx.namespace)/binary, "_response_body_", RawRef/binary - >>), - { - ResponsesAcc#{StatusCode => Ref}, - [{Ref, ResponseBody} | ResponseExtraSchemas] ++ ResponsesExtraSchemasAcc, - ResponseCTX - } - end, - {#{}, [], RequestBodyCTX}, - maps:to_list(RawResponses) - ), - - Operation = #{ - id => OperationId, - method => Method, - parameters => Parameters, - request_body => RequestBodyRef, - responses => Responses - }, - Schemas = ParametersSchemas ++ RequestBodySchemas ++ ResponsesSchemas, - {Operation, Schemas, ResponsesCTX}. - --spec parse_parameter(OAS, CTX) -> Result when - OAS :: oas(), - CTX :: ctx(), - Result :: {Parameter, ExtraSchemas, NewCTX}, - Parameter :: erf_parser:parameter(), - ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_parameter(#{<<"$ref">> := Ref}, CTX) -> - {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - parse_parameter(RefOAS, RefCTX); -parse_parameter(#{<<"content">> := Content} = RawParameter, #ctx{namespace = Namespace} = CTX) -> - ParameterType = - case maps:get(<<"in">>, RawParameter) of - <<"query">> -> - query; - <<"header">> -> - header; - <<"path">> -> - path; - <<"cookie">> -> - cookie - end, - DefaultRequired = - case ParameterType of - path -> - true; - _Type -> - false - end, - Required = maps:get(<<"required">>, RawParameter, DefaultRequired), - ParameterName = maps:get(<<"name">>, RawParameter), - ParameterRef = erf_util:to_snake_case(<>), - Parameter = #{ - ref => ParameterRef, - name => ParameterName, - type => ParameterType - }, - {AnyOf, ExtraSchemas, NewCTX} = - lists:foldl( - fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - { - [Schema#{<<"nullable">> => not Required} | AnyOfAcc], - ExtraSchemas ++ ExtraSchemasAcc, - SchemaCTX - } - end, - {[], [], CTX}, - maps:to_list(Content) - ), - ParameterSchema = #{<<"anyOf">> => AnyOf}, - {Parameter, [{ParameterRef, ParameterSchema} | ExtraSchemas], NewCTX}; -parse_parameter(#{<<"schema">> := RawSchema} = RawParameter, #ctx{namespace = Namespace} = CTX) -> - ParameterType = - case maps:get(<<"in">>, RawParameter) of - <<"query">> -> - query; - <<"header">> -> - header; - <<"path">> -> - path; - <<"cookie">> -> - cookie - end, - DefaultRequired = - case ParameterType of - path -> - true; - _Type -> - false - end, - Required = maps:get(<<"required">>, RawParameter, DefaultRequired), - ParameterName = maps:get(<<"name">>, RawParameter), - ParameterRef = erf_util:to_snake_case(<>), - Parameter = #{ - ref => ParameterRef, - name => ParameterName, - type => ParameterType - }, - {RawParameterSchema, NewExtraSchemas, NewCTX} = parse_schema(RawSchema, CTX), - ParameterSchema = maps:put(<<"nullable">>, not Required, RawParameterSchema), - {Parameter, [{ParameterRef, ParameterSchema} | NewExtraSchemas], NewCTX}. - --spec parse_request_body(OAS, CTX) -> Result when - OAS :: oas(), - CTX :: ctx(), - Result :: {RequestBody, ExtraSchemas, NewCTX}, - RequestBody :: erf_parser:schema(), - ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_request_body(#{<<"$ref">> := Ref}, CTX) -> - {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - {NewSchema, NewExtraSchemas, NewCTX} = parse_request_body(RefOAS, RefCTX), - {#{<<"$ref">> => RefResolved}, [{RefResolved, NewSchema} | NewExtraSchemas], NewCTX}; -parse_request_body(#{<<"content">> := Content} = ReqBody, CTX) -> - Required = maps:get(<<"required">>, ReqBody, false), - {AnyOf, NewExtraSchemas, NewCTX} = lists:foldl( - fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - { - [Schema#{<<"nullable">> => not Required} | AnyOfAcc], - ExtraSchemas ++ ExtraSchemasAcc, - SchemaCTX - } - end, - {[], [], CTX}, - maps:to_list(Content) - ), - {#{<<"anyOf">> => AnyOf}, NewExtraSchemas, NewCTX}; -parse_request_body(_ReqBody, CTX) -> - {undefined, [], CTX}. - --spec parse_response_body(OAS, CTX) -> Result when - OAS :: oas(), - CTX :: ctx(), - Result :: {ResponseBody, ExtraSchemas, NewCTX}, - ResponseBody :: erf_parser:schema(), - ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_response_body(#{<<"$ref">> := Ref}, CTX) -> - {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - {NewSchema, NewExtraSchemas, NewCTX} = parse_response_body(RefOAS, RefCTX), - {#{<<"$ref">> => RefResolved}, [{RefResolved, NewSchema} | NewExtraSchemas], NewCTX}; -parse_response_body(#{<<"content">> := Content}, CTX) -> - {AnyOf, NewExtraSchemas, NewCTX} = lists:foldl( - fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - {[Schema | AnyOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} - end, - {[], [], CTX}, - maps:to_list(Content) - ), - {#{<<"anyOf">> => AnyOf}, NewExtraSchemas, NewCTX}; -parse_response_body(_Response, CTX) -> - {undefined, [], CTX}. - --spec parse_schema(OAS, CTX) -> Result when - OAS :: oas(), - CTX :: ctx(), - Result :: {Schema, ExtraSchemas, NewCTX}, - Schema :: erf_parser:schema(), - ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], - NewCTX :: ctx(). -parse_schema(#{<<"$ref">> := Ref}, CTX) -> - {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - {NewSchema, NewExtraSchemas, NewCTX} = parse_schema(RefOAS, RefCTX), - {#{<<"$ref">> => RefResolved}, [{RefResolved, NewSchema} | NewExtraSchemas], NewCTX}; -parse_schema(#{<<"items">> := RawItems} = Schema, CTX) -> - {Items, NewExtraSchemas, NewCTX} = parse_schema(RawItems, CTX), - {Schema#{<<"items">> => Items}, NewExtraSchemas, NewCTX}; -parse_schema(#{<<"properties">> := RawProperties} = Schema, CTX) -> - {Properties, PropertiesExtraSchemas, PropertiesCTX} = lists:foldl( - fun({Name, RawProperty}, {PropertiesAcc, ExtraSchemasAcc, CTXAcc}) -> - {Property, ExtraSchemas, PropertyCTX} = parse_schema(RawProperty, CTXAcc), - {PropertiesAcc#{Name => Property}, ExtraSchemas ++ ExtraSchemasAcc, PropertyCTX} - end, - {#{}, [], CTX}, - maps:to_list(RawProperties) - ), - PropertiesSchema = Schema#{<<"properties">> => Properties}, - RawAdditionalProperties = maps:get(<<"additionalProperties">>, Schema, undefined), - {AdditionalProperties, AdditionalPropertiesExtraSchemas, AdditionalPropertiesCTX} = parse_schema( - RawAdditionalProperties, PropertiesCTX - ), - AdditionalPropertiesSchema = PropertiesSchema#{ - <<"additionalProperties">> => AdditionalProperties - }, - {AdditionalPropertiesSchema, PropertiesExtraSchemas ++ AdditionalPropertiesExtraSchemas, - AdditionalPropertiesCTX}; -parse_schema(#{<<"allOf">> := RawAllOf}, CTX) -> - {AllOf, NewExtraSchemas, NewCTX} = lists:foldl( - fun(RawSchema, {AllOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - {[Schema | AllOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} - end, - {[], [], CTX}, - RawAllOf - ), - {#{<<"allOf">> => AllOf}, NewExtraSchemas, NewCTX}; -parse_schema(#{<<"oneOf">> := RawOneOf}, CTX) -> - {OneOf, NewExtraSchemas, NewCTX} = lists:foldl( - fun(RawSchema, {OneOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - {[Schema | OneOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} - end, - {[], [], CTX}, - RawOneOf - ), - {#{<<"oneOf">> => OneOf}, NewExtraSchemas, NewCTX}; -parse_schema(#{<<"anyOf">> := RawAnyOf}, CTX) -> - {AnyOf, NewExtraSchemas, NewCTX} = lists:foldl( - fun(RawSchema, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - {[Schema | AnyOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} - end, - {[], [], CTX}, - RawAnyOf - ), - {#{<<"anyOf">> => AnyOf}, NewExtraSchemas, NewCTX}; -parse_schema(#{<<"not">> := RawNot}, CTX) -> - {Not, NewExtraSchemas, NewCTX} = parse_schema(RawNot, CTX), - {#{<<"not">> => Not}, NewExtraSchemas, NewCTX}; -parse_schema(Schema, CTX) -> - {Schema, [], CTX}. - --spec parse_version(OAS) -> Version when - OAS :: oas(), - Version :: binary(). -parse_version(#{<<"info">> := #{<<"version">> := Version}}) -> - Version. - --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, NewOAS, NewCTX}, - NewResolved :: binary(), - NewOAS :: oas(), - NewCTX :: ctx(). -resolve_ref(Ref, #ctx{base_path = BasePath, resolved = Resolved, spec = Spec}) -> - [FilePath, ElementPath] = binary:split(Ref, <<"#">>, [global]), - [<<>> | LocalPath] = binary:split(ElementPath, <<"/">>, [global]), - {NewSpec, NewBasePath, NewNamespace} = - case FilePath of - <<>> -> - ResetNamespace = filename:rootname(filename:basename(BasePath)), - {Spec, BasePath, ResetNamespace}; - _FilePath -> - RefBasePath = filename:join(filename:dirname(BasePath), FilePath), - case read_spec(RefBasePath) of - {ok, Bin} -> - case deserialize_spec(Bin) of - {ok, RefSpec} -> - RefNamespace = filename:rootname(filename:basename(FilePath)), - {RefSpec, RefBasePath, RefNamespace}; - {error, Reason} -> - erlang:error({invalid_ref, Reason}) - end; - {error, Reason} -> - erlang:error({invalid_ref, Reason}) - end - end, - NewResolved = <>, - NewOAS = get(LocalPath, NewSpec), - NewCTX = #ctx{ - base_path = NewBasePath, - namespace = NewNamespace, - resolved = [NewResolved | Resolved], - spec = NewSpec - }, - {NewResolved, NewOAS, NewCTX}. diff --git a/src/erf_parser.erl b/src/erf_parser.erl index ac1d4dd..95ca878 100644 --- a/src/erf_parser.erl +++ b/src/erf_parser.erl @@ -27,6 +27,10 @@ endpoints := [endpoint()], schemas := #{ref() => schema()} }. +-type body() :: #{ + ref := ref(), + required := boolean() +}. -type endpoint() :: #{ path := path(), parameters := [parameter()], @@ -37,32 +41,46 @@ id := binary(), method := method(), parameters := [parameter()], - request_body := ref(), + request := request(), responses := #{ - '*' | status_code() := ref() + '*' | status_code() := response() } }. -type parameter() :: #{ ref := ref(), name := parameter_name(), - type := parameter_type() + type := parameter_type(), + required := boolean() }. -type parameter_name() :: binary(). -type parameter_type() :: header | cookie | path | query. -type path() :: binary(). -type ref() :: binary(). +-type request() :: #{ + body := body() +}. +-type response() :: #{ + body := body() +}. -type schema() :: ndto:schema(). -type status_code() :: 100..599. %%% TYPE EXPORTS -export_type([ api/0, + body/0, endpoint/0, method/0, operation/0, parameter/0, + parameter_name/0, + parameter_type/0, + path/0, ref/0, - schema/0 + request/0, + response/0, + schema/0, + status_code/0 ]). %%%----------------------------------------------------------------------------- diff --git a/src/erf_parser/erf_parser_oas_3_0.erl b/src/erf_parser/erf_parser_oas_3_0.erl new file mode 100644 index 0000000..20bb300 --- /dev/null +++ b/src/erf_parser/erf_parser_oas_3_0.erl @@ -0,0 +1,813 @@ +%%% 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 OpenAPI Specification 3.0 erf_parser. +-module(erf_parser_oas_3_0). + +%%% BEHAVIOURS +-behaviour(erf_parser). + +%%% EXTERNAL EXPORTS +-export([ + parse/1 +]). + +%%% TYPES +-type ctx() :: #{ + base_path := binary(), + base_name := binary(), + namespace := binary(), + resolved := [binary()], + spec := spec() +}. +-type spec() :: njson:t(). + +%%% MACROS +-define(METHODS, [ + <<"get">>, + <<"put">>, + <<"post">>, + <<"delete">>, + <<"options">>, + <<"head">>, + <<"patch">>, + <<"trace">> +]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +-spec parse(SpecPath) -> Result when + SpecPath :: binary(), + Result :: {ok, API} | {error, Reason}, + API :: erf:api(), + Reason :: term(). +%% @doc Parses an OpenAPI Specification 3.0 file into an API AST. +parse(SpecPath) -> + case parse_spec(SpecPath) of + {ok, OAS} -> + case oas_3_0:is_valid(OAS) of + true -> + BasePath = filename:dirname(SpecPath), + BaseName = filename:rootname(filename:basename(SpecPath)), + CTX = #{ + base_path => BasePath, + base_name => BaseName, + namespace => BaseName, + resolved => [], + spec => OAS + }, + API = parse_api(OAS, CTX), + {ok, ndto_parser_json_schema:clean_optionals(API)}; + false -> + {error, {invalid_spec, <<"Invalid OpenAPI Specification 3.0">>}} + end; + {error, Reason} -> + {error, {invalid_spec, Reason}} + end. + +%%%----------------------------------------------------------------------------- +%%% INTERNAL FUNCTIONS +%%%----------------------------------------------------------------------------- +-spec parse_api(OAS, CTX) -> Result when + OAS :: spec(), + CTX :: ctx(), + Result :: erf:api(). +parse_api(OAS, CTX) -> + Name = parse_name(OAS), + Version = parse_version(OAS), + {RawEndpoints, RawSchemas, _NewCTX} = lists:foldl( + fun({Path, RawEndpoint}, {EndpointsAcc, SchemasAcc, CTXAcc}) -> + {Endpoint, EndpointSchemas, NewCTX} = parse_endpoint(Path, RawEndpoint, CTXAcc), + { + [Endpoint | EndpointsAcc], + SchemasAcc ++ EndpointSchemas, + CTXAcc#{resolved => maps:get(resolved, NewCTX)} + } + end, + {[], [], CTX}, + maps:to_list(maps:get(<<"paths">>, OAS)) + ), + Endpoints = lists:reverse(RawEndpoints), + Schemas = maps:from_list(RawSchemas), + #{ + name => Name, + version => Version, + endpoints => Endpoints, + schemas => Schemas + }. + +-spec parse_endpoint(Path, RawEndpoint, CTX) -> Result when + Path :: binary(), + RawEndpoint :: spec(), + CTX :: ctx(), + Result :: {Endpoint, Schemas, NewCTX}, + Endpoint :: erf_parser:endpoint(), + Schemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_endpoint(Path, RawEndpoint, CTX) -> + {Parameters, ParametersSchemas, ParametersCTX} = + lists:foldl( + fun(RawParameter, {ParametersAcc, ParametersExtraSchemasAcc, ParametersCTXAcc}) -> + {Parameter, ParameterExtraSchemas, ParameterCTX} = parse_parameter( + RawParameter, ParametersCTXAcc + ), + { + [Parameter | ParametersAcc], + ParameterExtraSchemas ++ ParametersExtraSchemasAcc, + ParametersCTXAcc#{resolved => maps:get(resolved, ParameterCTX)} + } + end, + {[], [], CTX}, + maps:get(<<"parameters">>, RawEndpoint, []) + ), + + RawOperations = lists:reverse( + lists:filtermap( + fun(Method) -> + case maps:get(Method, RawEndpoint, undefined) of + undefined -> + false; + Operation -> + {true, {Method, Operation}} + end + end, + ?METHODS + ) + ), + {Operations, OperationsSchemas, OperationsCTX} = + lists:foldl( + fun( + {Method, RawOperation}, {OperationsAcc, OperationsExtraSchemasAcc, OperationsCTXAcc} + ) -> + {Operation, Schemas, OperationCTX} = parse_operation( + Path, Method, RawOperation, OperationsCTXAcc + ), + { + [Operation | OperationsAcc], + Schemas ++ OperationsExtraSchemasAcc, + OperationsCTXAcc#{resolved => maps:get(resolved, OperationCTX)} + } + end, + {[], [], ParametersCTX}, + RawOperations + ), + + Endpoint = #{ + path => Path, + parameters => Parameters, + operations => Operations + }, + Schemas = ParametersSchemas ++ OperationsSchemas, + {Endpoint, Schemas, OperationsCTX}. + +-spec parse_method(Method) -> Result when + Method :: binary(), + Result :: erf_parser:method(). +parse_method(<<"get">>) -> + get; +parse_method(<<"post">>) -> + post; +parse_method(<<"put">>) -> + put; +parse_method(<<"delete">>) -> + delete; +parse_method(<<"patch">>) -> + patch; +parse_method(<<"head">>) -> + head; +parse_method(<<"options">>) -> + options; +parse_method(<<"trace">>) -> + trace; +parse_method(<<"connect">>) -> + connect. + +-spec parse_name(Val) -> Result when + Val :: spec(), + Result :: binary(). +parse_name(#{<<"info">> := #{<<"title">> := Name}}) -> + Name. + +-spec parse_operation(Path, Method, RawOperation, CTX) -> Result when + Path :: binary(), + Method :: binary(), + RawOperation :: spec(), + CTX :: ctx(), + Result :: {Operation, Schemas, NewCTX}, + Operation :: erf_parser:operation(), + Schemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_operation( + Path, + RawMethod, + #{<<"responses">> := RawResponses} = RawOperation, + #{namespace := Namespace} = CTX +) -> + OperationId = + case maps:get(<<"operationId">>, RawOperation, undefined) of + undefined -> + erf_util:to_snake_case(<>); + RawOperationId -> + erf_util:to_snake_case(RawOperationId) + end, + NewCTX = CTX#{namespace => <>}, + Method = parse_method(RawMethod), + + {Parameters, ParametersSchemas, ParametersCTX} = + lists:foldl( + fun(RawParameter, {ParametersAcc, ParametersExtraSchemasAcc, ParametersCTXAcc}) -> + {Parameter, ParameterExtraSchemas, ParameterCTX} = parse_parameter( + RawParameter, ParametersCTXAcc + ), + { + [Parameter | ParametersAcc], + ParameterExtraSchemas ++ ParametersExtraSchemasAcc, + ParametersCTXAcc#{resolved => maps:get(resolved, ParameterCTX)} + } + end, + {[], [], NewCTX}, + maps:get(<<"parameters">>, RawOperation, []) + ), + + RawRequestBody = maps:get(<<"requestBody">>, RawOperation, undefined), + {ParsedRequestBody, RawRequestBodySchemas, RequestBodyCTX} = parse_request_body( + RawRequestBody, ParametersCTX + ), + RequestBodyRef = + erf_util:to_snake_case(<< + (maps:get(namespace, NewCTX))/binary, "_request_body" + >>), + RequestBodyRequired = maps:get(required, ParsedRequestBody), + RequestBodySchema = maps:get(schema, ParsedRequestBody), + RequestBodySchemas = [{RequestBodyRef, RequestBodySchema} | RawRequestBodySchemas], + RequestBody = #{ + ref => RequestBodyRef, + required => RequestBodyRequired + }, + Request = #{ + body => RequestBody + }, + + {Responses, ResponsesSchemas, ResponsesCTX} = + lists:foldl( + fun( + {RawStatusCode, RawResponse}, + {ResponsesAcc, ResponsesExtraSchemasAcc, ResponsesCTXAcc} + ) -> + StatusCode = + case RawStatusCode of + <<"default">> -> + '*'; + _RawStatusCode -> + erlang:binary_to_integer(RawStatusCode) + end, + {ParsedResponse, RawResponseExtraSchemas, ResponseCTX} = parse_response( + RawResponse, ResponsesCTXAcc + ), + ResponseRef = + erf_util:to_snake_case(<< + (maps:get(namespace, NewCTX))/binary, + "_response_body_", + RawStatusCode/binary + >>), + ResponseRequired = maps:get(required, ParsedResponse), + ResponseSchema = maps:get(schema, ParsedResponse), + ResponseExtraSchemas = [{ResponseRef, ResponseSchema} | RawResponseExtraSchemas], + ResponseBody = #{ + ref => ResponseRef, + required => ResponseRequired + }, + Response = #{ + body => ResponseBody + }, + { + ResponsesAcc#{StatusCode => Response}, + ResponseExtraSchemas ++ ResponsesExtraSchemasAcc, + ResponsesCTXAcc#{resolved => maps:get(resolved, ResponseCTX)} + } + end, + {#{}, [], RequestBodyCTX}, + maps:to_list(RawResponses) + ), + + Operation = #{ + id => OperationId, + method => Method, + parameters => Parameters, + request => Request, + responses => Responses + }, + Schemas = ParametersSchemas ++ RequestBodySchemas ++ ResponsesSchemas, + {Operation, Schemas, ResponsesCTX}. + +-spec parse_parameter(OAS, CTX) -> Result when + OAS :: spec(), + CTX :: ctx(), + Result :: {Parameter, ExtraSchemas, NewCTX}, + Parameter :: erf_parser:parameter(), + ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_parameter(#{<<"$ref">> := Ref}, CTX) -> + {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), + parse_parameter(RefOAS, RefCTX); +parse_parameter(#{<<"content">> := Content} = RawParameter, #{namespace := Namespace} = CTX) -> + ParameterType = + case maps:get(<<"in">>, RawParameter) of + <<"query">> -> + query; + <<"header">> -> + header; + <<"path">> -> + path; + <<"cookie">> -> + cookie + end, + DefaultRequired = + case ParameterType of + path -> + true; + _Type -> + false + end, + Required = maps:get(<<"required">>, RawParameter, DefaultRequired), + ParameterName = maps:get(<<"name">>, RawParameter), + ParameterRef = erf_util:to_snake_case(<>), + Parameter = #{ + ref => ParameterRef, + name => ParameterName, + type => ParameterType, + required => Required + }, + {AnyOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, NewExtraSchemas, NewCTX} = parse_schemas(RawSchema, CTXAcc), + { + [Schema | AnyOfAcc], + NewExtraSchemas ++ ExtraSchemasAcc, + CTXAcc#{resolved => maps:get(resolved, NewCTX)} + } + end, + {[], [], CTX}, + maps:to_list(Content) + ), + ParameterSchema = #{any_of => AnyOf}, + {Parameter, [{ParameterRef, ParameterSchema} | ExtraSchemas], NewCTX}; +parse_parameter(#{<<"schema">> := RawSchema} = RawParameter, #{namespace := Namespace} = CTX) -> + ParameterType = + case maps:get(<<"in">>, RawParameter) of + <<"query">> -> + query; + <<"header">> -> + header; + <<"path">> -> + path; + <<"cookie">> -> + cookie + end, + DefaultRequired = + case ParameterType of + path -> + true; + _Type -> + false + end, + Required = maps:get(<<"required">>, RawParameter, DefaultRequired), + ParameterName = maps:get(<<"name">>, RawParameter), + ParameterRef = erf_util:to_snake_case(<>), + Parameter = #{ + ref => ParameterRef, + name => ParameterName, + type => ParameterType, + required => Required + }, + {ParameterSchema, NewExtraSchemas, NewCTX} = parse_schemas(RawSchema, CTX), + {Parameter, [{ParameterRef, ParameterSchema} | NewExtraSchemas], NewCTX}. + +-spec parse_request_body(OAS, CTX) -> Result when + OAS :: spec(), + CTX :: ctx(), + Result :: {RequestBody, ExtraSchemas, NewCTX}, + RequestBody :: #{schema := erf_parser:schema(), required := boolean()}, + ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_request_body(#{<<"$ref">> := Ref}, CTX) -> + {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), + parse_request_body(RefOAS, RefCTX); +parse_request_body(#{<<"content">> := Content} = ReqBody, CTX) -> + Required = maps:get(<<"required">>, ReqBody, false), + {AnyOf, ExtraSchemas, NewCTX} = lists:foldl( + fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, NewExtraSchemas, SchemaCTX} = parse_schemas(RawSchema, CTXAcc), + { + [Schema | AnyOfAcc], + NewExtraSchemas ++ ExtraSchemasAcc, + CTXAcc#{ + resolved => maps:get(resolved, SchemaCTX) + } + } + end, + {[], [], CTX}, + maps:to_list(Content) + ), + RequestBodySchema = #{any_of => AnyOf}, + RequestBody = #{ + schema => RequestBodySchema, + required => Required + }, + {RequestBody, ExtraSchemas, NewCTX}; +parse_request_body(_ReqBody, CTX) -> + RequestBody = #{ + schema => true, + required => false + }, + {RequestBody, [], CTX}. + +-spec parse_response(OAS, CTX) -> Result when + OAS :: spec(), + CTX :: ctx(), + Result :: {Response, ExtraSchemas, NewCTX}, + Response :: #{schema := erf_parser:schema(), required := boolean()}, + ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_response(#{<<"$ref">> := Ref}, CTX) -> + {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), + parse_response(RefOAS, RefCTX); +parse_response(#{<<"content">> := Content}, CTX) -> + {AnyOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Schema, ExtraSchemas, SchemaCTX} = parse_schemas(RawSchema, CTXAcc), + { + [Schema | AnyOfAcc], + ExtraSchemas ++ ExtraSchemasAcc, + CTXAcc#{ + resolved => maps:get(resolved, SchemaCTX) + } + } + end, + {[], [], CTX}, + maps:to_list(Content) + ), + ResponseSchema = #{any_of => AnyOf}, + Response = #{ + schema => ResponseSchema, + required => false + }, + {Response, ExtraSchemas, NewCTX}; +parse_response(_Response, CTX) -> + Response = #{ + schema => true, + required => false + }, + {Response, [], CTX}. + +-spec parse_spec(SpecPath) -> Result when + SpecPath :: binary(), + Result :: {ok, Spec} | {error, Reason}, + Spec :: 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, undefined} -> + {error, {invalid_spec, BinSpec}}; + {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 parse_version(OAS) -> Version when + OAS :: spec(), + Version :: binary(). +parse_version(#{<<"info">> := #{<<"version">> := Version}}) -> + Version. + +-spec parse_schemas(Spec, CTX) -> Result when + Spec :: spec(), + CTX :: ctx(), + Result :: {Schema, ExtraSchemas, NewCTX}, + Schema :: erf_parser:schema(), + ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], + NewCTX :: ctx(). +parse_schemas(false, CTX) -> + Schema = false, + {Schema, [], CTX}; +parse_schemas(true, CTX) -> + Schema = #{}, + {Schema, [], CTX}; +parse_schemas(#{<<"$ref">> := Ref} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {RefName, RefSchema, RefCTX} = resolve_ref(Ref, CTX), + Schema = #{ + ref => RefName, + nullable => Nullable + }, + case lists:member(RefName, maps:get(resolved, CTX)) of + true -> + {Schema, [], CTX}; + false -> + {NewSchema, NewExtraSchemas, NewCTX} = parse_schemas(RefSchema, RefCTX), + {Schema, [{RefName, NewSchema} | NewExtraSchemas], NewCTX} + end; +parse_schemas(#{<<"enum">> := Enum} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + Schema = #{ + enum => Enum, + nullable => Nullable + }, + {Schema, [], CTX}; +parse_schemas(#{<<"type">> := <<"boolean">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + Schema = #{ + type => boolean, + nullable => Nullable + }, + {Schema, [], CTX}; +parse_schemas(#{<<"type">> := <<"integer">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + 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, + nullable => Nullable + }, + {Schema, [], CTX}; +parse_schemas(#{<<"type">> := <<"number">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + 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 + } + ], + nullable => Nullable + }, + {Schema, [], CTX}; +parse_schemas(#{<<"type">> := <<"string">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + 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, + nullable => Nullable + }, + {Schema, [], CTX}; +parse_schemas(#{<<"type">> := <<"array">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {Items, ItemsExtraSchemas, ItemsCTX} = + case maps:get(<<"items">>, RawSchema, undefined) of + undefined -> + {undefined, [], CTX}; + RawItems -> + parse_schemas(RawItems, CTX) + 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, + nullable => Nullable + }, + {Schema, ItemsExtraSchemas, ItemsCTX}; +parse_schemas(#{<<"type">> := <<"object">>} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {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( + 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_schemas(RawAdditionalProperties, PropertiesCTX) + end, + Schema = + #{ + type => object, + properties => Properties, + required => Required, + min_properties => MinProperties, + max_properties => MaxProperties, + additional_properties => AdditionalProperties, + nullable => Nullable + }, + ExtraSchemas = PropertiesExtraSchemas ++ AdditionalPropertiesExtraSchemas, + {Schema, ExtraSchemas, AdditionalPropertiesCTX}; +parse_schemas(#{<<"anyOf">> := RawAnyOf} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {AnyOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSubschema, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Subschema, ExtraSchemas, NewCTX} = parse_schemas(RawSubschema, CTXAcc), + { + [Subschema | AnyOfAcc], + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + } + } + end, + {[], [], CTX}, + RawAnyOf + ), + Schema = #{ + any_of => AnyOf, + nullable => Nullable + }, + {Schema, ExtraSchemas, NewCTX}; +parse_schemas(#{<<"allOf">> := RawAllOf} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {AllOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSubschema, {AllOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Subschema, ExtraSchemas, NewCTX} = parse_schemas(RawSubschema, CTXAcc), + { + [Subschema | AllOfAcc], + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + } + } + end, + {[], [], CTX}, + RawAllOf + ), + Schema = #{ + all_of => AllOf, + nullable => Nullable + }, + {Schema, ExtraSchemas, NewCTX}; +parse_schemas(#{<<"not">> := RawNot} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {Not, ExtraSchemas, NewCTX} = parse_schemas(RawNot, CTX), + Schema = #{ + 'not' => Not, + nullable => Nullable + }, + {Schema, ExtraSchemas, NewCTX}; +parse_schemas(#{<<"oneOf">> := RawOneOf} = RawSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawSchema, undefined), + {OneOf, ExtraSchemas, NewCTX} = + lists:foldl( + fun(RawSubschema, {OneOfAcc, ExtraSchemasAcc, CTXAcc}) -> + {Subschema, ExtraSchemas, NewCTX} = parse_schemas(RawSubschema, CTXAcc), + { + [Subschema | OneOfAcc], + ExtraSchemasAcc ++ ExtraSchemas, + CTXAcc#{ + resolved => maps:get(resolved, NewCTX) + } + } + end, + {[], [], CTX}, + RawOneOf + ), + Schema = #{ + one_of => OneOf, + nullable => Nullable + }, + {Schema, ExtraSchemas, NewCTX}; +parse_schemas(RawUniversalSchema, CTX) -> + Nullable = maps:get(<<"nullable">>, RawUniversalSchema, undefined), + Schema = #{ + nullable => Nullable + }, + {Schema, [], CTX}. + +-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 -> + erf_util:to_snake_case(<>) + end, + NewSchema = ndto_parser_json_schema:get(LocalPath, NewSpec), + NewCTX = #{ + base_path => NewBasePath, + base_name => NewBaseName, + namespace => NewBaseName, + resolved => [NewResolved | Resolved], + spec => NewSpec + }, + {NewResolved, NewSchema, NewCTX}. diff --git a/src/erf_router.erl b/src/erf_router.erl index 4c450bf..c341a50 100644 --- a/src/erf_router.erl +++ b/src/erf_router.erl @@ -126,18 +126,24 @@ handle(ElliRequest, [Name]) -> {ok, PreProcessMiddlewares} = erf_conf:preprocess_middlewares(Name), {ok, RouterMod} = erf_conf:router_mod(Name), {ok, PostProcessMiddlewares} = erf_conf:postprocess_middlewares(Name), - Request = preprocess(ElliRequest), - {InitialResponse, InitialRequest} = - case apply_preprocess_middlewares(Request, PreProcessMiddlewares) of - {stop, PreprocessResponse, PreprocessRequest} -> - {PreprocessResponse, PreprocessRequest}; - PreprocessRequest -> - {RouterMod:handle(PreprocessRequest), PreprocessRequest} - end, - Response = apply_postprocess_middlewares( - InitialRequest, InitialResponse, PostProcessMiddlewares - ), - postprocess(InitialRequest, Response). + case preprocess(ElliRequest) of + {ok, Request} -> + {InitialResponse, InitialRequest} = + case apply_preprocess_middlewares(Request, PreProcessMiddlewares) of + {stop, PreprocessResponse, PreprocessRequest} -> + {PreprocessResponse, PreprocessRequest}; + PreprocessRequest -> + {RouterMod:handle(PreprocessRequest), PreprocessRequest} + end, + Response = apply_postprocess_middlewares( + InitialRequest, InitialResponse, PostProcessMiddlewares + ), + postprocess(InitialRequest, Response); + {error, _Reason} -> + ContentTypeHeader = string:casefold(<<"content-type">>), + % TODO: handle error + {500, [{ContentTypeHeader, <<"text/plain">>}], <<"Internal Server Error">>} + end. -spec handle_event(Event, Data, CallbackArgs) -> ok when Event :: atom(), @@ -264,7 +270,7 @@ handle_ast(API, #{callback := Callback} = Opts) -> end, Parameters ), - RequestBody = maps:get(request_body, Operation), + Request = maps:get(request, Operation), PathParametersAST = erl_syntax:list( lists:map( @@ -288,7 +294,7 @@ handle_ast(API, #{callback := Callback} = Opts) -> ), IsValidRequestAST = is_valid_request( Parameters, - RequestBody + Request ), erl_syntax:clause( @@ -581,100 +587,135 @@ handle_ast(API, #{callback := Callback} = Opts) -> RESTClauses ++ StaticClauses ++ [NotFoundClause] ). --spec is_valid_request(Parameters, RequestBody) -> Result when +-spec is_valid_request(Parameters, Request) -> Result when Parameters :: [erf_parser:parameter()], - RequestBody :: erf_parser:ref(), + Request :: erf_parser:request(), Result :: erl_syntax:syntaxTree(). -is_valid_request(RawParameters, RequestBody) -> - Body = - case RequestBody of - undefined -> - erl_syntax:atom(true); - _RequestBody -> - RequestBodyModule = erlang:binary_to_atom(erf_util:to_snake_case(RequestBody)), - erl_syntax:application( - erl_syntax:atom(RequestBodyModule), - erl_syntax:atom(is_valid), - [erl_syntax:variable('Body')] +is_valid_request(RawParameters, Request) -> + RawRequestBody = maps:get(body, Request), + RequestBodyRef = maps:get(ref, RawRequestBody), + RequestBodyModule = + erlang:binary_to_atom(erf_util:to_snake_case(RequestBodyRef)), + RequestBodyIsValid = + erl_syntax:application( + erl_syntax:atom(RequestBodyModule), + erl_syntax:atom(is_valid), + [erl_syntax:variable('Body')] + ), + RequestBody = + case maps:get(required, RawRequestBody) of + true -> + RequestBodyIsValid; + false -> + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Body'), + erl_syntax:operator('=:='), + erl_syntax:atom(undefined) + ), + erl_syntax:operator('orelse'), + RequestBodyIsValid ) end, - Parameters = lists:filtermap( - fun(Parameter) -> - ParameterModule = erlang:binary_to_atom(maps:get(ref, Parameter)), - ParameterName = maps:get(name, Parameter), - ParameterType = maps:get(type, Parameter), - case ParameterType of - header -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [ - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(ParameterName) - ) + FilteredParameters = + lists:filtermap( + fun(Parameter) -> + ParameterModule = erlang:binary_to_atom(maps:get(ref, Parameter)), + ParameterName = maps:get(name, Parameter), + ParameterType = maps:get(type, Parameter), + case ParameterType of + header -> + GetParameter = + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(ParameterName) ) - ]), - erl_syntax:variable('Headers') - ] - ) - ] - ) - }; - cookie -> - %% TODO: implement - false; - path -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:variable( - erlang:binary_to_atom( - erf_util:to_pascal_case(ParameterName) - ) + ) + ]), + erl_syntax:variable('Headers') + ] + ), + ParameterRequired = maps:get(required, Parameter), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => ParameterRequired + }}; + cookie -> + %% TODO: implement + false; + path -> + GetParameter = + erl_syntax:variable( + erlang:binary_to_atom( + erf_util:to_pascal_case(ParameterName) ) - ] - ) - }; - query -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [ - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(ParameterName) - ) + ), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => true + }}; + query -> + GetParameter = + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(ParameterName) ) - ]), - erl_syntax:variable('QueryParameters') - ] - ) - ] + ) + ]), + erl_syntax:variable('QueryParameters') + ] + ), + ParameterRequired = maps:get(required, Parameter), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => ParameterRequired + }} + end + end, + RawParameters + ), + Parameters = + lists:map( + fun(#{module := ParameterModule, get := GetParameter, required := ParameterRequired}) -> + IsValidParameter = + erl_syntax:application( + erl_syntax:atom(ParameterModule), + erl_syntax:atom(is_valid), + [GetParameter] + ), + OptionalParameter = + erl_syntax:infix_expr( + GetParameter, + erl_syntax:operator('=:='), + erl_syntax:atom(undefined) + ), + case ParameterRequired of + true -> + IsValidParameter; + false -> + erl_syntax:infix_expr( + OptionalParameter, + erl_syntax:operator('orelse'), + IsValidParameter ) - } - end - end, - RawParameters - ), - chain_conditions([Body | Parameters], 'andalso'). + end + end, + FilteredParameters + ), + chain_conditions([RequestBody | Parameters], 'andalso'). -spec load_binary(ModuleName, Bin) -> Result when ModuleName :: atom(), @@ -711,41 +752,69 @@ postprocess( ), {Status, Headers, {file, File, Range}}; postprocess(_Request, {Status, RawHeaders, RawBody}) -> - {Headers, Body} = - case proplists:get_value(<<"content-type">>, RawHeaders, undefined) of - undefined -> - { - [{<<"content-type">>, <<"application/json">>} | RawHeaders], - njson:encode(RawBody) - }; - _Otherwise -> - case RawBody of - undefined -> - {RawHeaders, <<>>}; - _RawBody -> - {RawHeaders, RawBody} - end - end, - {Status, Headers, Body}. + ContentTypeHeader = string:casefold(<<"content-type">>), + case proplists:get_value(ContentTypeHeader, RawHeaders, undefined) of + JSON when JSON =:= undefined orelse JSON =:= <<"application/json">> -> + case njson:encode(RawBody) of + {ok, EncodedBody} -> + Headers = [ + {ContentTypeHeader, <<"application/json">>} + | proplists:delete(ContentTypeHeader, RawHeaders) + ], + {Status, Headers, EncodedBody}; + {error, _Reason} -> + % TODO: handle error + {500, [{ContentTypeHeader, <<"text/plain">>}], <<"Internal Server Error">>} + end; + _Otherwise -> + {Status, RawHeaders, RawBody} + end. --spec preprocess(Req) -> Request when +-spec preprocess(Req) -> Result when Req :: elli:req(), - Request :: erf:request(). + Result :: {ok, Request} | {error, Reason}, + Request :: erf:request(), + Reason :: term(). preprocess(Req) -> Path = elli_request:path(Req), Method = preprocess_method(elli_request:method(Req)), QueryParameters = elli_request:get_args_decoded(Req), Headers = elli_request:headers(Req), - Body = njson:decode(elli_request:body(Req)), Peer = elli_request:peer(Req), - #{ - path => Path, - method => Method, - query_parameters => QueryParameters, - headers => Headers, - body => Body, - peer => Peer - }. + ContentTypeHeader = string:casefold(<<"content-type">>), + RawBody = + case elli_request:body(Req) of + <<>> -> + undefined; + ElliBody -> + ElliBody + end, + + case proplists:get_value(ContentTypeHeader, Headers, undefined) of + <<"application/json">> -> + case njson:decode(RawBody) of + {ok, Body} -> + {ok, #{ + path => Path, + method => Method, + query_parameters => QueryParameters, + headers => Headers, + body => Body, + peer => Peer + }}; + {error, Reason} -> + {error, {cannot_decode_body, Reason}} + end; + _ContentType -> + {ok, #{ + path => Path, + method => Method, + query_parameters => QueryParameters, + headers => Headers, + body => RawBody, + peer => Peer + }} + end. -spec preprocess_method(ElliMethod) -> Result when ElliMethod :: elli:http_method(), diff --git a/test/erf_oas_3_0_SUITE.erl b/test/erf_parser_oas_3_0_SUITE.erl similarity index 55% rename from test/erf_oas_3_0_SUITE.erl rename to test/erf_parser_oas_3_0_SUITE.erl index 5defcc8..5ab07c8 100644 --- a/test/erf_oas_3_0_SUITE.erl +++ b/test/erf_parser_oas_3_0_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(erf_oas_3_0_SUITE). +-module(erf_parser_oas_3_0_SUITE). %%% INCLUDE FILES -include_lib("stdlib/include/assert.hrl"). @@ -72,38 +72,61 @@ petstore(_Conf) -> code:priv_dir(erf) ++ "/oas/3.0/examples/petstore.json" ), - {ok, PetstoreAPI} = erf_parser:parse(PetstoreOAS, erf_oas_3_0), + {ok, PetstoreAPI} = erf_parser:parse(PetstoreOAS, erf_parser_oas_3_0), ?assertMatch( #{ name := <<"Swagger Petstore">>, version := <<"1.0.0">>, schemas := #{ + <<"petstore_error">> := #{ + properties := + #{ + <<"code">> := #{type := integer}, + <<"message">> := #{type := string} + }, + required := [<<"code">>, <<"message">>], + type := object + }, + <<"petstore_pet">> := #{ + properties := + #{ + <<"id">> := #{type := integer}, + <<"name">> := #{type := string}, + <<"tag">> := #{type := string} + }, + required := [<<"id">>, <<"name">>], + type := object + }, + <<"petstore_pets">> := #{ + items := #{ref := <<"petstore_pet">>}, + max_items := 100, + type := array + }, <<"petstore_list_pets_limit">> := #{ - <<"type">> := <<"integer">>, - <<"maximum">> := 100 + type := integer, + maximum := 100 }, - <<"petstore_list_pets_request_body">> := undefined, + <<"petstore_list_pets_request_body">> := true, <<"petstore_list_pets_response_body_200">> := #{ - <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Pets">>}] + any_of := [#{ref := <<"petstore_pets">>}] }, <<"petstore_list_pets_response_body_default">> := #{ - <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Error">>}] + any_of := [#{ref := <<"petstore_error">>}] }, - <<"petstore_create_pets_request_body">> := undefined, - <<"petstore_create_pets_response_body_201">> := undefined, + <<"petstore_create_pets_request_body">> := true, + <<"petstore_create_pets_response_body_201">> := true, <<"petstore_create_pets_response_body_default">> := #{ - <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Error">>}] + any_of := [#{ref := <<"petstore_error">>}] }, <<"petstore_show_pet_by_id_pet_id">> := #{ - <<"type">> := <<"string">>, - <<"nullable">> := false + type := string }, - <<"petstore_show_pet_by_id_request_body">> := undefined, + <<"petstore_show_pet_by_id_request_body">> := true, <<"petstore_show_pet_by_id_response_body_200">> := #{ - <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Pet">>}] + any_of := [#{ref := <<"petstore_pet">>}] }, <<"petstore_show_pet_by_id_response_body_default">> := #{ - <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Error">>}] + any_of := [#{ref := <<"petstore_error">>}] } }, endpoints := [ @@ -118,23 +141,54 @@ petstore(_Conf) -> #{ ref := <<"petstore_list_pets_limit">>, name := <<"limit">>, - type := query + type := query, + required := false } ], - request_body := <<"petstore_list_pets_request_body">>, + request := #{ + body := #{ + ref := <<"petstore_list_pets_request_body">>, + required := false + } + }, responses := #{ - 200 := <<"petstore_list_pets_response_body_200">>, - '*' := <<"petstore_list_pets_response_body_default">> + 200 := #{ + body := #{ + ref := <<"petstore_list_pets_response_body_200">>, + required := false + } + }, + '*' := #{ + body := #{ + ref := <<"petstore_list_pets_response_body_default">>, + required := false + } + } } }, #{ id := <<"create_pets">>, method := post, parameters := [], - request_body := <<"petstore_create_pets_request_body">>, + request := #{ + body := #{ + ref := <<"petstore_create_pets_request_body">>, + required := false + } + }, responses := #{ - 201 := <<"petstore_create_pets_response_body_201">>, - '*' := <<"petstore_create_pets_response_body_default">> + 201 := #{ + body := #{ + ref := <<"petstore_create_pets_response_body_201">>, + required := false + } + }, + '*' := #{ + body := #{ + ref := <<"petstore_create_pets_response_body_default">>, + required := false + } + } } } ] @@ -153,10 +207,25 @@ petstore(_Conf) -> type := path } ], - request_body := <<"petstore_show_pet_by_id_request_body">>, + request := #{ + body := #{ + ref := <<"petstore_show_pet_by_id_request_body">>, + required := false + } + }, responses := #{ - 200 := <<"petstore_show_pet_by_id_response_body_200">>, - '*' := <<"petstore_show_pet_by_id_response_body_default">> + 200 := #{ + body := #{ + ref := <<"petstore_show_pet_by_id_response_body_200">>, + required := false + } + }, + '*' := #{ + body := #{ + ref := <<"petstore_show_pet_by_id_response_body_default">>, + required := false + } + } } } ] @@ -173,26 +242,25 @@ with_refs(_Conf) -> code:lib_dir(erf, test) ++ "/fixtures/with_refs_oas_3_0_spec.json" ), - {ok, WithRefsAPI} = erf_parser:parse(WithRefsOAS, erf_oas_3_0), + {ok, WithRefsAPI} = erf_parser:parse(WithRefsOAS, erf_parser_oas_3_0), ?assertMatch( #{ name := <<"With refs">>, version := <<"1.0.0">>, schemas := #{ <<"common_oas_3_0_spec_enabled">> := #{ - <<"type">> := <<"boolean">> + type := boolean }, <<"common_oas_3_0_spec_version">> := #{ - <<"type">> := <<"string">>, - <<"pattern">> := <<"^[0-9]+$">>, - <<"nullable">> := false - }, - <<"with_refs_oas_3_0_spec_delete_foo_request_body">> := undefined, - <<"with_refs_oas_3_0_spec_delete_foo_response_body_204">> := #{ - <<"$ref">> := <<"common_oas_3_0_spec_NoContent">> + type := string, + pattern := <<"^[0-9]+$">> }, + <<"with_refs_oas_3_0_spec_delete_foo_request_body">> := true, + <<"with_refs_oas_3_0_spec_delete_foo_response_body_204">> := true, <<"with_refs_oas_3_0_spec_delete_foo_response_body_404">> := #{ - <<"$ref">> := <<"common_oas_3_0_spec_NotFound">> + any_of := [ + #{ref := <<"common_oas_3_0_spec_error">>} + ] } } }, @@ -207,7 +275,7 @@ invalid(_Conf) -> ), {error, {invalid_spec, <<"Invalid OpenAPI Specification 3.0">>}} = erf_parser:parse( - Invalid, erf_oas_3_0 + Invalid, erf_parser_oas_3_0 ), ok. diff --git a/test/erf_router_SUITE.erl b/test/erf_router_SUITE.erl index cbcda05..8d4de36 100644 --- a/test/erf_router_SUITE.erl +++ b/test/erf_router_SUITE.erl @@ -71,34 +71,23 @@ foo(_Conf) -> version => <<"1.0.0">>, schemas => #{ <<"version_foo_version">> => #{ - <<"type">> => <<"integer">>, - <<"nullable">> => false + type => integer }, - <<"get_foo_request_body">> => undefined, - <<"get_foo_response_body">> => #{ - <<"anyOf">> => [ - #{ - <<"anyOf">> => [ - #{ - <<"description">> => <<"A foo">>, - <<"enum">> => [<<"bar">>, <<"baz">>], - <<"type">> => <<"string">> - } - ] - }, + <<"get_foo_request_body">> => true, + <<"get_foo_response_body_200">> => #{ + any_of => [#{enum => [<<"bar">>, <<"baz">>]}] + }, + <<"get_foo_response_body_default">> => #{ + any_of => [ #{ - <<"anyOf">> => [ - #{ - <<"properties">> => #{ - <<"description">> => #{ - <<"description">> => - <<"An English human-friendly description of the error.">>, - <<"type">> => <<"string">> - } - }, - <<"type">> => <<"object">> + type => object, + properties => #{ + description => #{ + description => + <<"An English human-friendly description of the error.">>, + type => string } - ] + } } ] } @@ -110,7 +99,8 @@ foo(_Conf) -> #{ ref => <<"version_foo_version">>, name => <<"version">>, - type => path + type => path, + required => true } ], operations => [ @@ -118,8 +108,24 @@ foo(_Conf) -> id => <<"get_foo">>, method => get, parameters => [], - request_body => undefined, - response_body => <<"get_foo_response_body">> + request => #{ + body => #{ + ref => <<"get_foo_request_body">>, + required => false + } + }, + responses => #{ + 200 => #{ + body => #{ + ref => <<"get_foo_response_body_200">> + } + }, + '*' => #{ + body => #{ + ref => <<"get_foo_response_body_default">> + } + } + } } ] } diff --git a/test/fixtures/common_oas_3_0_spec.json b/test/fixtures/common_oas_3_0_spec.json index 9eabce2..7656b26 100644 --- a/test/fixtures/common_oas_3_0_spec.json +++ b/test/fixtures/common_oas_3_0_spec.json @@ -56,7 +56,7 @@ } }, "requestBodies": { - "Foo": { + "PostFoo": { "content": { "application/json": { "schema": { diff --git a/test/fixtures/with_refs_oas_3_0_spec.json b/test/fixtures/with_refs_oas_3_0_spec.json index 6598234..d75d010 100644 --- a/test/fixtures/with_refs_oas_3_0_spec.json +++ b/test/fixtures/with_refs_oas_3_0_spec.json @@ -38,7 +38,7 @@ "summary": "Create foo", "operationId": "createFoo", "requestBody": { - "$ref": "common_oas_3_0_spec.json#/components/requestBodies/Foo" + "$ref": "common_oas_3_0_spec.json#/components/requestBodies/PostFoo" }, "responses": { "201": {