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.
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!
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 officialswagger-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.
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:
JSON
, using the getOpenApiAsJson() method
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.
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:
@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"); });
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:
router.POST("/books/${bookId:<AN+>}") .specs(...) .handle(booksController::get);
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:
router.POST("/books/${bookId:<AN+>}") .specs(new SpecsObject() {}) .handle(booksController::get);
You annotate this anonymous class with @Specs:
router.POST("/books/${bookId:<AN+>}") .specs(@Specs(...) new SpecsObject() {}) .handle(booksController::get);
And you can finally add a @Operation,
@Consumes,
and/or @Produces
annotations inside @Specs
:
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);
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:
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 :
full
" querystring parameter.
If you prefer that to annotations, you can also document a route using plain YAML:
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);
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.
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:
router.POST("/books/${bookId:<AN+>}") .specsIgnore() .handle(booksController::get);
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:
@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");
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:
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);
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.
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:
SpincastOpenApiManager
instance (the cache, the
ignored routes, the base OpenAPI object, etc.), you can call
resetAll().
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
.
1. Add this Maven artifact to your project:
<dependency> <groupId>org.spincast</groupId> <artifactId>spincast-plugins-openapi-bottomup</artifactId> <version>2.2.0</version> </dependency>
2. Add an instance of the SpincastOpenApiBottomUpPlugin plugin to your Spincast Bootstrapper:
Spincast.configure() .plugin(new SpincastOpenApiBottomUpPlugin()) // ...