Skip to content
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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions docs/reusing-schemas.md
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"
Copy link

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?

Copy link
Author

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

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. This module has to implement `PhoenixSwagger`
behaviour which understands `phoenix_swagger` macros for defining schemas.
char0n marked this conversation as resolved.
Show resolved Hide resolved

```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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These very general schemas could be added to the MyAppWeb.Router.swagger_info callback.

Copy link
Author

Choose a reason for hiding this comment

The 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(%{
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
}

Copy link
Author

Choose a reason for hiding this comment

The 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 Address you have to include all other general common schemas, that Address schema is using. Otherwise they're not gonna appear in generate swagger.json.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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
```