An Elixir server implementation of OpenAPI Specification v3 within Plug
MIT License
Background
The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for REST APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interface descriptions have done for lower-level programming, the OpenAPI Specification removes guesswork in calling a service.
Oasis is:
Base on Plug
's implements, according to the OpenAPI Specification to generate server's router and the all well defined HTTP request handlers, since the OAS defines a detailed collection of data types by JSON Schema Specification, Oasis leverages this and focuses on the types conversion and validation to the parameters of the HTTP request.
Add oasis
as a dependency to your mix.exs
def deps do
[
{:oasis, "~> 0.3"}
]
end
Oasis does not cover the full OpenAPI specification, so far the implements contain:
Oasis.Plug.BearerAuth
for details.Oasis.Plug.HMACAuth
for details.We also have some OpenAPI Specification Extensions defined for use, please see our Specification Extensions Guide.
First, write your API document refer the OpenAPI Specification(see reference for details), we build Oasis with the version 3.1.0 of the OAS, as usual, the common use case should be covered for the version 3.0.*
of the OAS.
Here is a minimum specification for an example in YAML, we save it as "petstore-mini.yaml" in this tutorial.
openapi: "3.1.0"
info:
title: Petstore Mini
paths:
/pets:
get:
parameters:
- name: tags
in: query
required: false
schema:
type: array
items:
type: string
- name: limit
in: query
requried: false
schema:
type: integer
format: int32
response:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
post:
operationId: addPet
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
response:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
/pets/{id}:
get:
operationId: find pet by id
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
response:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64
NewPet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string
And then we run the following mix task to generate the corresponding files:
mix oas.gen.plug --file path/to/petstore-mini.yaml
We will see these output:
Generates Router and Plug modules from OAS
* creating lib/oasis/gen/router.ex
* creating lib/oasis/gen/pre_find_pet_by_id.ex
* creating lib/oasis/gen/find_pet_by_id.ex
* creating lib/oasis/gen/pre_add_pet.ex
* creating lib/oasis/gen/add_pet.ex
* creating lib/oasis/gen/pre_get_pets.ex
* creating lib/oasis/gen/get_pets.ex
The arguments of oas.gen.plug
mix task:
--file
, required, the completed path to the specification file in YAML or JSON format.--router
, optional, the generated router's module alias, by default it is Router
(the full module name is Oasis.Gen.Router
by default), for example we set --router Hello.MyRouter
meanwhile there is no other special name space defined, the final router module is Oasis.Gen.Hello.MyRouter
in /lib/oasis/gen/hello/my_router.ex
path.--name-space
, optional, the generated all modules' name space, by default it is Oasis.Gen
, this argument will always override the name space from the input --file
if any "x-oasis-name-space"
field(s) defined.Refer the OAS, the operationId
field of Operation Object is not required, but it should be unique among all operations described in the API if operationId
field exists.
find_pet_by_id.ex
file with Oasis.Gen.FindPetById
module name.get_pets.ex
file with Oasis.Gen.GetPets
module name.The generated codes are always put in the lib
directory of your application root path, because they need to be integrated into your application in runtime.
By default, the name space of the generated module is Oasis.Gen
and its folder path is lib/oasis/gen
, but we can custom it use these ways:
oas.gen.plug
mix task, input --name-space
argument to set this, this argument always overrides all name space even we have some sections of document have special definition (via "x-oasis-name-space"
), for example:mix oas.gen.plug --file path/to/petstore-mini.yaml --name-space My.Petstore
Generates Router and Plug modules from OAS
* creating lib/my/petstore/router.ex
* creating lib/my/petstore/pre_find_pet_by_id.ex
* creating lib/my/petstore/find_pet_by_id.ex
* creating lib/my/petstore/pre_add_pet.ex
* creating lib/my/petstore/add_pet.ex
* creating lib/my/petstore/pre_get_pets.ex
* creating lib/my/petstore/get_pets.ex
Now, we can see the generation folder path is lib/my/petstore
, let's open the get_pets.ex
file, the module name changed from Oasis.Gen.GetPets
to My.Petstore.GetPets
.
"x-oasis-name-space"
in the OAS's Operation Object, for example, add this newline x-oasis-name-space: Common.Api
as below: paths:
/pets:
get:
x-oasis-name-space: Common.Api
parameters:
- name: tags
...
post:
...
/pets/{id}:
...
Run again, please note this time no --name-space
argument.
mix oas.gen.plug --file path/to/petstore-mini.yaml
Generates Router and Plug modules from OAS
* creating lib/oasis/gen/router.ex
* creating lib/oasis/gen/pre_find_pet_by_id.ex
* creating lib/oasis/gen/find_pet_by_id.ex
* creating lib/oasis/gen/pre_add_pet.ex
* creating lib/oasis/gen/add_pet.ex
* creating lib/common/api/pre_get_pets.ex
* creating lib/common/api/get_pets.ex
We can see pre_get_pets.ex
and get_pets.ex
files are moved into the expected path, and their module names are Common.Api.GetPets
and Common.Api.PreGetPets
, other operations do not define any "x-oasis-name-space"
field, so they still use the default one Oasis.Gen
.
"x-oasis-name-space"
in the OAS's Paths Object, this use case as a global setting and can archive it in the document under the file version management, for example, add this newline x-oasis-name-space: Common.Api
as below: paths:
x-oasis-name-space: Common.Api
/pets:
get:
parameters:
- name: tags
...
post:
...
/pets/{id}:
...
Run again, please note this time no --name-space
argument, and the GET operation of the "/pets" path is no "x-oasis-name-space" either.
mix oas.gen.plug --file path/to/petstore-mini.yaml
Generates Router and Plug modules from OAS
* creating lib/common/api/router.ex
* creating lib/common/api/pre_find_pet_by_id.ex
* creating lib/common/api/find_pet_by_id.ex
* creating lib/common/api/pre_add_pet.ex
* creating lib/common/api/add_pet.ex
* creating lib/common/api/pre_get_pets.ex
* creating lib/common/api/get_pets.ex
We can see all generated files are moved into the expected path, and all modules' name start with Common.Api
.
Summarize about the name space of generated module:
The optional --name-space
argument to the mix oas.gen.plug
command line is in the highest priority to set the name space;
We also can define "x-oasis-name-space"
extension field in the specification document to make this archivable, please see here for details.
The generated HTTP request handler files are named in pairs, one is pre_operation.ex
, and another is operation.ex
, the name beginning with pre_
file is in charge of converting and validating the parameters of each HTTP request definition, the content of this file will be updated according to the section of the document changes or Oasis upgrade in the future, so we CAN NOT write any business logic in this file; another file is only created in the first time of generation as long as this file does not exist, this is a common Plug
module which is after the defined preprocessor by Plug
's pipeline, we need to fill in business logic in here.
mix oas.gen.plug --file path/to/petstore-mini.yaml
Generates Router and Plug modules from OAS
* creating lib/oasis/gen/router.ex
* creating lib/oasis/gen/pre_find_pet_by_id.ex
* creating lib/oasis/gen/find_pet_by_id.ex
* creating lib/oasis/gen/pre_add_pet.ex
* creating lib/oasis/gen/add_pet.ex
* creating lib/oasis/gen/pre_get_pets.ex
* creating lib/oasis/gen/get_pets.ex
After the above generation, let's integrate it, assume the generated router is Oasis.Gen.Router
, we can use it in the Plug
's adapter like this:
Plug.Adapters.Cowboy.child_spec(
scheme: :http,
plug: Oasis.Gen.Router,
options: [
port: port
]
)
Or we can plug this router into the existence router module like this:
defmodule MyExistenceRouter do
use Plug.Router
plug(Oasis.Gen.Router)
plug(:match)
plug(:dispatch)
# other
end
Please note that the added line of plug(Oasis.Gen.Router)
is before the line of plug(:match)
.