Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 84e5bbe..f65ad4e 100644
--- a/README.md
+++ b/README.md
@@ -1,300 +1,302 @@
# Open API Spex
[![Build Status](https://travis-ci.com/open-api-spex/open_api_spex.svg?branch=master)](https://travis-ci.com/open-api-spex/open_api_spex)
[![Hex.pm](https://img.shields.io/hexpm/v/open_api_spex.svg)](https://hex.pm/packages/open_api_spex)
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
- Generate and serve a JSON Open Api Spec document from your code
- Use the spec to cast request params to well defined schema structs
- Validate params against schemas, eliminate bad requests before they hit your controllers
- Validate responses against schemas in tests, ensuring your docs are accurate and reliable
- Explore the API interactively with with [SwaggerUI](https://swagger.io/swagger-ui/)
Full documentation available on [hexdocs](https://hexdocs.pm/open_api_spex/)
## Installation
The package can be installed by adding `open_api_spex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:open_api_spex, "~> 3.2"}
]
end
```
## Generate Spec
Start by adding an `ApiSpec` module to your application to populate an `OpenApiSpex.OpenApi` struct.
```elixir
defmodule MyApp.ApiSpec do
alias OpenApiSpex.{OpenApi, Server, Info, Paths}
+ @behaviour OpenApi
+ @impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(MyAppWeb.Endpoint, otp_app: :my_app)
],
info: %Info{
title: "My App",
version: "1.0"
},
# populate the paths from a phoenix router
paths: Paths.from_router(MyAppWeb.Router)
}
|> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
end
end
```
For each plug (controller) that will handle api requests, add an `open_api_operation` callback.
It will be passed the plug opts that were declared in the router, this will be the action for a phoenix controller. The callback populates an `OpenApiSpex.Operation` struct describing the plug/action.
```elixir
defmodule MyApp.UserController do
alias OpenApiSpex.Operation
alias MyApp.Schemas.UserResponse
@spec open_api_operation(any) :: Operation.t
def open_api_operation(action), do: apply(__MODULE__, :"#{action}_operation", [])
@spec show_operation() :: Operation.t
def show_operation() do
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
Operation.parameter(:id, :path, :integer, "User ID", example: 123)
],
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
}
end
def show(conn, %{"id" => id}) do
{:ok, user} = MyApp.Users.find_by_id(id)
json(conn, 200, user)
end
end
```
Declare the JSON schemas for request/response bodies in a `Schemas` module:
Each module should implement the `OpenApiSpex.Schema` behaviour.
The only callback is `schema/0`, which should return an `OpenApiSpex.Schema` struct.
You may optionally declare a struct, linked to the JSON schema through the `x-struct` extension property.
See `OpenApiSpex.schema/1` macro for a convenient way to reduce some boilerplate.
```elixir
defmodule MyApp.Schemas do
alias OpenApiSpex.Schema
defmodule User do
@behaviour OpenApiSpex.Schema
@derive [Jason.Encoder]
@schema %Schema{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name"},
email: %Schema{type: :string, description: "Email address", format: :email},
birthday: %Schema{type: :string, description: "Birth date", format: :date},
inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
},
required: [:name, :email],
example: %{
"id" => 123,
"name" => "Joe",
"email" => "joe@gmail.com"
},
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
end
defmodule UserResponse do
require OpenApiSpex
# OpenApiSpex.schema/1 macro can be optionally used to reduce boilerplate code
OpenApiSpex.schema %{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
end
end
```
Now you can create a mix task to write the swagger file to disk:
```elixir
defmodule Mix.Tasks.MyApp.OpenApiSpec do
def run([output_file]) do
json =
MyApp.ApiSpec.spec()
|> Jason.encode!(pretty: true)
:ok = File.write!(output_file, json)
end
end
```
Generate the file with: `mix myapp.openapispec spec.json`
## Serve Spec
To serve the API spec from your application, first add the `OpenApiSpex.Plug.PutApiSpec` plug somewhere in the pipeline.
```elixir
pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: MyApp.ApiSpec
end
```
Now the spec will be available for use in downstream plugs.
The `OpenApiSpex.Plug.RenderSpec` plug will render the spec as JSON:
```elixir
scope "/api" do
pipe_through :api
resources "/users", MyApp.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Serve Swagger UI
Once your API spec is available through a route, the `OpenApiSpex.Plug.SwaggerUI` plug can be used to serve a SwaggerUI interface. The `path:` plug option must be supplied to give the path to the API spec.
All JavaScript and CSS assets are sourced from cdnjs.cloudflare.com, rather than vendoring into this package.
```elixir
scope "/" do
pipe_through :browser # Use the default browser stack
get "/", MyAppWeb.PageController, :index
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
scope "/api" do
pipe_through :api
resources "/users", MyAppWeb.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Validating and Casting Params
Add the `OpenApiSpex.Plug.CastAndValidate` plug to a controller to validate request parameters, and to cast to Elixir types defined by the operation schema.
```elixir
plug OpenApiSpex.Plug.CastAndValidate, operation_id: "UserController.show"
```
The `operation_id` can be inferred when used from a Phoenix controller from the contents of `conn.private`.
```elixir
defmodule MyApp.UserController do
use MyAppWeb, :controller
alias OpenApiSpex.Operation
alias MyApp.Schemas.{User, UserRequest, UserResponse}
plug OpenApiSpex.Plug.CastAndValidate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def create_operation do
import Operation
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [
parameter(:id, :query, :integer, "user ID")
],
requestBody: request_body("The user attributes", "application/json", UserRequest),
responses: %{
201 => response("User", "application/json", UserResponse)
}
}
end
def create(conn = %{body_params: %UserRequest{user: %User{name: name, email: email, birthday: birthday = %Date{}}}}, %{id: id}) do
# conn.body_params cast to UserRequest struct
# conn.params.id cast to integer
end
end
```
Now the client will receive a 422 response whenever the request fails to meet the validation rules from the api spec.
The response body will include the validation error message:
```json
{
"errors": [
{
"message": "Invalid format. Expected :date",
"source": {
"pointer": "/data/birthday"
},
"title": "Invalid value"
}
]
}
```
See also `OpenApiSpex.cast_and_validate/3` and `OpenApiSpex.Cast.cast/3` for more examples outside of a `plug` pipeline.
## Validate Examples
As schemas evolve, you may want to confirm that the examples given match the schemas.
Use the `OpenApiSpex.Test.Assertions` module to assert on schema validations.
```elixir
Use ExUnit.Case
import OpenApiSpex.Test.Assertions
test "UsersResponse example matches schema" do
api_spec = MyApp.ApiSpec.spec()
schema = MyApp.Schemas.UsersResponse.schema()
assert_schema(schema.example, "UsersResponse", api_spec)
end
```
## Validate Responses
API responses can be tested against schemas using `OpenApiSpex.Test.Assertions` also:
```elixir
use MyApp.ConnCase
import OpenApiSpex.Test.Assertions
test "UserController produces a UsersResponse", %{conn: conn} do
api_spec = MyApp.ApiSpec.spec()
json =
conn
|> get(user_path(conn, :index))
|> json_response(200)
assert_schema(json, "UsersResponse", api_spec)
end
```
diff --git a/examples/phoenix_app/lib/phoenix_app_web/api_spec.ex b/examples/phoenix_app/lib/phoenix_app_web/api_spec.ex
index 4ed0090..265db6b 100644
--- a/examples/phoenix_app/lib/phoenix_app_web/api_spec.ex
+++ b/examples/phoenix_app/lib/phoenix_app_web/api_spec.ex
@@ -1,14 +1,16 @@
defmodule PhoenixAppWeb.ApiSpec do
alias OpenApiSpex.{Info, OpenApi, Paths}
+ @behaviour OpenApi
+ @impl OpenApi
def spec do
%OpenApi{
info: %Info{
title: "Phoenix App",
version: "1.0"
},
paths: Paths.from_router(PhoenixAppWeb.Router)
}
|> OpenApiSpex.resolve_schema_modules()
end
-end
\ No newline at end of file
+end
diff --git a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
index 5a30f53..a5da601 100644
--- a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
+++ b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
@@ -1,82 +1,82 @@
defmodule PhoenixAppWeb.UserController do
use PhoenixAppWeb, :controller
import OpenApiSpex.Operation, only: [parameter: 5, request_body: 4, response: 3]
alias OpenApiSpex.Operation
alias PhoenixApp.{Accounts, Accounts.User}
alias PhoenixAppWeb.Schemas
plug(OpenApiSpex.Plug.Cast)
plug(OpenApiSpex.Plug.Validate)
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def index_operation() do
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.json", users: users)
end
def create_operation() do
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [
Operation.parameter(:group_id, :path, :integer, "Group ID", example: 1)
],
requestBody:
request_body("The user attributes", "application/json", Schemas.UserRequest,
required: true
),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
- def create(conn = %{body_params: %Schemas.UserRequest{user: user_params}}, %{group_id: group_id}) do
+ def create(conn = %{body_params: %Schemas.UserRequest{user: user_params}}, %{group_id: _group_id}) do
with {:ok, %User{} = user} <- Accounts.create_user(user_params) do
conn
|> put_status(:created)
|> put_resp_header("location", user_path(conn, :show, user))
|> render("show.json", user: user)
end
end
@doc """
API Spec for :show action
"""
def show_operation() do
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123, minimum: 1)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def show(conn, %{id: id}) do
user = Accounts.get_user!(id)
render(conn, "show.json", user: user)
end
end
diff --git a/examples/plug_app/lib/plug_app/api_spec.ex b/examples/plug_app/lib/plug_app/api_spec.ex
index b8ce9f7..09c77eb 100644
--- a/examples/plug_app/lib/plug_app/api_spec.ex
+++ b/examples/plug_app/lib/plug_app/api_spec.ex
@@ -1,22 +1,24 @@
defmodule PlugApp.ApiSpec do
- alias OpenApiSpex.{Info, OpenApi, Paths}
+ alias OpenApiSpex.{Info, OpenApi}
+ @behaviour OpenApi
+ @impl OpenApi
def spec do
%OpenApi{
info: %Info{
title: "Plug App",
version: "1.0"
},
paths: %{
"/api/users/{id}" => OpenApiSpex.PathItem.from_routes([
%{verb: :get, plug: PlugApp.UserHandler.Show, opts: []}
]),
"/api/users" => OpenApiSpex.PathItem.from_routes([
%{verb: :get, plug: PlugApp.UserHandler.Index, opts: []},
%{verb: :post, plug: PlugApp.UserHandler.Create, opts: []}
])
}
}
|> OpenApiSpex.resolve_schema_modules()
end
end
diff --git a/lib/open_api_spex/open_api.ex b/lib/open_api_spex/open_api.ex
index a9c15ba..0e18636 100644
--- a/lib/open_api_spex/open_api.ex
+++ b/lib/open_api_spex/open_api.ex
@@ -1,74 +1,99 @@
-defmodule OpenApiSpex.OpenApi do
+defmodule OpenApiSpex.OpenApi do
@moduledoc """
- Defines the `OpenApiSpex.OpenApi.t` type.
+ Defines the `OpenApiSpex.OpenApi.t` type and the behaviour for application modules that
+ construct an `OpenApiSpex.OpenApi.t` at runtime.
"""
alias OpenApiSpex.{
Info, Server, Paths, Components,
SecurityRequirement, Tag, ExternalDocumentation,
OpenApi
}
@enforce_keys [:info, :paths]
defstruct [
openapi: "3.0.0",
info: nil,
servers: [],
paths: nil,
components: nil,
security: [],
tags: [],
externalDocs: nil
]
@typedoc """
[OpenAPI Object](https://swagger.io/specification/#oasObject)
This is the root document object of the OpenAPI document.
"""
@type t :: %OpenApi{
openapi: String.t,
info: Info.t,
servers: [Server.t] | nil,
paths: Paths.t,
components: Components.t | nil,
security: [SecurityRequirement.t] | nil,
tags: [Tag.t] | nil,
externalDocs: ExternalDocumentation.t | nil
}
+ @doc """
+ A spec/0 callback function is required for use with the `OpenApiSpex.Plug.PutApiSpec` plug.
+
+ ## Example
+
+ @impl OpenApiSpex.OpenApi
+ def spec do
+ %OpenApi{
+ servers: [
+ # Populate the Server info from a phoenix endpoint
+ Server.from_endpoint(MyAppWeb.Endpoint, otp_app: :my_app)
+ ],
+ info: %Info{
+ title: "My App",
+ version: "1.0"
+ },
+ # populate the paths from a phoenix router
+ paths: Paths.from_router(MyAppWeb.Router)
+ }
+ |> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
+ end
+ """
+ @callback spec() :: t
+
@json_encoder Enum.find([Jason, Poison], &Code.ensure_loaded?/1)
def json_encoder, do: @json_encoder
for encoder <- [Poison.Encoder, Jason.Encoder] do
if Code.ensure_loaded?(encoder) do
defimpl encoder do
def encode(api_spec = %OpenApi{}, options) do
api_spec
|> to_json()
|> unquote(encoder).encode(options)
end
defp to_json(%Regex{source: source}), do: source
defp to_json(value = %{__struct__: _}) do
value
|> Map.from_struct()
|> to_json()
end
defp to_json(value) when is_map(value) do
value
|> Stream.map(fn {k,v} -> {to_string(k), to_json(v)} end)
|> Stream.filter(fn {_, nil} -> false; _ -> true end)
|> Enum.into(%{})
end
defp to_json(value) when is_list(value) do
Enum.map(value, &to_json/1)
end
defp to_json(nil), do: nil
defp to_json(true), do: true
defp to_json(false), do: false
defp to_json(value) when is_atom(value), do: to_string(value)
defp to_json(value), do: value
end
end
end
end
diff --git a/lib/open_api_spex/plug/put_api_spec.ex b/lib/open_api_spex/plug/put_api_spec.ex
index 94d8246..2c8d571 100644
--- a/lib/open_api_spex/plug/put_api_spec.ex
+++ b/lib/open_api_spex/plug/put_api_spec.ex
@@ -1,58 +1,62 @@
defmodule OpenApiSpex.Plug.PutApiSpec do
@moduledoc """
Module plug that calls a given module to obtain the Api Spec and store it as private in the Conn.
This allows downstream plugs to use the API spec for casting, validating and rendering.
+ ## Options
+
+ - module: A module implementing the `OpenApiSpex.OpenApi` behaviour
+
## Example
plug OpenApiSpex.Plug.PutApiSpec, module: MyAppWeb.ApiSpec
"""
@behaviour Plug
@cache OpenApiSpex.Plug.Cache.adapter()
@impl Plug
def init([module: _spec_module] = opts) do
opts[:module]
end
@impl Plug
def call(conn, spec_module) do
{spec, operation_lookup} =
case @cache.get(spec_module) do
nil ->
spec = build_spec(spec_module)
@cache.put(spec_module, spec)
spec
spec ->
spec
end
private_data =
conn
|> Map.get(:private)
|> Map.get(:open_api_spex, %{})
|> Map.put(:spec_module, spec_module)
|> Map.put(:spec, spec)
|> Map.put(:operation_lookup, operation_lookup)
Plug.Conn.put_private(conn, :open_api_spex, private_data)
end
@spec build_spec(module) :: {OpenApiSpex.OpenApi.t, %{String.t => OpenApiSpex.Operation.t}}
defp build_spec(mod) do
spec = mod.spec()
operation_lookup = build_operation_lookup(spec)
{spec, operation_lookup}
end
@spec build_operation_lookup(OpenApiSpex.OpenApi.t) :: %{String.t => OpenApiSpex.Operation.t}
defp build_operation_lookup(spec = %OpenApiSpex.OpenApi{}) do
spec
|> Map.get(:paths)
|> Stream.flat_map(fn {_name, item} -> Map.values(item) end)
|> Stream.filter(fn x -> match?(%OpenApiSpex.Operation{}, x) end)
|> Stream.map(fn operation -> {operation.operationId, operation} end)
|> Enum.into(%{})
end
end
diff --git a/test/operation2_test.exs b/test/operation2_test.exs
index 668521c..7a0918d 100644
--- a/test/operation2_test.exs
+++ b/test/operation2_test.exs
@@ -1,208 +1,211 @@
defmodule OpenApiSpex.Operation2Test do
use ExUnit.Case
alias OpenApiSpex.{Operation, Operation2, Schema}
alias OpenApiSpex.Cast.Error
defmodule SchemaFixtures do
@user %Schema{
type: :object,
properties: %{
user: %Schema{
type: :object,
properties: %{
email: %Schema{type: :string}
}
}
}
}
@user_list %Schema{
type: :array,
items: @user
}
@schemas %{"User" => @user, "UserList" => @user_list}
def user, do: @user
def user_list, do: @user_list
def schemas, do: @schemas
end
defmodule OperationFixtures do
@user_index %Operation{
operationId: "UserController.index",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name"),
Operation.parameter(:age, :query, :integer, "User's age")
],
responses: %{
200 => Operation.response("User", "application/json", SchemaFixtures.user())
}
}
def user_index, do: @user_index
@create_user %Operation{
operationId: "UserController.create",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name")
],
requestBody:
Operation.request_body("request body", "application/json", SchemaFixtures.user(),
required: true
),
responses: %{
200 => Operation.response("User list", "application/json", SchemaFixtures.user_list())
}
}
def create_user, do: @create_user
end
defmodule SpecModule do
+ @behaviour OpenApiSpex.OpenApi
+
+ @impl OpenApiSpex.OpenApi
def spec do
paths = %{
"/users" => %{
"post" => OperationFixtures.create_user()
}
}
%OpenApiSpex.OpenApi{
info: nil,
paths: paths,
components: %{
schemas: SchemaFixtures.schemas()
}
}
end
end
defmodule RenderError do
def init(_) do
nil
end
def call(_conn, _errors) do
raise "should not have errors"
end
end
describe "cast/4" do
test "cast request body" do
conn = create_conn(%{"user" => %{"email" => "foo@bar.com"}})
assert {:ok, conn} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SchemaFixtures.schemas()
)
assert %Plug.Conn{} = conn
end
test "cast request body - invalid data type" do
conn = create_conn(%{"user" => %{"email" => 123}})
assert {:error, errors} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SchemaFixtures.schemas()
)
assert [error] = errors
assert %Error{} = error
assert error.reason == :invalid_type
end
test "validate undefined query param name" do
query_params = %{"unknown" => "asdf"}
assert {:error, [error]} = do_index_cast(query_params)
assert %Error{} = error
assert error.reason == :unexpected_field
assert error.name == "unknown"
assert error.path == ["unknown"]
end
test "validate invalid data type for query param" do
query_params = %{"age" => "asdf"}
assert {:error, [error]} = do_index_cast(query_params)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.type == :integer
assert error.value == "asdf"
end
test "validate missing required query param" do
parameter =
Operation.parameter(:name, :query, :string, "Filter by user name", required: true)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{}, operation: operation)
assert %Error{} = error
assert error.reason == :missing_field
assert error.name == :name
end
test "validate invalid value for integer range" do
parameter =
Operation.parameter(
:age,
:query,
%Schema{type: :integer, minimum: 1, maximum: 99},
"Filter by user age",
required: true
)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{"age" => 100}, operation: operation)
assert %Error{} = error
assert error.reason == :maximum
assert {:error, [error]} = do_index_cast(%{"age" => 0}, operation: operation)
assert %Error{} = error
assert error.reason == :minimum
end
defp do_index_cast(query_params, opts \\ []) do
conn =
:get
|> Plug.Test.conn("/api/users?" <> URI.encode_query(query_params))
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> build_params()
operation = opts[:operation] || OperationFixtures.user_index()
Operation2.cast(
operation,
conn,
"application/json",
SchemaFixtures.schemas()
)
end
defp create_conn(body_params) do
:post
|> Plug.Test.conn("/api/users")
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> Map.put(:body_params, body_params)
|> build_params()
end
defp build_params(conn) do
params =
conn.path_params
|> Map.merge(conn.query_params)
|> Map.merge(conn.body_params)
%{conn | params: params}
end
end
end
diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex
index 9b6611a..85c82a1 100644
--- a/test/support/api_spec.ex
+++ b/test/support/api_spec.ex
@@ -1,34 +1,36 @@
defmodule OpenApiSpexTest.ApiSpec do
alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info, Components}
alias OpenApiSpexTest.{Router, Schemas}
+ @behaviour OpenApi
+ @impl OpenApi
def spec() do
%OpenApi{
servers: [
%Server{url: "http://example.com"},
],
info: %Info{
title: "A",
version: "3.0",
contact: %Contact{
name: "joe",
email: "Joe@gmail.com",
url: "https://help.joe.com"
},
license: %License{
name: "MIT",
url: "http://mit.edu/license"
}
},
components: %Components{
schemas:
for schemaMod <- [Schemas.Pet, Schemas.Cat, Schemas.Dog, Schemas.CatOrDog], into: %{} do
schema = schemaMod.schema()
{schema.title, schema}
end
},
paths: Paths.from_router(Router)
}
|> OpenApiSpex.resolve_schema_modules()
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 29, 11:47 AM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41207
Default Alt Text
(25 KB)

Event Timeline