Generate an OpenAPI v3 compliant yaml file declaratively from your web framework code.
Then serve it from a Swagger UI instance.
- Add the dependency to your
shard.yml
:
dependencies:
openapi-generator:
github: elbywan/openapi-generator
-
Run
shards install
-
Require the shard
require "openapi-generator"
From the OpenAPI specification.
In OpenAPI terms, paths are endpoints (resources), such as /users or /reports/summary/, that your API exposes, and operations are the HTTP methods used to manipulate these paths, such as GET, POST or DELETE.
Use the @[OpenAPI]
annotation with a yaml
encoded string argument.
class Controller
include OpenAPI::Generator::Controller
@[OpenAPI(<<-YAML
tags:
- tag
summary:
A brief summary.
YAML
)]
def handler
# …
end
end
Use the open_api
macro with a yaml
encoded string argument.
class Handler
include OpenAPI::Generator::Controller
open_api <<-YAML
tags:
- tag
summary:
A brief summary.
YAML
route do
# …
end
end
The OpenAPI::Generator::Controller::Schema
class exposes shorthands for common OpenAPI yaml constructs.
# Example:
open_api <<-YAML
tags:
- tag
summary:
A brief summary.
parameters:
#{Schema.qp name: "id", description: "Filter by id.", required: true}
responses:
200:
description: OK
#{Schema.error 404}
YAML
Schema.error(code, message = nil)
Schema.header(name, description, type = "string")
Schema.header_param(name, description, *, required = false, type = "string")
Schema.qp(name, description, *, required = false, type = "string")
Schema.ref(schema, *, content_type = "application/json")
Schema.ref_array(schema, *, content_type = "application/json")
Schema.string_array(*, content_type = "application/json")
After declaring the operations, you can call OpenAPI::Generator.generate
to generate the openapi.yaml
file that will describe your server.
Note: An OpenAPI::Generator::RoutesProvider::Base
implementation must be provided. A RoutesProvider
is responsible from extracting the server routes and mapping these routes with the declared operations in order to produce the final openapi file.
OpenAPI::Generator.generate(
provider: provider
)
Currently, the Amber, Lucky, Spider-gazelle providers are included out of the box.
Amber
# Amber provider
require "openapi-generator/providers/amber"
OpenAPI::Generator.generate(
provider: OpenAPI::Generator::RoutesProvider::Amber.new
)
Lucky
# Lucky provider
require "openapi-generator/providers/lucky"
OpenAPI::Generator.generate(
provider: OpenAPI::Generator::RoutesProvider::Lucky.new
)
Spider-gazelle
# Spider-gazelle provider
require "openapi-generator/providers/action-controller"
OpenAPI::Generator.generate(
provider: OpenAPI::Generator::RoutesProvider::ActionController.new
)
Custom
# Or define your own…
class MockProvider < OpenAPI::Generator::RoutesProvider::Base
def route_mappings : Array(OpenAPI::Generator::RouteMapping)
[
{"get", "/{id}", "HelloController::index", ["id"]},
{"head", "/{id}", "HelloController::index", ["id"]},
{"options", "/{id}", "HelloController::index", ["id"]},
]
end
end
OpenAPI::Generator.generate(
provider: MockProvider.new
)
The .generate
method accepts additional options:
OpenAPI::Generator.generate(
provider: provider,
options: {
# Customize output path
output: Path[Dir.current] / "public" / "openapi.yaml"
},
# Customize openapi.yaml base document fields
base_document: {
info: {
title: "My Server",
version: "v0.1",
}
}
)
Adding extend OpenAPI::Generator::Serializable
to an existing class or struct will:
- register the object as a reference making it useable anywhere in the openapi file
- add a
.to_openapi_schema
method that will produce the associatedOpenAPI::Schema
class Coordinates
extend OpenAPI::Generator::Serializable
def initialize(@lat, @long); end
property lat : Int32
property long : Int32
end
# Produces an OpenAPI::Schema reference.
puts Coordinates.to_openapi_schema.to_yaml
# ---
# allOf:
# - $ref: '#/components/schemas/Coordinates'
And in the openapi.yaml
file that gets generated, the Coordinates
object is registered as a /components/schemas/Coordinates
reference.
components:
schemas:
Coordinates:
required:
- lat
- long
type: object
properties:
lat:
type: integer
long:
type: integer
The object can now be referenced from the yaml declaration…
class Controller
include OpenAPI::Generator::Controller
@[OpenAPI(<<-YAML
requestBody:
required: true
content:
#{Schema.ref Coordinates}
YAML
)]
def method
# …
end
end
…and it can be used by the schema inference (more on that later).
class Hello::Index < Lucky::Action
include OpenAPI::Generator::Helpers::Lucky
disable_cookies
default_format :text
post "/hello" do
coordinates = body_as Coordinates?, description: "Some coordinates."
plain_text "Hello (#{coordinates.x}, #{coordinates.y})"
end
end
Use the @[OpenAPI::Field]
annotation to add properties to the fields.
class MyClass
extend OpenAPI::Generator::Serializable
# Ignore the field. It will not appear in the schema.
@[OpenAPI::Field(ignore: true)]
property ignored_field
# Enforce a type in the schema and disregard the crystal type.
@[OpenAPI::Field(type: String)]
property str_field : Int32
# Add an example that will appear in swagger for instance.
@[OpenAPI::Field(example: "an example value")]
property some_field : String
# Will not appear in POST / PUT/ PATCH requests body.
@[OpenAPI::Field(read_only: true)]
property read_only_field : String
# Will only appear in POST / PUT / PATCH requests body.
@[OpenAPI::Field(write_only: true)]
property write_only_field : String
end
openapi-generator
can infer some schema properties from the code, removing the need to declare it with yaml.
Can be inferred:
- Request body
- Response body
- Query parameters
Supported Frameworks:
Amber
require "openapi-generator/helpers/amber"
# …declare routes and operations… #
# Before calling .generate you need to bootstrap the amber inference:
OpenAPI::Generator::Helpers::Amber.bootstrap
require "openapi-generator/helpers/amber"
class Coordinates
include JSON::Serializable
extend OpenAPI::Generator::Serializable
def initialize(@x, @y); end
property x : Int32
property y : Int32
end
class CoordinatesController < Amber::Controller::Base
include ::OpenAPI::Generator::Controller
include ::OpenAPI::Generator::Helpers::Amber
@[OpenAPI(
<<-YAML
summary: Adds up a Coordinate object and a number.
YAML
)]
def add
# Infer query parameter.
add = query_params("add", description: "Add this number to the coordinates.").to_i32
# Infer body as a Coordinate json payload.
coordinates = body_as(::Coordinates, description: "Some coordinates").not_nil!
coordinates.x += add
coordinates.y += add
# Infer responses.
respond_with 200, description: "Returns a Coordinate object with the number added up." do
json coordinates, type: ::Coordinates
xml %(<coordinate x="#{coordinates.x}" y="#{coordinates.y}"></coordinate>), type: String
text "Coordinates (#{coordinates.x}, #{coordinates.y})", type: String
end
end
end
openapi-generator
overload existing or adds similar methods and macros to intercept calls and infer schema properties.
Query parameters
macro query_params(name, description, multiple = false, schema = nil, **args)
macro query_params?(name, description, multiple = false, schema = nil, **args)
Body
macro body_as(type, description = nil, content_type = "application/json", constructor = :from_json)
Responses
-
macro respond_with(code = 200, description = nil, headers = nil, links = nil, &)
-
macro json(body, type = nil, schema = nil)
-
macro xml(body, type = nil, schema = nil)
-
macro txt(body, type = nil, schema = nil)
-
macro text(body, type = nil, schema = nil)
-
macro html(body, type = nil, schema = nil)
-
macro js(body, type = nil, schema = nil)
Lucky
require "openapi-generator/helpers/lucky"
# …declare routes and operations… #
# Before calling .generate you need to bootstrap the lucky inference:
OpenAPI::Generator::Helpers::Lucky.bootstrap
Important: In your Actions, use include OpenAPI::Generator::Helpers::Lucky
instead of include OpenAPI::Generator::Controller
.
require "openapi-generator/helpers/lucky"
class Coordinates
include JSON::Serializable
extend OpenAPI::Generator::Serializable
def initialize(@x, @y); end
property x : Int32
property y : Int32
end
class Api::Coordinates::Create < Lucky::Action
# `OpenAPI::Generator::Controller` is included alongside `OpenAPI::Generator::Helpers::Lucky`.
include OpenAPI::Generator::Helpers::Lucky
disable_cookies
default_format :json
# Infer query parameter.
param add : Int32, description: "Add this number to the coordinates."
def action
# Infer body as a Coordinate json payload.
coordinates = body_as! ::Coordinates, description: "Some coordinates"
coordinates.x += add
coordinates.y += add
# Infer responses.
if json?
json coordinates, type: ::Coordinates
elsif xml?
xml %(<coordinate x="#{coordinates.x}" y="#{coordinates.y}"></coordinate>), schema: OpenAPI::Schema.new(type: "string")
elsif plain_text?
plain_text "Coordinates (#{coordinates.x}, #{coordinates.y})"
else
head 406
end
end
route { action }
end
openapi-generator
overload existing or adds similar methods and macros to intercept calls and infer schema properties.
Query parameters
macro param(declaration, description = nil, schema = nil, **args)
Body
macro body_as(type, description = nil, content_type = "application/json", constructor = :from_json)
macro body_as!(type, description = nil, content_type = "application/json", constructor = :from_json)
Responses
macro json(body, status = 200, description = nil, type = nil, schema = nil, headers = nil, links = nil)
macro head(status, description = nil, headers = nil, links = nil)
macro xml(body, status = 200, description = nil, type = String, schema = nil, headers = nil, links = nil)
macro plain_text(body, status = 200, description = nil, type = String, schema = nil, headers = nil, links = nil)
Spider-gazelle
require "openapi-generator/helpers/action-controller"
# …declare routes and operations… #
# Before calling .generate you need to bootstrap the spider-gazelle inference:
OpenAPI::Generator::Helpers::ActionController.bootstrap
require "openapi-generator/helpers/action-controller"
class Coordinates
include JSON::Serializable
extend OpenAPI::Generator::Serializable
def initialize(@x, @y); end
property x : Int32
property y : Int32
end
class CoordinatesController < ActionController::Controller::Base
include ::OpenAPI::Generator::Controller
include ::OpenAPI::Generator::Helpers::ActionController
@[OpenAPI(
<<-YAML
summary: Adds up a Coordinate object and a number.
YAML
)]
def add
# Infer query parameter.
add = param add : Int32, description: "Add this number to the coordinates."
# Infer body as a Coordinate json payload.
coordinates = body_as(::Coordinates, description: "Some coordinates").not_nil!
coordinates.x += add
coordinates.y += add
# Infer responses.
respond_with 200, description: "Returns a Coordinate object with the number added up." do
json coordinates, type: ::Coordinates
xml %(<coordinate x="#{coordinates.x}" y="#{coordinates.y}"></coordinate>), type: String
text "Coordinates (#{coordinates.x}, #{coordinates.y})", type: String
end
end
end
openapi-generator
overload existing or adds similar methods and macros to intercept calls and infer schema properties.
Query parameters
macro param(declaration, description, multiple = false, schema = nil, **args)
Body
macro body_as(type, description = nil, content_type = "application/json", constructor = :from_json)
Responses
-
macro respond_with(code = 200, description = nil, headers = nil, links = nil, &)
macro json(body, type = nil, schema = nil)
macro xml(body, type = nil, schema = nil)
macro txt(body, type = nil, schema = nil)
macro text(body, type = nil, schema = nil)
macro html(body, type = nil, schema = nil)
macro js(body, type = nil, schema = nil)
-
macro render(status_code = :ok, head = Nop, json = Nop, yaml = Nop, xml = Nop, html = Nop, text = Nop, binary = Nop, template = Nop, partial = Nop, layout = nil, description = nil, headers = nil, links = nil, type = nil, schema = nil)
The method to serve a Swagger UI instance depends on which framework you are using.
- Setup a static file handler. (ex: Lucky, Amber)
- Download the latest release archive
- Move the
/dist
folder to your static file directory. - Edit the
index.html
file and change the assets andopenapi.yaml
paths.
- Fork it (https://github.com/your-github-user/openapi-generator/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Do not run crystal specs
without arguments. It will not compile due to global cookies and session class overrides issues between Amber & Lucky.
To test the project, have a look at the .travis.yml
file which contains the right command to use:
crystal spec ./spec/core && \
crystal spec ./spec/amber && \
crystal spec ./spec/lucky && \
crystal spec ./spec/spider-gazelle
- elbywan - creator and maintainer
- dukeraphaelng