open-api-spex / open_api_spex Goto Github PK
View Code? Open in Web Editor NEWOpen API Specifications for Elixir Plug applications
License: Mozilla Public License 2.0
Open API Specifications for Elixir Plug applications
License: Mozilla Public License 2.0
I can view the Swagger just fine, but when I come to generate it, I get
** (ArgumentError) argument error
(stdlib) :ets.lookup(EmbedWeb.Endpoint, :__phoenix_struct_url__)
lib/phoenix/config.ex:45: Phoenix.Config.cache/3
(open_api_spex) lib/open_api_spex/server.ex:39: OpenApiSpex.Server.from_endpoint/1
lib/embed/api_spec.ex:9: Embed.V1.ApiSpec.spec/0
lib/embed/api_task.ex:6: Mix.Tasks.Embed.OpenApiSpec.run/1
(mix) lib/mix/task.ex:331: Mix.Task.run_task/3
(mix) lib/mix/cli.ex:79: Mix.CLI.run_task/2
I'm not sure what's missing. The Spec and Task modules are created correctly.
Example implementation that I have used in my project (still lacks a little bit options, but overall idea can be seen):
defmodule KokonWeb.Rest do
alias OpenApiSpex.Operation
defmacro __using__(_opts) do
quote do
alias KokonWeb.Rest.Schema
alias OpenApiSpex.Operation
plug(OpenApiSpex.Plug.Cast)
plug(OpenApiSpex.Plug.Validate)
@on_definition KokonWeb.Rest
@before_compile KokonWeb.Rest
Module.register_attribute(__MODULE__, :parameter, accumulate: true)
Module.register_attribute(__MODULE__, :response, accumulate: true)
Module.register_attribute(__MODULE__, :open_api_operations, accumulate: true)
end
end
def __on_definition__(_env, _type, :open_api_operation, _args, _guards, _body), do: nil
def __on_definition__(_env, _type, :action, _args, _guards, _body), do: nil
def __on_definition__(%Macro.Env{module: mod}, :def, name, _args, _guards, _body) do
parameters = Module.delete_attribute(mod, :parameter)
response = Module.delete_attribute(mod, :response)
{summary, doc} = docs(Module.get_attribute(mod, :doc))
operation =
%Operation{
summary: summary || "TODO",
description: doc,
operationId: module_name(mod) <> ".#{name}",
parameters: parameters,
responses: Map.new(response)
}
Module.put_attribute(mod, :open_api_operations, {name, operation})
end
def __on_definition__(_env, _type, _name, _args, _guards, _body), do: nil
defmacro __before_compile__(_env) do
quote unquote: false do
for {name, operation} <- Module.delete_attribute(__MODULE__, :open_api_operations) do
def open_api_operation(unquote(name)), do: unquote(Macro.escape(operation))
end
end
end
defp docs(nil), do: {nil, nil}
defp docs({_, doc}) do
[summary | _] = String.split(doc, ~r/\n\s*\n/, parts: 2)
{summary, doc}
end
defp module_name(mod) when is_atom(mod), do: module_name(Atom.to_string(mod))
defp module_name("Elixir." <> name), do: name
defp module_name(name) when is_binary(name), do: name
end
This causes controller to look like this:
defmodule KokonWeb.Rest.Controllers.Schedule do
use KokonWeb.Rest
@doc "List schedule"
@response {200, Operation.response(
"Submissions",
"application/json",
KokonWeb.Rest.Schema.Submissions
)}
def index(conn, _params) do
{:ok, submissions} = Kokon.Submissions.all()
json(conn, submissions)
end
@doc "Create new submission"
@parameter Operation.parameter(:title, :query, :string, "Submission title",
required: true
)
@parameter Operation.parameter(
:abstract,
:query,
:string,
"Submission description",
required: true
)
@response {200, Operation.response(
"Submissions",
"application/json",
KokonWeb.Rest.Schema.Submission
)}
def create(conn, %{title: title, abstract: abstract}) do
with {:ok, submission} <-
Kokon.Submissions.create(%{title: title, abstract: abstract}) do
json(conn, submission)
end
end
end
I am opening this as an issue instead of PR as I would like to know opinions about this beforehand.
The type spec for params
and body_params
is params() :: %{required(binary()) => param()}
(https://hexdocs.pm/plug/1.7.2/Plug.Conn.html#t:params/0), but OpenApiSpex turns them into something like %{__struct__: module(), required(atom()) => term()}
, or something else that doesn't match the spec. This causes dialyzer complaints in the controller actions for some situations.
IMO, OpenApiSpex shouldn't change conn.params
or conn.body_params
, as it breaks the contract with Plug. Instead, Spex can put the casted values in the :private
attribute of the %Conn{}
.
When adding casted value to the connection body params this raises dialyzer errors as this does not meet the specification of Plug.Conn.t()
where only atom
and binary
are allowed specificatallly.
%Plug.Conn{
:adapter => {atom(), _},
:assigns => %{atom() => _},
:before_send => [(map() -> map())],
:body_params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
...
In order to meet the specification of Plug.Conn.t()
the casted values will need to be put into the body params like so:
conn = %{body_params: %{"request" => %SomeRequest{}}}
Or I will need to make the request schema an unstructured map to enable this without a code change which feels bad as it adds a layer of nesting. I will try this out.
This might be a non-issue, but at the very least this will need documented as request_body
function pushes users in this direction during schema definition.
request_body(
"Integration registration attributes",
"application/json",
__MODULE__.RegistrationRequest
),
It is unfortunate that this will require: @dialyzer {:nowarn_function, {:register, 2}}
or a less strict exclusion such as :no_return
(see examples) as one of the benefits of the casting is the ability to perform this type checking.
Controller Action
def register(conn = %{body_params: request}, _params) do
%RegistrationRequest{} = request
render(conn, "not_implemented.json")
end
The inference of the body_params
type as a Struct (as above) is enough for dialyzer to warn:
apps/bond_web/lib/bond_web/controllers/integration_controller.ex:9:no_return
Function register/2 has no local return.
This report becomes more specific when you put the destructure into the function param like so:
def register(conn = %{body_params: %RegistrationRequest{} = request}, _params) do
render(conn, "not_implemented.json")
end
Phoenix.Controller.render(
_conn :: %{
:body_params => %BondWeb.Schemas.Integrations.Operations.RegistrationRequest{_ => _},
_ => _
},
<<_::160>>
)
will never return since the success typing is:
(
%Plug.Conn{
:adapter => {atom(), _},
:assigns => %{atom() => _},
:before_send => [(map() -> map())],
:body_params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
:halted => _,
:host => binary(),
:method => binary(),
:owner => pid(),
:params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:path_info => [binary()],
:path_params => %{
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:port => char(),
:private => %{atom() => _},
:query_params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:query_string => binary(),
:remote_ip =>
{byte(), byte(), byte(), byte()}
| {char(), char(), char(), char(), char(), char(), char(), char()},
:req_cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
:req_headers => [{binary(), binary()}],
:request_path => binary(),
:resp_body =>
nil
| binary()
| maybe_improper_list(
binary() | maybe_improper_list(any(), binary() | []) | byte(),
binary() | []
),
:resp_cookies => %{binary() => %{}},
:resp_headers => [{binary(), binary()}],
:scheme => :http | :https,
:script_name => [binary()],
:secret_key_base => nil | binary(),
:state => :chunked | :file | :sent | :set | :set_chunked | :set_file | :unset,
:status => nil | non_neg_integer()
},
atom() | binary() | Keyword.t() | map()
) :: %Plug.Conn{
:adapter => {atom(), _},
:assigns => %{atom() => _},
:before_send => [(map() -> map())],
:body_params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
:halted => _,
:host => binary(),
:method => binary(),
:owner => pid(),
:params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:path_info => [binary()],
:path_params => %{
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:port => char(),
:private => %{atom() => _},
:query_params => %Plug.Conn.Unfetched{
:aspect => atom(),
binary() =>
binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
},
:query_string => binary(),
:remote_ip =>
{byte(), byte(), byte(), byte()}
| {char(), char(), char(), char(), char(), char(), char(), char()},
:req_cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
:req_headers => [{binary(), binary()}],
:request_path => binary(),
:resp_body =>
nil
| binary()
| maybe_improper_list(
binary() | maybe_improper_list(any(), binary() | []) | byte(),
binary() | []
),
:resp_cookies => %{binary() => %{}},
:resp_headers => [{binary(), binary()}],
:scheme => :http | :https,
:script_name => [binary()],
:secret_key_base => nil | binary(),
:state => :sent,
:status => nil | non_neg_integer()
}
and the contract is
(Plug.Conn.t(), Keyword.t() | map() | binary() | atom()) :: Plug.Conn.t()
tags
security
x-struct
, title
I believe that since serialization is not part of the lib this has to be done somewhere else, but maybe there's a Protocol to implement that can use the "omitempty" idiom of Golang? I may be wrong as I am somewhat new to Elixir.
Thanks
When I define result schema as:
Operation.response(
"Submissions",
"application/json",
%OpenApiSpex.Schema{
title: "Submissions",
description: "List of submissions",
type: :array,
items: KokonWeb.Rest.Schema.Submission
}
)
Then in the the output it is defined as:
{
"description": "Submissions",
"content": {
"application/json": {
"schema": {
"type": "array",
"title": "Submissions",
"items": "Elixir.KokonWeb.Rest.Schema.Submission",
"description": "List of submissions"
}
}
}
}
Where I would expect items
to be reference to Submission
definition.
I'm no longer using swagger in my day job, so I haven't had the need to make many improvements to this package.
If there are any users who are relying on open_api_spex in commercial / production settings that are willing to share in the maintenance of the project, please respond in the comments, or email me ([email protected]).
I'm happy to move this repo to a Github org and share ownership of the Hex package.
Following on #23 (and my attempt at solving it: #42), I have been instead using the really good ex_json_schema
.
Here is the code I am using:
case %{
"components" => components,
"additionalProperties" => false,
"type" => "object",
"required" => [schema_name],
"properties" => %{
schema_name => %{"$ref" => "#/components/schemas/#{schema_name}"}
}
}
|> ExJsonSchema.Schema.resolve()
|> ExJsonSchema.Validator.validate(resp)
There needs to be a translation step first though, to rewrite nullable: true
and such incompatibilities of OpenAPIV3 schemas with JSON Schema Draft-04.
Would you be open to use ex_json_schema
validators instead of the current ones in this lib?
Where do you think this translation step should be: within this lib or within ex_json_schema
?
Thanks
Hi,
Is this possible, given I have a request and a response that I wish to validate. I'd like to:
I don't want to use Plug
s, I'd like to use it as a pure API.
Is there a recommendation for casting with both body and url parameters, using the cast plug?
I see there is support for defining out of band callbacks which are part of an operation.
https://swagger.io/specification/#callbackObject
I cannot figure out how this should be written using OpenAPISpex as I am running into some strange errors. Can you please provide an example of how callbacks should be written?
I see Operation has a key callbacks
, this takes a map of String.t
=>PathItem.t
.
The PathItem.t
has fields which each take an Operation.t
.
callbacks: %{
"account_link_callback" => %PathItem{
post: %Operation{
summary: "Account Link Callback",
description:
"Invoked when account link is established or if the account link process fails",
responses:
response(
"List registered integrations response",
"application/json",
__MODULE__.ListResponse
)
}
}
This produces:
Which from the outset looks OK?
But there is likely an issue as it renders like so:
If I expose the swagger endpoint in my Phoenix router with:
scope "/" do
_ = get("/swagger", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi")
end
I get the error
unmatched_return
Expression produces a value of type:
%{:path => _}
but this value is unmatched.
I'm assuming the issue isn't actually in the router, but the module itself. Is that right?
I need to represent a parameter that can be an integer or a string in this way:
%OpenApiSpex.Schema{
oneOf: [
%OpenApiSpex.Schema{type: :integer},
%OpenApiSpex.Schema{type: :string}
]
}
but this results in error
** (ArgumentError) the following keys must also be given when building struct OpenApiSpex.Schema: [:type]
The same happens using references and it the documentation example
%OpenApiSpex.Schema{
oneOf: [
%OpenApiSpex.Reference{"$ref": "#/components/schemas/Foo"},
%OpenApiSpex.Reference{"$ref": "#/components/schemas/Bar"}
]
}
I am seeing the following error when testing a controller action (with the stock Phoenix ConnCase style tests):
1) test POST /api/v0/integrations/register produces a RegistrationSuccessfulResponse (BondWeb.IntegrationControllerTest)
test/bond_web/controllers/integration_controller_test.exs:26
** (UndefinedFunctionError) function nil.schema/0 is undefined
code: |> post(integration_path(conn, :register), registration_args)
stacktrace:
nil.schema()
(open_api_spex) lib/open_api_spex/operation2.ex:35: OpenApiSpex.Operation2.cast_request_body/4
(open_api_spex) lib/open_api_spex/operation2.ex:21: OpenApiSpex.Operation2.cast/4
(open_api_spex) lib/open_api_spex/plug/cast_and_validate.ex:70: OpenApiSpex.Plug.CastAndValidate.call/2
(bond_web) lib/bond_web/controllers/integration_controller.ex:1: BondWeb.IntegrationController.phoenix_controller_pipeline/2
(bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
(bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.plug_builder_call/2
(bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:235: Phoenix.ConnTest.dispatch/5
test/bond_web/controllers/integration_controller_test.exs:39: (test)
I can see that that the error is happening within the plug OpenApiSpex.Plug.CastAndValidate
however when I call the API when the Phoenix server is running, this plug does not fail and the Casting and Validation works fine.
This leads me to conclude that something is not happening (possibly some information in conn
?) in test that is present during development execution.
This may not be a bug, but it does seem that there is an initialisation step needed in testing to avoid this issue?
Appreciate any help in this matter, love the project <3!
Hi @mbuhot,
could you pls release the latest patches to a new hex version?
Or are there any metrics on which you decide to do so?
Cheers.
I'm using a lot of Phoenix Resource Controllers in my project.
Since the 'update' function is registered on the patch and put routes for the resource controller, there will also be a patch and put call in the openapi spec.
The problem now is, that the definition for the update function is identicaly on both calls, including the operation id.
This isn't actually just e resource controller problem. Duplicated operation Ids will be present as soon as multiple routes lead to the same controller function.
When we get a validation error in the fallback controller we receive a string in the body for the error.
This string is hard to parse and we have started down the path of trying to parse these strings to get the path of the invalid property in order to produce proper json api error responses that are meaningful to our API consumers.
I would like to propose returning something easier to work with within the code.
Perhaps returning a Map with some keys we can get the info from without parsing strings.
Examples:
(Current Error)
"#/data: Missing required properties: [:relationships]"
could be something like
%{
path: ["data", "relationships"],
message: "Missing required properties"
}
and
(Current Error)
"#/data/attributes/resource: null value where string expected"
could be something like
%{
path: ["data", "attributes", "resource"],
message: "null value where string expected"
}
We should have a way to represent paths that have arrays in them as well
"#/data/0/id: null value where string expected"
%{
path: ["data", 0, "id"],
message: "null value where string expected"
}
We would be happy to create a PR for this, we wanted to propose the idea and get some feedback before going forward with this approach.
We also may be interested in maintaining this project as well as it looks like we will be using it extensively for our applications.
https://github.com/GhostGroup
Thank you
Currently the community is mostly migrating to jason
library due to it's performance benefits. It would be worth making JSON encoder configurable so when in need someone could change it to fit their own preferences and/or needs (for example to use streaming parser instead).
If I have a schema like the following
OpenApiSpex.schema(%{
title: "Attributes",
type: :array,
items: Api.Schemas.Attribute
})
end
would be nice to do assert_schema("Attributes", spec)
This at the moment does not work as assert_schema accepts a map.
Is it something something that can be added?
cast/1
should cast string types with a format of :date
to a %Date{}
, and a format of :"date-time"
to %DateTime{}
. Instead, it passes the input string untouched.
The old cast function casted date and date-time strings to their respective Elixir types, but the new module doesn't.
Here are the invalid tests:
We are using open_api_spex to document our apis and slowly adding schema validation in our tests. However we are seeing some unexpected result and errors mostly to using the oneOf
schemas.
In order to describe what we are seeing (maybe its because we are not correctly setting up the schema) we created a small scenario using the example on the swagger.io site: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof
On the swagger site 3 examples are given with explanation why data should be valid or invalid. We implemented these as test, unfortunately we cannot seem to make the tests pass.
defmodule OAS do
require OpenApiSpex
alias OpenApiSpex.{Schema, OpenApi, Components}
defmodule Cat,
do:
OpenApiSpex.schema(%{
title: "Cat",
type: :object,
properties: %{
hunts: %Schema{type: :boolean},
age: %Schema{type: :integer}
}
})
defmodule Dog,
do:
OpenApiSpex.schema(%{
title: "Dog",
type: :object,
properties: %{
bark: %Schema{type: :boolean},
breed: %Schema{type: :string, enum: ["Dingo", "Husky", "Retriever", "Shepherd"]}
}
})
defmodule CatOrDog,
do:
OpenApiSpex.schema(%{
title: "CatOrDog",
anyOf: [OAS.Cat, OAS.Dog]
})
def spec() do
schemas =
for module <- [OAS.Cat, OAS.Dog, OAS.CatOrDog], into: %{} do
{module.schema().title, module.schema()}
end
%OpenApi{info: %{}, paths: %{}, components: %Components{schemas: schemas}}
|> OpenApiSpex.resolve_schema_modules()
end
end
defmodule OASTest do
@moduledoc false
use ExUnit.Case
alias OpenApiSpex.Schema
@api_spec OAS.spec()
@schema @api_spec.components.schemas["CatOrDog"]
test "casting yields strange data" do
input = %{"bark" => true, "breed" => "Dingo"}
assert {:ok, %OAS.Dog{bark: true, breed: "Dingo"}} =
OpenApiSpex.cast(@api_spec, @schema, input)
end
test "should be invalid (not valid against both schemas)" do
input = %{"bark" => true, "hunts" => true}
refute :ok == OpenApiSpex.validate(@api_spec, @schema, input)
end
test "should be invalid (valid against both)" do
input = %{"bark" => true, "hunts" => true, "breed" => "Husky", "age" => 3}
refute :ok == OpenApiSpex.validate(@api_spec, @schema, input)
end
end
Are we missing something ? Any suggestion would be awesome !
There is no way to validate requests without also casting the request to a struct, as opposed to a simple map. We haven't found any benefit of receiving structs-- only drawbacks.
The two drawbacks we have found are:
Map.from_struct/1
on the casted struct, because the __struct__
key interferes with what we're trying to do.Would anyone please list specific benefits of returning structs for the cast operation?
Hi,
Im currently figuring out, if we should use open_api_spex
in our project. There we have the need to use :oneOf
(https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof).
I could not figure out, how to use it. My schema is similar to the following example:
defmodule MySchema do
alias OpenApiSpex.Schema
@behaviour OpenApiSpex.Schema
@derive [Poison.Encoder]
@schema %Schema{
title: "MySchema",
type: :object,
oneOf: [
MyOtherSchemaA,
MyOtherSchemaB
],
properties: %{},
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
end
Running this, give me the following stacks track:
2018-05-25 14:17:53.319 [error] #PID<0.1238.0> running Web.Endpoint terminated
Server: localhost:4000 (http)
Request: GET /api/v1/doc
** (exit) an exception was raised:
** (FunctionClauseError) no function clause matching in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:133: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema([MyOtherSchemaA, MyOtherSchemaB], %{})
(open_api_spex) lib/open_api_spex/schema_resolver.ex:149: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:172: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema_properties/2
(stdlib) lists.erl:1263: :lists.foldl/3
(open_api_spex) lib/open_api_spex/schema_resolver.ex:154: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:142: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:98: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_media_type/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:92: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_content/2
(stdlib) lists.erl:1263: :lists.foldl/3
(open_api_spex) lib/open_api_spex/schema_resolver.ex:121: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_response/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:115: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_responses/2
(stdlib) lists.erl:1263: :lists.foldl/3
(open_api_spex) lib/open_api_spex/schema_resolver.ex:61: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_operation/2
(open_api_spex) lib/open_api_spex/schema_resolver.ex:53: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_path_item/2
(elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
(open_api_spex) lib/open_api_spex/schema_resolver.ex:43: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_paths/2
(stdlib) lists.erl:1263: :lists.foldl/3
(open_api_spex) lib/open_api_spex/schema_resolver.ex:36: OpenApiSpex.SchemaResolver.resolve_schema_modules/1
(open_api_spex) lib/open_api_spex/plug/put_api_spec.ex:36: OpenApiSpex.Plug.PutApiSpec.build_spec/1
(open_api_spex) lib/open_api_spex/plug/put_api_spec.ex:20: OpenApiSpex.Plug.PutApiSpec.call/2
How do I define a schema which should consists of one of two other schemas?
Any help is appreciated.
Cheers,
Tobias
I was trying to generate the json docs for a module I am working on and wasn't able to. After some time trying I decided to try with a clean phoenix server project. Copying and pasting the code from the docs and running the command:
mix apitest.open_api_spec spec.json
I couldn't get the command to work, getting the same error I have in my own personal project. Which is this:
** (ArgumentError) argument error
(stdlib) :ets.lookup(ApitestWeb.Endpoint, :__phoenix_struct_url__)
(phoenix) lib/phoenix/config.ex:42: Phoenix.Config.cache/3
lib/open_api_spex/server.ex:39: OpenApiSpex.Server.from_endpoint/1
lib/api_spec.ex:11: ApitestWeb.ApiSpec.spec/0
lib/apitest/openapispec.ex:4: Mix.Tasks.Apitest.OpenApiSpec.run/1
(mix) lib/mix/task.ex:331: Mix.Task.run_task/3
(mix) lib/mix/cli.ex:79: Mix.CLI.run_task/2
(elixir) lib/code.ex:767: Code.require_file/2
Not sure exactly what is going on but I am following the getting started docs to the letter. I can share the code if it helps.
And I am using phoenix v1.4.6
I didn't want to put a lot of information as to not bloat the issue, but I'll be more than happy to provide any information you deem relevant.
@mbuhot could we get a release cut and pushed to hex? There's been a lot of updates and fixes since the last release at the end of October.
Thanks!
Hi Mike
Lets say one have a following query
/books?filter[name]=Encyclopedia&filter[edition]=2&filter[published]=2018-01-01
i.e. there is a "deepObject" (https://swagger.io/docs/specification/serialization/) in the query,
called filter
, which can (but does not have to) contain properties with specific format (string, number, date).
How one could make an operation spec, using open_api_spex,
so that OpenApiSpex.Plug.Validate would properly validate its parts?
Thanks
Slavo
This would simplify unification of error displaying in responses after failure. Right now it can be a little bit troublesome as user need to handle 2 different formats.
After merging #45 it is not possible anymore to cast GET request, because of GET request does not contains request body, no content type header is required, which leads to this exception:
** (FunctionClauseError) no function clause matching in String.split/3
The following arguments were given to String.split/3:
# 1
nil
# 2
";"
# 3
[]
Attempted function clauses (showing 4 out of 4):
def split(string, %Regex{} = pattern, options) when is_binary(string)
def split(string, "", options) when is_binary(string)
def split(string, pattern, []) when is_tuple(pattern) or is_binary(string)
def split(string, pattern, options) when is_binary(string)
code: conn = .....
stacktrace:
(elixir) lib/string.ex:383: String.split/3
(open_api_spex) lib/open_api_spex/plug/cast.ex:58: OpenApiSpex.Plug.Cast.call/2
It's too easy to make silly mistakes in OpenApiSpex.schema
macro :-)
My mistake was omitting type
field of a regular (non-polymorphic) schema.
OpenApiSpex.schema(
%{
title: "Response",
# type: :object" <-- it's missing here
properties: %{
page: %Schema{type: :integer, required: true},
hitsPerPage: %Schema{type: :integer},
....
}
}
As a result, testing the response structure in my test suite didn't really check the properties
# this always succeeded
assert_schema(response, "Response", api_spec)
I guess the validation was halting at this code in schema.ex
def validate(%Schema{type: nil}, _value, _path, _schemas) do
# polymorphic schemas will terminate here after validating against anyOf/oneOf/allOf/not
:ok
end
What do you think about adding some checks to OpenApiSpex.schema
macro to avoid at least this particular silly but dangerous mistake?
open_api_spex/lib/open_api_spex/server.ex
Lines 25 to 39 in 1c7b252
I feel this can be better achieved through:
%Server{
url: MyApp.Endpoint.url()
}
Add tests and implementation for the following
oneOf
anyOf
allOf
not
discriminator
A concern about divergence of the Open API spec was brought up here: #69 (comment)
Adjust the behavior in the string validation logic. The current behavior is to trim the string before validating string length. Change it to not trim the string before validating string length. The reason for trimming was to provide a cleaner way to validate non-blankness. However, this is not supported by the spec. The workaround for validating non-blankness is to use a regular expression validation with the pattern /^\S+$/
.
Caching can be problematic in development and cleaning configuration after each recompilation can be troublesome.
Additionally such behaviour can be problematic when doing hot upgrades.
One possible solution is to store mod.module_info(:md5)
along the cached data and drop everything on hash change.
In case OpenApiSpex.Plug.Cast
is used (in Phoenix) without OpenApiSpex.Plug.PutApiSpec
before it,
the request might never end.
I suppose it is because
https://github.com/mbuhot/open_api_spex/blob/a683e7a444bbfae6f78cce52a068eed97c1a3f54/lib/open_api_spex/plug/cast.ex#L41-L43
is causing an infinite recursion.
(as the open_api_spex
is not present in the conn
)
Hi @mbuhot, if an undefined query parameter is transmitted the cast plug (and also the validate plug) is just ignoring this parameter. I think it would be better for the api user to respond with an undefined parameter error. Otherwise it is hard for the consumer to debug unexpected behavior.
What do you think?
Cheers.
Hi there ๐
This probably relates to #23
First off, thanks a lot for all the work to put this library together!
I've had a great time using it to document some Elixir APIs.
Recently I've been trying to invest some effort in building better schemas and let OpenApiSpex.Plug.Cast
cast them into structs. However I ran into a few issues when trying to use polymorphic schemas.
For instance if I have the following schemas:
defmodule OpenApiSpexOneOfDemo.PetPlug.PetRequest do
require OpenApiSpex
alias OpenApiSpex.Discriminator
alias OpenApiSpexOneOfDemo.Schemas.{Cat, Dog}
OpenApiSpex.schema(%{
title: "PetRequest",
type: :object,
oneOf: [Cat, Dog],
discriminator: %Discriminator{
propertyName: "pet_type"
},
example: %{"pet_type" => "Cat", "meow" => "meow"}
})
end
defmodule OpenApiSpexOneOfDemo.Schemas do
defmodule Cat do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Cat",
type: :object,
properties: %{
pet_type: %Schema{type: :string},
meow: %Schema{type: :string}
},
required: [:pet_type, :meow]
})
end
defmodule Dog do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Dog",
type: :object,
properties: %{
pet_type: %Schema{type: :string},
bark: %Schema{type: :string}
},
required: [:pet_type, :bark]
})
end
end
I would expect posting the following JSON:
{
"pet_type": "Cat",
"meow": "meow"
}
To be cast into the following struct when using the OpenApiSpex.Plug.Cast
plug:
%OpenApiSpexOneOfDemo.Schemas.Cat{meow: "meow", pet_type: "Cat"}
However, in my tests, it ends up being cast into:
%OpenApiSpexOneOfDemo.PetPlug.PetRequest{}
Having gone through the code a bit, it seems that casting into polymorphic schemas is simply not implemented... ๐ข
Is that the case or / and I doing something wrong?
If it hasn't been implemented yet, is there anything I can do to help?
Any pointers towards what would need to be done?
Do you have any idea how much effort you think it would take this implemented?
As a first step, only supporting polymorphic schemas that use discriminators might make this easier to implement. What do you think?
The full code for my test from which the above snippets are taken from can be found here: maxmellen/open_api_spex_one_of_demo
Running mix test
will showcase the issue I am talking about here:
1) test {"pet_type": "Cat", "meow": "meow"} is cast into %OpenApiSpexOneOfDemo.Schemas.Cat{} (OpenApiSpexOneOfDemo.PetPlugTest)
test/open_api_spex_one_of_demo/pet_plug_test.exs:8
Assertion with == failed
code: assert conn.resp_body() == "%OpenApiSpexOneOfDemo.Schemas.Cat{meow: \"meow\", pet_type: \"Cat\"}"
left: "%OpenApiSpexOneOfDemo.PetPlug.PetRequest{}"
right: "%OpenApiSpexOneOfDemo.Schemas.Cat{meow: \"meow\", pet_type: \"Cat\"}"
stacktrace:
test/open_api_spex_one_of_demo/pet_plug_test.exs:19: (test)
I also have a version of the schemas using a Pet
parent schema that can be found on the all-of
branch:
defmodule OpenApiSpexOneOfDemo.Schemas do
defmodule Pet do
require OpenApiSpex
alias OpenApiSpex.{Schema, Discriminator}
OpenApiSpex.schema(%{
title: "Pet",
type: :object,
properties: %{
pet_type: %Schema{type: :string}
},
required: [:pet_type],
discriminator: %Discriminator{
propertyName: "pet_type"
}
})
end
defmodule Cat do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Cat",
type: :object,
allOf: [
Pet,
%Schema{
type: :object,
properties: %{
pet_type: %Schema{type: :string},
meow: %Schema{type: :string}
},
required: [:meow]
}
]
})
end
defmodule Dog do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Dog",
type: :object,
allOf: [
Pet,
%Schema{
type: :object,
properties: %{
pet_type: %Schema{type: :string},
bark: %Schema{type: :string}
},
required: [:bark]
}
]
})
end
end
This schema results in the same behavior.
Thanks in advance for your help
Best โจ
Currently this is not defined anywhere that module passed to the OpenApiSpex.Plug.PutApiSpec
should define spec/0
, this should be defined by behaviour or it should accept MFA tuple to specify used function directly.
Within the docs the open_api_operation/1
function creates an atom :"#{action}_operation"
which is invoked during the request execution.
This is a potential exploit vector (as atoms are not gc'd)
Sobelow highlighted this as follows:
##############################################
# #
# Running Sobelow - v0.7.7 #
# Created by Griffin Byatt - @griffinbyatt #
# NCC Group - https://nccgroup.trust #
# #
##############################################
Unsafe atom interpolation - Low Confidence
File: apps/bond_web/lib/bond_web/schemas/integrations.ex - open_api_operation:99
Variable: action
I suggest the documentation is updated to have an example like the following:
@spec open_api_operation(any) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
Even better would be guidance on what to do if the operation is not defined, maybe a nice error message to aid the developer?
Using https://editor.swagger.io/ I just caught a few errors that so far had gone unnoticed. I had a few routes with path parameters (e.g. get("/dota2/abilities/:dota2_ability_id", Dota2.AbilityController, :show)
) but I forgot to declare :dota2_ability_id
in the parameters list. Here it is, commented out:
@spec open_api_operation(atom()) :: Op.t()
# /dota2/abilities/:dota2_ability_id
def open_api_operation(:show),
do: %Op{
# parameters: [OpenAPI.param(:dota2_ability_id)],
responses: OpenAPI.resp_Dota2Ability()
}
Could we add some check that makes the ....OpenAPI.spec()
call fail if path parameters are missing?
I'm not sure what could be done for missing parameters of other kinds.
It's unclear to me if this is supposed to be usable in a pure plug project, which does not use phoenix. Will it work? If so how?
I've tried adapting the examples in the README but unsuccessfully...
If in example of a Schema the map contains atom keys and not string keys,
string format validation is skipped.
See https://github.com/slavo2/phoenix-sample/blob/master/lib/sample/schemas.ex
and https://github.com/slavo2/phoenix-sample/blob/master/test/sample/schemas_test.exs
Note: Seems that in case of mixed map containing string and atom keys, format validation is skipped for atom keys only.
Hello mbuhot,
thanks for the latest updates. They look very promising. Could you please release a new version on hexdocs to make them available to us? Or is there anything left to do before you can do so?
Greetings Thomas
Hey there! Love this project! Im currently trying to migrate my teamโs elixir swagger docs from swagger format 2.0 to 3.0. (All of the rest of our teams use OpenApi 3.0 in their Rails apps)
My project also conforms to JSON API Schema. The old tool we were using (phoenix_swagger) had JSONAPI helpers. Ex: https://hexdocs.pm/phoenix_swagger/json-api-helpers.html#content
They donโt support openapi 3.0 though, which is why we want to start using this library. JSONAPI helpers to make writing these Schemas smoother would be a great addition.
Is that something that could be added to this project?
Hello @mbuhot ,
i already found another small problem. Basically, we are defining the spec just as you describe in the README.
defmodule UnifysellApiWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Server, Info, Paths}
@spec spec :: any
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(UnifysellApiWeb.Endpoint, otp_app: :unifysell_api)
],
info: %Info{
title: "UnifysellApi",
version: "0.0.1"
},
# populate the paths from a phoenix router
paths: Paths.from_router(UnifysellApiWeb.Router),
components: %OpenApiSpex.Components{
securitySchemes: %{
OAuthBearer: %OpenApiSpex.SecurityScheme{
type: "http",
scheme: "bearer",
bearerFormat: "JWT"
}
}
}
}
# discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end
We added a definition for an OAuth security scheme. The problem now is, that dialyzer is reporting the following warning: (i inserted the linebreak for better readability)
lib/unifysell_api_web/api_spec.ex:40: The call 'Elixir.OpenApiSpex':resolve_schema_modules
(#{'__struct__':='Elixir.OpenApiSpex.OpenApi', 'components':=#{'__struct__':='Elixir.OpenApiSpex.Components', 'callbacks':='nil', 'examples':='nil', 'headers':='nil', 'links':='nil', 'parameters':='nil', 'requestBodies':='nil', 'responses':='nil', 'schemas':='nil', 'securitySchemes':=#{'OAuthBearer2xx':=#{'__struct__':='Elixir.OpenApiSpex.SecurityScheme', 'bearerFormat':='nil', 'description':='nil', 'flows':='nil', 'in':=<<_:48>>, 'name':=<<_:104>>, 'openIdConnectUrl':='nil', 'scheme':='nil', 'type':=<<_:48>>}, 'OAuthBearer300':=#{'__struct__':='Elixir.OpenApiSpex.SecurityScheme', 'bearerFormat':=<<_:24>>, 'description':='nil', 'flows':='nil', 'in':='nil', 'name':='nil', 'openIdConnectUrl':='nil', 'scheme':=<<_:48>>, 'type':=<<_:32>>}}}, 'externalDocs':='nil', 'info':=#{'__struct__':='Elixir.OpenApiSpex.Info', 'contact':='nil', 'description':='nil', 'license':='nil', 'termsOfService':='nil', 'title':=<<_:96>>, 'version':=<<_:40>>}, 'openapi':=<<_:40>>, 'paths':=#{binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'description':=binary(), 'get':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'head':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'options':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'parameters':=[#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>#{binary()=>map()}, 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>#{binary()=>map()}, 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Schema', '$ref'=>binary(), 'additionalProperties'=>atom() | map(), 'allOf'=>'nil' | [any()], 'anyOf'=>'nil' | [any()], 'default'=>_, 'deprecated'=>'false' | 'nil' | 'true', 'description'=>binary(), 'discriminator'=>'nil' | map(), 'enum'=>'nil' | [any()], 'example'=>_, 'exclusiveMaximum'=>'false' | 'nil' | 'true', 'exclusiveMinimum'=>'false' | 'nil' | 'true', 'externalDocs'=>'nil' | map(), 'format'=>'nil' | binary(), 'items'=>atom() | map(), 'maxItems'=>'nil' | integer(), 'maxLength'=>'nil' | integer(), 'maxProperties'=>'nil' | integer(), 'maximum'=>'nil' | number(), 'minItems'=>'nil' | integer(), 'minLength'=>'nil' | integer(), 'minProperties'=>'nil' | integer(), 'minimum'=>'nil' | number(), 'multipleOf'=>'nil' | number(), 'not'=>atom() | map(), 'nullable'=>'false' | 'nil' | 'true', 'oneOf'=>'nil' | [any()], 'pattern'=>'nil' | binary() | map(), 'properties'=>'nil' | map(), 'readOnly'=>'false' | 'nil' | 'true', 'required'=>'nil' | [any()], 'title'=>binary(), 'type'=>atom(), 'uniqueItems'=>'false' | 'nil' | 'true', 'writeOnly'=>'false' | 'nil' | 'true', 'x-struct'=>atom(), 'xml'=>'nil' | map()}, 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'patch':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'post':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'put':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=#{binary()=>map()}}], 'summary':=binary(), 'trace':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}}}, 'security':=[], 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil', 'url':=<<_:32,_:_*8>>, 'variables':=#{}},...], 'tags':=[]})
will never return since it differs in the 1st argument from the success typing arguments:
(#{'__struct__':='Elixir.OpenApiSpex.OpenApi', 'components':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Components', 'callbacks':=#{binary()=>map()}, 'examples':=#{binary()=>map()}, 'headers':=#{binary()=>map()}, 'links':=#{binary()=>map()}, 'parameters':=#{binary()=>map()}, 'requestBodies':=#{binary()=>map()}, 'responses':=#{binary()=>map()}, 'schemas':=#{binary()=>map()}, 'securitySchemes':=#{binary()=>map()}}, 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'info':=#{'__struct__':='Elixir.OpenApiSpex.Info', 'contact':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Contact', 'email':=binary(), 'name':=binary(), 'url':=binary()}, 'description':='nil' | binary(), 'license':='nil' | #{'__struct__':='Elixir.OpenApiSpex.License', 'name':=binary(), 'url':=binary()}, 'termsOfService':='nil' | binary(), 'title':=binary(), 'version':=binary()}, 'openapi':=binary(), 'paths':=#{binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), 'options':=map(), 'parameters':=[any()], 'patch':=map(), 'post':=map(), 'put':=map(), 'servers':=[any()], 'summary':=binary(), 'trace':=map()}}, 'security':=[#{binary()=>[any()]}], 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'tags':=[#{'__struct__':='Elixir.OpenApiSpex.Tag', 'description':=binary(), 'externalDocs':=map(), 'name':=binary()}]})
The problem is, that we are inserting an incomplete %OpenApiSpex.Components{}
struct, that is merged with the schemas definition later. Do you have any ideas how we could handle that?
Hello again @mbuhot ,
we are running into a problem using the the validation plug.
We are defining a schema as request body:
def mark_as_shipped_operation do
%Operation{
...
requestBody:
Operation.request_body(
"the request body object",
"application/json",
Schemas.OrderMarkAsShippedRequest,
required: false
),
...
}
end
While the validate plug expects a map, it gets a struct in this situation. This leads to the following error, since struct[key]
access is not possible on a struct:
test update order valid mark order shipped (UnifysellApiWeb.OrderControllerTest)
test/unifysell_api_web/controllers/order_controller_test.exs:79
** (UndefinedFunctionError) function UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest.fetch/2 is undefined (UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest does not implement the Access behaviour)
code: conn = put conn, "/api/order/1/mark-as-shipped", ~S({"labelId": 1})
stacktrace:
(unifysell_api) UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest.fetch(%{__struct__: UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest, labelId: 1, order_id: 1}, :order_id)
(elixir) lib/access.ex:308: Access.get/3
(open_api_spex) lib/open_api_spex/operation.ex:179: OpenApiSpex.Operation.validate_parameter_schemas/3
(open_api_spex) lib/open_api_spex/operation.ex:156: OpenApiSpex.Operation.validate/4
(open_api_spex) lib/open_api_spex/plug/validate.ex:33: OpenApiSpex.Plug.Validate.call/2
(unifysell_api) lib/unifysell_api_web/controllers/order_controller.ex:1: UnifysellApiWeb.OrderController.phoenix_controller_pipeline/2
(unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
(unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.plug_builder_call/2
(unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
test/unifysell_api_web/controllers/order_controller_test.exs:93: (test)
Updating schema works, but of course is not a good solution, But we get an idea why the error occurs.:
defmodule OrderMarkAsShippedRequest do
def fetch(a, b) do
Map.fetch(a, b)
end
OpenApiSpex.schema(%{...})
end
We could update the validate_parameter_schemas(...)
function like this:
@spec validate_parameter_schemas([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp validate_parameter_schemas([], params, _schemas), do: {:ok, params}
defp validate_parameter_schemas(param_list, %_{} = params, schemas) do
validate_parameter_schemas(param_list, Map.from_struct(params), schemas)
end
defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
with :ok <- Schema.validate(Parameter.schema(p), params[p.name], schemas),
{:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
{:ok, Map.delete(remaining, p.name)}
end
end
We could also update it like this:
@spec validate_parameter_schemas([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp validate_parameter_schemas([], params, _schemas), do: {:ok, params}
defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
with :ok <- Schema.validate(Parameter.schema(p), Map.fetch(params, p.name), schemas),
{:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
{:ok, Map.delete(remaining, p.name)}
end
end
If one of these solutions is okay to you, just tell me and i will create a pull request. Or maybe we are doing something else wrong?
Greetings, Thomas
Fields with a nil value are stripped out by the protocol implementation of the Jason/Poison encoder for %OpenApi{}
.
The examples are also affected by this. For instance, the :example
field in a %MediaType{}
. Shouldn't the examples be kept intact ?
I started to work on a fix: https://github.com/supermaciz/open_api_spex/tree/fix/keep_examples_null_fields
From #42
enums validation: cast does not "atomify" keys in maps of enum payloads. In other words, the following code does not crash:
schema = %Schema{enum: [%{id: 42}]}
spec = ...
# string keys
{:ok, data} = OpenApiSpex.cast(spec, schema, %{"id" => 42})
{:error, _} = OpenApiSpex.validate(spec, schema, data)
# atom keys
{:ok, data} = OpenApiSpex.cast(spec, schema, %{id: 42})
:ok = OpenApiSpex.validate(spec, schema, data)
I think it would be very dangerous (OOM) to atomify enum keys anyway!
My current workaround is to specify enum schemas with string keys instead, which can be easily missed by anyone.
So how about throwing an error when creating a schema that has atom keys in enum: ...?
Instead of manually implementing endpoint response serialization, why not leverage OpenApiSpex to do it? We've been using ja_serializer for our json:api endpoints, but it's kind of a pain, and it duplicates what's already in our OpenApiSpex specifications.
Hey There,
It looks like there is an issue with the allOf
casting mechanism.
The main issue is, that the caster will return the error :unexpected_field
since the payload will mostly provide some files which are not defined in every schema of allOf.
As an example:
schema = %Schema{
title: "demo",
type: :object,
allOf: [
MyApp.Schemas.GenericRequest,
%Schema{
properties: %{
bar: %Schema{type: :string}
}
}
]
}
Let's just assume that GenericRequest has something defined like id: %Schema{type: string}
and the request looks something like this: {"id": "1231-123155-1231-12312", "bar": "whatever"}
Based on https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/cast/all_of.ex#L8, the caster will first try to cast everything from the request on the first schema, but since I've only defined the id and not the bar in the first one, this will always result in a :unexpected_field
error
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.