-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document reusing schemas #244
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
# Reusing swagger schemas | ||
|
||
When building an API it can be common to have schemas that are common to multiple actions. | ||
|
||
## Scenario | ||
|
||
For example, let's say we've got two endpoints: | ||
- `GET /projects` | ||
- `GET /books` | ||
|
||
Both of these endpoints can have an erroneous responses like `401 Unauthorized`. | ||
We don't want to define our new `Error` schema for both of these endpoints. This will create | ||
duplicate code and we just want to keep our code [DRY](https://cs.wikipedia.org/wiki/Don%27t_repeat_yourself). | ||
|
||
Here's what the swagger spec in our `ProjectsController` and `BookController` would look like: | ||
|
||
```elixir | ||
defmodule ProjectsController do | ||
use HelloWeb, :controller | ||
use PhoenixSwagger | ||
|
||
swagger_path :index do | ||
get "/projects" | ||
produces "application/json" | ||
parameter("Authorization", :header, :string, "OAuth2 access token", required: true) | ||
parameters do | ||
sort_by :query, :string, "The property to sort by" | ||
sort_direction :query, :string, "The sort direction", enum: [:asc, :desc], default: :asc | ||
company_id :string, :query, "The company id" | ||
end | ||
response(200, "OK", Schema.ref(:ListOfProjects)) | ||
response(401, "Unauthorized", Schema.ref(:Error)) | ||
end | ||
|
||
|
||
|
||
@doc false | ||
def swagger_definitions do | ||
%{ | ||
ListOfProjects: ...schema definition..., | ||
Error: | ||
swagger_schema do | ||
properties do | ||
code(:string, "Error code", required: true) | ||
message(:string, "Error message", required: true) | ||
end | ||
end | ||
} | ||
end | ||
end | ||
``` | ||
|
||
```elixir | ||
defmodule BooksController do | ||
use HelloWeb, :controller | ||
use PhoenixSwagger | ||
|
||
swagger_path :index do | ||
get "/books" | ||
produces "application/json" | ||
parameter("Authorization", :header, :string, "OAuth2 access token", required: true) | ||
parameters do | ||
sort_by :query, :string, "The property to sort by" | ||
sort_direction :query, :string, "The sort direction", enum: [:asc, :desc], default: :asc | ||
company_id :string, :query, "The company id" | ||
end | ||
response(200, "OK", Schema.ref(:ListOfBooks)) | ||
response(401, "Unauthorized", Schema.ref(:Error)) | ||
end | ||
|
||
|
||
|
||
@doc false | ||
def swagger_definitions do | ||
%{ | ||
ListOfBooks: ...schema definition..., | ||
Error: | ||
swagger_schema do | ||
properties do | ||
code(:string, "Error code", required: true) | ||
message(:string, "Error message", required: true) | ||
end | ||
end | ||
} | ||
end | ||
end | ||
``` | ||
|
||
Our ProjectsController and BooksControllers have now identical `Error` schema defined in their modules. | ||
|
||
|
||
## Extracting schemas into a module for reuse | ||
|
||
We can easily extract common schemas into an ordinary elixir module. | ||
We make the phoenix_swagger macro DSL available with `use PhoenixSwagger`. | ||
|
||
```elixir | ||
defmodule CommonSchemas do | ||
@moduledoc "Common schema declarations for phoenix swagger" | ||
|
||
use PhoenixSwagger | ||
|
||
@doc """ | ||
Returns map of common swagger definitions merged with the map of provided schema definitions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These very general schemas could be added to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you pls elaborate more or provide a concrete code example ? |
||
|
||
The common definitions (data structures) are not specific to any controller or | ||
business domain logic. | ||
""" | ||
def create_swagger_definitions(%{} = schemas) do | ||
Map.merge( | ||
%{ | ||
Error: | ||
swagger_schema do | ||
properties do | ||
code(:string, "Error code", required: true) | ||
message(:string, "Error message", required: true) | ||
end | ||
end, | ||
Errors: | ||
swagger_schema do | ||
properties do | ||
errors( | ||
Schema.new do | ||
title("Errors") | ||
description("A collection of Errors") | ||
type(:array) | ||
items(Schema.ref(:Error)) | ||
end | ||
) | ||
end | ||
end | ||
}, | ||
schemas | ||
) | ||
end | ||
end | ||
``` | ||
|
||
As you can see, it is also possible to reference other common schemas defined inside the `create_swagger_definitions/1`. | ||
|
||
## Reusing the common schemas | ||
|
||
Now, instead of defining the `Error` schema in every controller, we can just use `create_swagger_definitions/1` | ||
inside our controllers. | ||
|
||
```elixir | ||
defmodule ProjectsController do | ||
import CommonSchemas, only: [create_swagger_definitions: 1] | ||
|
||
use HelloWeb, :controller | ||
use PhoenixSwagger | ||
|
||
swagger_path :index do | ||
get "/projects" | ||
produces "application/json" | ||
parameter("Authorization", :header, :string, "OAuth2 access token", required: true) | ||
parameters do | ||
sort_by :query, :string, "The property to sort by" | ||
sort_direction :query, :string, "The sort direction", enum: [:asc, :desc], default: :asc | ||
company_id :string, :query, "The company id" | ||
end | ||
response(200, "OK", Schema.ref(:ListOfProjects)) | ||
response(401, "Unauthorized", Schema.ref(:Error)) | ||
end | ||
|
||
|
||
|
||
@doc false | ||
def swagger_definitions do | ||
create_swagger_definitions(%{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is a simpler way to include specific schemas by putting each shared schema in its own module and including it in the map: %{
ListOfProjects: ... schema definitions ...
Address: MyAppWeb.Schemas.Adress.schema()
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes this is something that I did from be beginning. But the problem was that along with |
||
ListOfProjects: ...schema definition... | ||
}) | ||
end | ||
end | ||
``` | ||
|
||
```elixir | ||
defmodule BooksController do | ||
import CommonSchemas, only: [create_swagger_definitions: 1] | ||
|
||
use HelloWeb, :controller | ||
use PhoenixSwagger | ||
|
||
swagger_path :index do | ||
get "/books" | ||
produces "application/json" | ||
parameter("Authorization", :header, :string, "OAuth2 access token", required: true) | ||
parameters do | ||
sort_by :query, :string, "The property to sort by" | ||
sort_direction :query, :string, "The sort direction", enum: [:asc, :desc], default: :asc | ||
company_id :string, :query, "The company id" | ||
end | ||
response(200, "OK", Schema.ref(:ListOfBooks)) | ||
response(401, "Unauthorized", Schema.ref(:Error)) | ||
end | ||
|
||
|
||
|
||
@doc false | ||
def swagger_definitions do | ||
create_swagger_definitions(%{ | ||
ListOfBooks: ...schema definition... | ||
}) | ||
end | ||
end | ||
``` | ||
|
||
To avoid importing `create_swagger_definitions/1` in every controller, find a `HelloWeb` module and add an import | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This advice feels a bit opinionated. Users are free to organise their projects and code as they like. My preference is to delete this file for API projects trading off some dry-ness for explicitness of imports. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I agree, this is my opinionated hint how to use something that pre-generated phoenix project already contains. Should we remove this section then ? |
||
inside the `controller/0` function. This module will be called differently in your project depending on the the name of your | ||
Phoenix project: `<PhoenixProjectName>Web`. This file is the entrypoint for defining your web interface and is part | ||
of every Phoenix installation. | ||
|
||
|
||
```elixir | ||
defmodule HelloWeb do | ||
@moduledoc """ | ||
The entrypoint for defining your web interface, such | ||
as controllers, views, channels and so on. | ||
|
||
This can be used in your application as: | ||
|
||
use HelloWeb, :controller | ||
use HelloWeb, :view | ||
|
||
The definitions below will be executed for every view, | ||
controller, etc, so keep them short and clean, focused | ||
on imports, uses and aliases. | ||
|
||
Do NOT define functions inside the quoted expressions | ||
below. Instead, define any helper function in modules | ||
and import those modules here. | ||
""" | ||
|
||
def controller do | ||
quote do | ||
use Phoenix.Controller, namespace: HelloWeb | ||
|
||
import Plug.Conn | ||
import CommonSchemas, only: [create_swagger_definitions: 1] | ||
|
||
alias HelloWeb.Router.Helpers, as: Routes | ||
end | ||
end | ||
end | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does location and type differ between line 28 & 29?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example was directly copied from this location of the official documentation: https://hexdocs.pm/phoenix_swagger/reusing-swagger-parameters.html#content