templates.plugins.spincast-openapi-bottomup.spincast-openapi-bottomup.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{% extends "../../layout.html" %}
{% block sectionClasses %}plugins hasBreadCrumb plugins-spincast-openapi-bottomup{% endblock %}
{% block meta_title %}Plugins - Spincast OpenAPI bottom up{% endblock %}
{% block meta_description %}Spincast OpenAPI bottom up plugin - OpenAPI / Swagger specifications file generation from the applicatin code{% endblock %}
{% block scripts %}
{% endblock %}
{% block body %}
Overview
This plugin allows you to generate
OpenAPI / Swagger specifications
(currently 3.0 compatible) for your Spincast REST API, as JSON
or YAML
.
It automatically uses information from your routes and provides tools for you to complete/tweak the
specifications to expose as the documentation for your API.
Introduction
OpenAPI is one of the most popular ways of documenting a
REST API
. It is a specification
around which many tools and libraries have been created. The
most known of those tools are probably the Swagger Editor and
the Swagger UI.
The OpenAPI ecosystem may be a little bit confusing at first because there are a lot of different ways
of using the specification. Some libraries will
provide a way of generating application code (controllers, validators, etc.) given an existing
specifications file... This is called the "top-down" approach (or "API first" approach).
Others libraries will do the opposite: they will generate the specifications
file from your application code... This is called the "bottom-up" approach (or "code first" approach)
and is the one used by this plugin.
Whatever approach you use, the most important part of an OpenAPI implementation
is to be able to provide the specifications to the consumers of your API, as JSON
or as YAML
.
You could send those specifications by email to developers in charge of developing a client using your API
(for example as a "specs.yaml
" file), but most of the time you expose
the specifications as an HTTP endpoint
so they can be easily accessed!
How this plugin works
Some frameworks try to develop an OpenAPI implementation using a custom
parser and custom code to generate the final specifications. This is not
a trivial task since the OpenAPI specification contains many details! It can be easy to forget something
or to generate something invalid. Also, keeping up-to-date with new versions of the
specification may be challenging...
This is why we decided to develop this plugin in a way that code from the
official reference implementation
can be reused as much as possible.
By doing so, we make sure we use battle tested and well-maintained code. We also make sure it is super easy to
upgrade to a new version of the specification in the future.
The problem with the official Java implementation is that it is made
specifically for JAX-RS
based applications and Spincast is not!
Spincast is indeed more flexible: it allows you to define your routes as you want (inline if required),
without any annotations. Also, routes can be dynamically added to the Router, they don't
necessarily have to be defined in controllers using special annotations.
So, how can we reuse the official code?
This plugin automagically generate JAX-RS classes from the Spincast code.
It does this using Byte Buddy. With those generated
dummy classes, it is possible to reuse the code from the official swagger-jaxrs2
library,
without any change.
In other words, the job of this plugin is only to generate pseudo JAX-RS
classes... Everything else
is done by the official library: the parsing and the specifications generation.
To help you specify the information to include in the resulting specifications, this plugin
also provides a way to reuse another official part of OpenAPI/Swagger: the
Swagger annotations.
As you'll see in the Documenting the routes section, the most important annotation
you can use is @Operation.
If you don't provide any explicit information about your routes, using annotations
or using plain YAML, the plugin will still be able to generate basic specifications.
You manually add the extra pieces of information required for the final specifications to be completed.
Usage
Accessing the generated specifications
First of all, the specifications resulting from collecting the various pieces of information about your API
are accessible as an
OpenAPI Java object. You access this
object via the SpincastOpenApiManager#getOpenApi()
method.
It is this object that will be converted to generate the final specifications as JSON
or as YAML
!
You generate the specifications:
-
As
JSON
, using the getOpenApiAsJson() method
-
As
YAML
, using the getOpenApiAsYaml() method
As you can see, the
SpincastOpenApiManager
component is at the very core of this plugin. You can inject it anywhere you need to interact with it.
Serving the specifications as an HTTP endpoint
When your application is properly documented (we'll learn how to do this in the next section), it is often
desirable to serve the resulting specifications as an HTTP endpoint
so they can be accessed
easily.
You create such endpoint as any other one. If your specifications won't
change at runtime, using a Dynamic Resource
is the way to go! Let's see how you can do this:
{% verbatim %}
@Inject
protected SpincastOpenApiManager spincastOpenApiManager;
// As JSON
router.file("/specifications.json")
.pathRelative("/generated/specifications.json")
.handle((context) -> {
context.response().sendJson(spincastOpenApiManager.getOpenApiAsJson());
});
// As YAML
router.file("/specifications.yaml")
.pathRelative("/generated/specifications.yaml")
.handle((context) -> {
context.response().sendCharacters(spincastOpenApiManager.getOpenApiAsYaml(), "text/yaml");
});
{% endverbatim %}
Documenting the routes
If you don't add any explicit information, the generated specifications will still be available but
will be basic. They will contain information about your routes:
their HTTP methods, their paths, their consumes content-types and their path parameters.
To add more information, you can use the
.specs(...)
method available when you define the routes. You pass to this method the extra information
using the official annotations.
Read the official documentation to learn more about those annotations.
Here's a step-by-step guide on how to add the annotation(s)... You first start by using the .specs(...)
method:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specs(...)
.handle(booksController::get);
{% endverbatim %}
In that method, you create an anonymous class based on the SpecsObject
interface. This
acts as a container for the annotations so they can later be retrieved by Spincast:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specs(new SpecsObject() {})
.handle(booksController::get);
{% endverbatim %}
You annotate this anonymous class with @Specs:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specs(@Specs(...) new SpecsObject() {})
.handle(booksController::get);
{% endverbatim %}
And you can finally add a @Operation,
@Consumes,
and/or @Produces
annotations inside @Specs
:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specs(new @Specs(consumes = @Consumes({"application/xml"}),
produces = @Produces({"application/json"}),
value = @Operation(summary = "My summary",
description = "My description"
)) SpecsObject() {})
.handle(booksController::get);
{% endverbatim %}
This may seem a little bit complicated at first but this is what allows
the reuse of the
official, well supported, Swagger reference implementation code!
Here's another example:
{% verbatim %}
router.POST("/users")
.specs(new @Specs(@Operation(
parameters = {
@Parameter(name = "full",
in = ParameterIn.QUERY,
schema = @Schema(description = "Return all informations?")),
})) SpecsObject() {})
.handle(booksController::get);
Explanation :
-
2 : We use the
.specs(...)
method to start adding specifications to the route. The @Specs
annotation is only a wrapper allowing us to use the official @Operation annotation.
-
3-6 : The extra OpenAPI specifications to associate with the route.
In this simple example, we only document a "
full
" querystring parameter.
-
7 : The annotations need to be added on something for Spincast
to be able to retrieve them. You create an
anonymous class from the SpecsObject interface
to store them.
{% endverbatim %}
Using YAML instead of annotations
If you prefer that to annotations, you can also document a route using plain YAML:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specs("post:\n" +
" operationId: getBook\n" +
" responses:\n" +
" default:\n" +
" description: default response\n" +
" content:\n" +
" application/json: {}\n")
.handle(booksController::get);
{% endverbatim %}
The YAML specifications you provide must start by the HTTP methods (you can provide more than one!).
And you must follow the syntax required by the OpenAPI specification.
Note that the indentation in your YAML content IS important and an exception will be thrown if
it is invalid.
Ignoring some routes
You may want to prevent some of your routes to be added to the final specifications.
To do so, you can call specsIgnore()
on those routes:
{% verbatim %}
router.POST("/books/${bookId:<AN+>}")
.specsIgnore()
.handle(booksController::get);
{% endverbatim %}
If you need to ignore routes that have been added by a third-party plugin, you can do so
by using their id (if they have one), or their HTTP method and path otherwise:
{% verbatim %}
@Inject
protected SpincastOpenApiManager spincastOpenApiManager;
// Ignoring a route by its id:
spincastOpenApiManager.ignoreRoutesByIds("someRouteId");
// Ignoring a route by its HTTP method and path:
spincastOpenApiManager.ignoreRouteUsingHttpMethodAndPath(HttpMethod.POST, "/some-path");
{% endverbatim %}
Global OpenAPI information
Using setOpenApiBase(...)
on the SpincastOpenApiManager
component, you can provide any OpenAPI information you want to be part of
the generated specifications. You do this by creating a custom
OpenAPI object:
{% verbatim %}
OpenAPI openApiBase = new OpenAPI();
Info info = new Info().title("The name of your REST API")
.description("A description of your API...")
.termsOfService("https://example.com/tos");
openApiBase.info(info);
getSpincastOpenApiManager().setOpenApiBase(openApiBase);
{% endverbatim %}
Note that you can use that base OpenAPI object to fully define the specifications
of your API, without using any information from the routes themselves!
You do so by disabling the automatic specifications via the plugin configurations,
and then specify everything (paths
included) using this base OpenAPI object.
Dynamic specifications
If your specifications may change at runtime (for example if a new route may be added at some point),
there are some things to know:
-
You shouldn't use a Dynamic Resource route to serve the
specifications since the first generated version would be cached forever. You need to use a standard route.
-
The code adding new information to the specifications (or simply adding a new route)
needs to call clearCache().
This is required since Spincast caches the specifications for performance reasons.
-
If, at some point, you want to reset everything in the
SpincastOpenApiManager
instance (the cache, the
ignored routes, the base OpenAPI object, etc.), you can call
resetAll().
Configurations
To tweak the configurations used by this plugin, you need to bind a custom
implementation of the SpincastOpenApiBottomUpPluginConfig
interface. You can also extend the default SpincastOpenApiBottomUpPluginConfigDefault
implementation as a base class.
The available configurations are:
-
boolean isDisableAutoSpecs()
If this method returns true
no automatic specifications are going to
be created. You are then responsible to add all the information about your API using
the base OpenAPI object.
The default is to return false
.
-
String[] getDefaultConsumesContentTypes()
The default content-types
that are going to be used as the
@Consumes
information of a route if not explicitly defined otherwise.
The default is application/json
.
Note that specifying a @Consumes
annotation inside .specs(...)
will overwrite that default, but also using
.accept(...)
on the route definition!
-
String[] getDefaultProducesContentTypes()
The default content-types
that are going to be used as the
@Produces
information of a route if not explicitly defined otherwise.
The default is application/json
.
Installation
1.
Add this Maven artifact to your project:
<dependency>
<groupId>org.spincast</groupId>
<artifactId>spincast-plugins-openapi-bottomup</artifactId>
<version>{{spincast.spincastCurrrentVersion}}</version>
</dependency>
2. Add an instance of the SpincastOpenApiBottomUpPlugin
plugin to your Spincast Bootstrapper:
{% verbatim %}
Spincast.configure()
.plugin(new SpincastOpenApiBottomUpPlugin())
// ...
{% endverbatim %}
{% endblock %}