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": {