API documentation (Swagger)
We use swagger specification for RESTful API documentation with tools:
- Swagger UI - visualize and interact with the API’s resources. It is available at IdM backend entrypoint e.g.
<server>/idm-backend
or<server>/idm-backend/api
. - Swagger2Markup - generation of an up-to-date RESTful API documentation by combining documentation that’s been hand-written with auto-generated API documentation produced by Swagger. The result is intended to be an up-to-date, easy-to-read, on- and offline user guide.
- Asciidoctor maven plugin - official way to convert AsciiDoc documentation using Asciidoctor from an Maven build.
- Dynamic documentation is exposed by Swagger UI from rest controllers with
@Api
annotation. Documentation is available on root backend server url e.g.<server>
or<server>/api
- documented endpoints can be tested with demo credentials directly from Swagger UI. Documentation is splitted by modules. Raw Swagger specification in json format is available on url e.g.<server>/api/doc?group=core
. Documentation always contains authentication endpoint (security information). - Static documentation is generated from raw swagger specification (e.g.
<server>/api/doc?group=core
) and static content (see next chapter with static documentation folder structure). Documentation is splitted by modules. Generated output html can be found in<module>/target/asciidoc/html/index.html
. This documentation can be exposed as api reference and is exposed directly in application, which is built underrelease
profile on urls with convention<server>/webjars/<module>/<version>/doc/index.html
.
Configuration
Parent project contains basic settings for module documentation. When a new module is added, some steps have to be done.
Complete configuration can be found in example
module.
Java
Module properties
Use PropertyModuleDescriptor
generalization for module descriptor definition and prepare module-example.properties.
/** * Example module descriptor */ @Component @PropertySource("classpath:module-" + ExampleModuleDescriptor.MODULE_ID + ".properties") @ConfigurationProperties(prefix = "module." + ExampleModuleDescriptor.MODULE_ID + ".build", ignoreUnknownFields = true, ignoreInvalidFields = true) public class ExampleModuleDescriptor extends PropertyModuleDescriptor { public static final String MODULE_ID = "example"; @Override public String getId() { return MODULE_ID; } /** * Enables links to swagger documentation */ @Override public boolean isDocumentationAvailable() { return true; } }
Swagger endpoint
/** * Example module swagger configuration */ @Configuration @ConditionalOnProperty(prefix = "springfox.documentation.swagger", name = "enabled", matchIfMissing = true) public class ExampleSwaggerConfig extends AbstractSwaggerConfig { @Autowired private ExampleModuleDescriptor moduleDescriptor; @Override protected ModuleDescriptor getModuleDescriptor() { return moduleDescriptor; } @Bean public Docket exampleApi() { return api("eu.bcvsolutions.idm.example"); } }
Rest controller
Add swagger annotations to controllers.
/** * Ping pong example controller */ @RestController @RequestMapping(value = BaseController.BASE_PATH + "/examples", produces = BaseController.APPLICATION_HAL_JSON_VALUE) @Api(value = "Examples", description = "Example operations", tags = { "Examples" }) public class ExampleController { @ResponseBody @RequestMapping(method = RequestMethod.GET) @ApiOperation( value = "Ping - Pong operation", notes= "Returns message with additional informations", nickname = "ping", tags={ "Examples" }, response = Pong.class, authorizations = { @Authorization(SwaggerConfig.AUTHENTICATION_BASIC), @Authorization(SwaggerConfig.AUTHENTICATION_CIDMST) }) public ResponseEntity<?> ping( @ApiParam(value = "In / out message", example = "hello", defaultValue = "hello") @RequestParam(required = false, defaultValue = "hello") String message ) { return new ResponseEntity<>(new Pong(message), HttpStatus.OK); } }
Model
Add swagger annotations to dtos.
/** * Example ping - pong response dto */ @ApiModel(description = "Ping - Pong response") public class Pong implements BaseDto { private static final long serialVersionUID = 1L; // @ApiModelProperty(required = true, notes = "Unique pong identifier") private UUID id; @ApiModelProperty(notes = "Ping - Pong response message") private String message; @ApiModelProperty(required = true, notes = "Creation time") private DateTime created; // ... getters, setters }
Test
Create integration test:
/** * Static swagger generation to sources - will be used as input for swagger2Markup build */ public class Swagger2MarkupTest extends AbstractSwaggerTest { @Test public void testConvertSwagger() throws Exception { super.convertSwagger(ExampleModuleDescriptor.MODULE_ID); } }
Static documentation folder structure
We are using asciidoctor maven plugin for static documentation (see maven chapter). Maven plugins prepare all generated artifacts, but requires some static artifacts with static / written documentation in structure in module sources folder:
<module>/src/docs/asciidoc/
- root documentation folderindex.adoc
- main documentation file / entrypoint. Contains documentation structure, includes all other generated and extension files.extensions
- contains files with extensionsdefinitions
- api modelsoverview
- antre sectionpaths
- controller pathssecurity
- security section
All files have to be written in asciidoc format. Read more about extensions in swagger2markup documentation.
Example extension
Add security information about demo identity credentials. Create file <module>/src/docs/asciidoc/extensions/security/document-begin-text.adoc
with content:
== Demo credentials admin / admin
This content will be automatically included to static documentation to the security section. Read more about available extension points.
Maven
Documentation is generated under release
profile. Add profile to module pom.xml
:
<profile> <id>release</id> <build> <plugins> <!-- First, use the swagger2markup plugin to generate asciidoc --> <plugin> <groupId>io.github.swagger2markup</groupId> <artifactId>swagger2markup-maven-plugin</artifactId> <version>${swagger2markup.version}</version> <configuration> <swaggerInput>${swagger.input}</swaggerInput> <outputDir>${generated.asciidoc.directory}</outputDir> <config> <swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage> <swagger2markup.outputLanguage>EN</swagger2markup.outputLanguage> <swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy> <swagger2markup.generatedExamplesEnabled>false</swagger2markup.generatedExamplesEnabled> <swagger2markup.extensions.dynamicOverview.contentPath>${asciidoctor.input.extensions.directory}/overview</swagger2markup.extensions.dynamicOverview.contentPath> <swagger2markup.extensions.dynamicDefinitions.contentPath>${asciidoctor.input.extensions.directory}/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath> <swagger2markup.extensions.dynamicPaths.contentPath>${asciidoctor.input.extensions.directory}/paths</swagger2markup.extensions.dynamicPaths.contentPath> <swagger2markup.extensions.dynamicSecurity.contentPath>${asciidoctor.input.extensions.directory}/security/</swagger2markup.extensions.dynamicSecurity.contentPath> <swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri> <swagger2markup.extensions.springRestDocs.defaultSnippets>true</swagger2markup.extensions.springRestDocs.defaultSnippets> </config> </configuration> <executions> <execution> <phase>test</phase> <goals> <goal>convertSwagger2markup</goal> </goals> </execution> </executions> </plugin> <!-- Run the generated asciidoc through Asciidoctor to generate other documentation types, such as PDFs or HTML5 --> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.3</version> <!-- Configure generic document generation settings --> <configuration> <sourceDirectory>${asciidoctor.input.directory}</sourceDirectory> <sourceDocumentName>index.adoc</sourceDocumentName> <attributes> <doctype>book</doctype> <toc>left</toc> <toclevels>2</toclevels> <!-- Resources by tag names in menu only --> <numbered /> <hardbreaks /> <sectlinks /> <sectanchors /> <generated>${generated.asciidoc.directory}</generated> </attributes> </configuration> <!-- Since each execution can only handle one backend, run separate executions for each desired output type --> <executions> <execution> <id>output-html</id> <phase>test</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html5</backend> <!-- static documentation will be available as webjars --> <!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html --> <outputDirectory>${asciidoctor.html.output.directory.prefix}/core/${project.version}/doc</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile>
All maven properties are preconfigured in parent pom.xml
:
<swagger.version>2.7.0</swagger.version> <swagger2markup.version>1.3.1</swagger2markup.version> <!-- properties are used ind test and doc profile, test: swagger.json input is generated doc: swagger2markup and asciidoctor plugins require them --> <asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory> <asciidoctor.input.extensions.directory>${asciidoctor.input.directory}/extensions</asciidoctor.input.extensions.directory> <swagger.output.dir>${project.build.directory}/swagger</swagger.output.dir> <swagger.output.filename>swagger.json</swagger.output.filename> <swagger.input>${swagger.output.dir}/${swagger.output.filename}</swagger.input> <swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippets</swagger.snippetOutput.dir> <generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory> <!-- static documentation will be available as webjars --> <!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html --> <asciidoctor.html.output.directory.prefix>${project.build.directory}/classes/META-INF/resources/webjars</asciidoctor.html.output.directory.prefix> <asciidoctor.html.output.directory>${asciidoctor.html.output.directory.prefix}/${project.artifactId}/${project.version}/doc</asciidoctor.html.output.director
Conventions
- Add Swagger annotation. What can be written into annotation, will be written to annotation - will be shown in dynamic and static documentation. Static documentation extension is used, when annotation doesn't fit.
- Use module-<module>.properties with
PropertyModuleDescriptor
. - Use
produces = BaseController.APPLICATION_HAL_JSON_VALUE
in controller mapping - Use
@ApiOperation(nickname = "<operatonId>")
e.g.@ApiOperation(nickname = "ping")
for controller methods - nickname (⇒ operationId) can be used in permalink.
Tips
Use IdmIdentityController as inspiration.
Export swagger.json by running single test:
mvn clean package -DskipTests mvn surefire:test -Dtest=Swagger2MarkupTest -Prelease
Generate static documentation only (swagger.json has to be exported - see previous tip). Generated html will be available in project's folder /target/classes/META-INF/resources/webjars/<module>/<version>/doc/index.html
:
mvn package -DskipTests -Prelease
Static html documentation will be available from url (application's war has to be build under release
profile):
<server>/webjars/<module>/<version>/doc/index.html // e.g. http://localhost:8080/idm/webjars/core/7.3.0-rc.4-SNAPSHOT/doc/index.html
release
profile, then static html and javadoc are packed into archive /target/<version>-doc.zip
(tar.gz, tar.bz2).
cd aggregator/
mvn package -Prelease -DdocumentationOnly=true
Implementation details
- Static documentation contains all files for now (copy / paste redundancy - see security section - same in all modules) - will be improved soon.
- JSR303 for model documentation (comming soon)
- Model SPI (comming soon)
- UUID, GuardedString types (comming soon)