Slide 1

Slide 1 text

@codeJENNerator March 20, 2019 Javaland Jenn Strater Documenting RESTful APIs with Spring REST Docs

Slide 2

Slide 2 text

@codeJENNerator • Bulleted text like this indicates the key points mentioned on a previous slide. They may not have been included in the official presentation. • If this view does not support links, the links will work in the pdf. In speakerdeck, you can click the ‘download pdf’ button at the right.

Slide 3

Slide 3 text

@codeJENNerator • Documenting RESTful APIs with Spring REST Docs (this talk): • English • Groovy • Gradle • REST-like APIs • focus: how to build documentation • Schnittstellen-Dokumentation mit Spring REST Docs with Gerrit Meier: • German • Java • Maven • Hypermedia • focus: live coding examples

Slide 4

Slide 4 text

@codeJENNerator https://speakerdeck.com/jlstrater/test-driven-docs-javaland-2019 https://github.com/jlstrater/groovy-spring-boot-restdocs-example Follow Along

Slide 5

Slide 5 text

@codeJENNerator • Open Source • Build Automation • Commercial SaaS Product (on-premises) • Development Productivity • for Gradle AND Apache Maven Gradle Products

Slide 6

Slide 6 text

@codeJENNerator Before Gradle

Slide 7

Slide 7 text

@codeJENNerator • Several years experience as a software engineer mostly with APIs • Spring REST Docs user and contributor since 2015 • Organizer of various Groovy community groups and events • GR8DI, GR8Conf, Groovy community slack Before Gradle

Slide 8

Slide 8 text

@codeJENNerator Audience Background

Slide 9

Slide 9 text

@codeJENNerator • Creating RESTful APIs • Spring Boot • JavaEE/Jakarta/ Microprofile • Ratpack Audience Background

Slide 10

Slide 10 text

@codeJENNerator • Creating RESTful APIs • Spring Boot • JavaEE/Jakarta/ Microprofile • Ratpack • API Documentation • Wiki Pages, Word Documents, Confluence, etc • Asciidoc / Asciidoctor • Swagger / RAML Audience Background

Slide 11

Slide 11 text

@codeJENNerator src: https://flic.kr/p/rehEf5

Slide 12

Slide 12 text

@codeJENNerator I hate writing documentation!* src: https://flic.kr/p/rehEf5

Slide 13

Slide 13 text

✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦ Spring REST Docs

Slide 14

Slide 14 text

✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦ Spring REST Docs

Slide 15

Slide 15 text

Case Study

Slide 16

Slide 16 text

@codeJENNerator REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

Slide 17

Slide 17 text

@codeJENNerator REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

Slide 18

Slide 18 text

@codeJENNerator • I’m not really a fan of the right vs wrong REST debate, but I like this categorization of APIs. • Most of our APIS were level one or two, but we wanted to have the flexibility to use hypermedia • Spring REST docs includes support for level 3 / hypermedia • Swagger 2.0 did not support hypermedia. Swagger 3.0 (released end of July 2017) now does REST Maturity Model

Slide 19

Slide 19 text

@codeJENNerator Attribution: @Alvaro_Sanchez

Slide 20

Slide 20 text

@codeJENNerator • As architecture evolves, many companies move from a central monolith to micro services or maybe even gateways and multi-tiered architectures. • For documentation, it was important to have: • a consistent look and feel • a way to show how the services can work together Monolith vs Microservices

Slide 21

Slide 21 text

@codeJENNerator Central Information Security Http Verbs Error Handling Http Status

Slide 22

Slide 22 text

@codeJENNerator • Central Information • For example, security tokens, patterns for error messages, http verbs/status codes, etc • This information needs to be written out and defined once; not on every endpoint. Keep it DRY. Central Information

Slide 23

Slide 23 text

@codeJENNerator Document Design

Slide 24

Slide 24 text

@codeJENNerator

Slide 25

Slide 25 text

@codeJENNerator • Who’s seen this before? Of the people who have never used swagger before, how many understand what this means? Even our CTO who is technical, didn’t want to spend time figuring it out. Also, product teams. • This is a swagger ui example but the concept is not limited to Swagger. I have seen many different APIs document in this way. It’s not just URI centric, but also very developer centric. No matter whether you leave here choosing Swagger or Spring REST Docs, think about your users! Document Design

Slide 26

Slide 26 text

@codeJENNerator

Slide 27

Slide 27 text

@codeJENNerator • This is an example from Spring Rest Docs using Asciidoc. • Notice the very different way of organizing information on the second slide. Resource centric document design organizes information by topic and includes urls in the examples only. The information from generated solutions isn’t enough. We need the other information too! Document Design

Slide 28

Slide 28 text

@codeJENNerator Available Tools

Slide 29

Slide 29 text

@codeJENNerator

Slide 30

Slide 30 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML

Slide 31

Slide 31 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis

Slide 32

Slide 32 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 33

Slide 33 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 34

Slide 34 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First

Slide 35

Slide 35 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First

Slide 36

Slide 36 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML

Slide 37

Slide 37 text

@codeJENNerator

Slide 38

Slide 38 text

@codeJENNerator

Slide 39

Slide 39 text

@codeJENNerator • Swagger is: — a lot of things • At the core, it is a way to standardize and define HTTP APIs over RPC. • It is very popular because of the many plugins built on top of it for things such as generating client libraries, generating docs, and much more. • In earlier versions, it did not support hypermedia. Documenting across micro services was possible, but required a bit of setup. Depending on the library, some central information is duplicated. Swagger

Slide 40

Slide 40 text

@codeJENNerator Custom Swagger Specification { "swagger": "2.0", "info": { "version": "1", "title": "My Service", "contact": { "name": "Company Name" }, "license": {} }, "host": "example.com", "basepath": "/docs" }

Slide 41

Slide 41 text

@codeJENNerator Automation img src: https://flic.kr/p/eduUfU

Slide 42

Slide 42 text

@codeJENNerator img src: https://www.flickr.com/photos/ 24874528@N04/17125924230 SpringFox

Slide 43

Slide 43 text

@codeJENNerator • Generates a Swagger Specification from source • Is very easy to setup (in simple cases) • No OpenAPI Spec 3.0 support! SpringFox

Slide 44

Slide 44 text

@codeJENNerator Swagger UI

Slide 45

Slide 45 text

@codeJENNerator

Slide 46

Slide 46 text

@codeJENNerator • Use SpringFox library • Copy static files and customize SpringFox UI approaches

Slide 47

Slide 47 text

✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦ Spring REST Docs

Slide 48

Slide 48 text

@codeJENNerator Customization img src: http://sergiodelamo.es/how-to-secure-your-grails-3-api-with-spring-security-rest-for-grails/

Slide 49

Slide 49 text

@codeJENNerator • For any non-standard configuration, you may have to override the UI. • As one example, we were adding custom headers for oauth jwt tokens. At the time, it was not supported with springfox-ui. Customization

Slide 50

Slide 50 text

@codeJENNerator Object Mapping img src: https://github.com/springfox/springfox/issues/281

Slide 51

Slide 51 text

@codeJENNerator https://github.com/swagger-api/swagger-core/issues/97

Slide 52

Slide 52 text

@codeJENNerator • In Swagger/OpenAPI Spec 2.0, there was no hypermedia support. • In OpenAPI Spec 3.0, there is some support for links Hypermedia

Slide 53

Slide 53 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 }

Slide 54

Slide 54 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell

Slide 55

Slide 55 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell

Slide 56

Slide 56 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell X

Slide 57

Slide 57 text

@codeJENNerator Annotation Hell 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/ actionName', method = 3 RequestMethod.POST) 4 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 5 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 6 settingsValue.fieldOne,
 7 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 8 new Double(settingsValue.value))]
 9 )
 10 api.saveUpdatedSetting(request)
 11 }

Slide 58

Slide 58 text

@codeJENNerator Swagger Advantages

Slide 59

Slide 59 text

@codeJENNerator

Slide 60

Slide 60 text

@codeJENNerator “Try it” Button Alternatives

Slide 61

Slide 61 text

@codeJENNerator

Slide 62

Slide 62 text

@codeJENNerator Curl -> curl 'http://localhost:8080/greetings' -i -H 'Content-Type: text/plain' HTTP/1.1 200 X-Application-Context: application:8080 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 26 Jan 2017 13:28:19 GMT [{"id":1,"message":"Hello"},{"id":2,"message":"Hi"},{"id":3,"message":"Hola"},{"id": 4,"message":"Olá"},{"id":5,"message":"Hej"}]

Slide 63

Slide 63 text

@codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First

Slide 64

Slide 64 text

@codeJENNerator Spring REST Docs

Slide 65

Slide 65 text

@codeJENNerator • What problem are you trying to solve? • What kind of documentation do you need to solve that problem? Summary

Slide 66

Slide 66 text

✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦ Spring REST Docs

Slide 67

Slide 67 text

@codeJENNerator AsciiDoc = Example API Guide Jenn Strater; :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks: [introduction] = Introduction The Example API is a RESTful web service that shows how Spring REST docs works. Interact with these endpoints in https://documenter.getpostman.com/view/6964523/ S17nVWHG#627c80fe-7520-1ff0-8f73-9b989042bd3e[image:postman-logo.png[Postman, title="Postman", width=100]]. [[overview-http-verbs]] == HTTP verbs The Example API tries to adhere as closely as possible to standard HTTP and REST conventions in its use of HTTP verbs.

Slide 68

Slide 68 text

@codeJENNerator Asciidoctor Gradle Plugin https://asciidoctor.org/docs/asciidoctor-gradle-plugin/

Slide 69

Slide 69 text

@codeJENNerator AsciiDoc Gradle Configuration plugins { id "org.asciidoctor.convert" version "1.5.3" } asciidoctor { backends 'html5' attributes 'source-highlighter' : 'prettify', 'imagesdir':'images', 'toc':'left', 'icons': 'font', 'setanchors':'true', 'idprefix':'', 'idseparator':'-', 'docinfo1':'true', }

Slide 70

Slide 70 text

@codeJENNerator Building the docs src/docs/ asciidoc index.adoc build/asciidoc/ html5 index.html ./gradlew asciidoctor

Slide 71

Slide 71 text

@codeJENNerator

Slide 72

Slide 72 text

@codeJENNerator • Docs-as-Code, arc42, AsciiDoc, Gradle & Co. im Einsatz by Ralf Müller (yesterday & April 8 at Dortmund JUG). • Test-Driven Approaches to Documentation by Jenn Strater April 2 at MicroXchg in Berlin. • Asciidoctor: Because Writing Docs Does Not Have to Suck by Andres Almiray April 4 at K15t TeamTalks in Stuttgart. More on Asciidoc

Slide 73

Slide 73 text

✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦ Spring REST Docs

Slide 74

Slide 74 text

@codeJENNerator Green Red Refactor Test-Driven Development

Slide 75

Slide 75 text

@codeJENNerator Document Green Red Refactor Test-Driven Documentation

Slide 76

Slide 76 text

@codeJENNerator Winning Solution https://flic.kr/p/5XiKxU Winning Solution

Slide 77

Slide 77 text

@codeJENNerator • Ensures documentation matches implementation • Encourages writing more tests • Reduces duplication in docs and tests • Removes annotations from source Winning Solution

Slide 78

Slide 78 text

@codeJENNerator Spring REST Docs https://flic.kr/p/5XiKxU

Slide 79

Slide 79 text

@codeJENNerator •Start with reading the docs; The written docs are good! •Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson •Current Version - 2.0.0 released Nov 28 •Twitter Account and Official Logo About Spring REST Docs

Slide 80

Slide 80 text

@codeJENNerator • Test-Driven Documentation with Spring REST Docs (Java and Spring Boot) - Spring I/O 2016 Andy Wilkinson • Writing comprehensive and guaranteed up-to-date REST API documentation - SpringOne Platform 2016 Anders Evers History of Spring REST Docs

Slide 81

Slide 81 text

@codeJENNerator projects.spring.io/spring-restdocs github.com/spring-projects/spring-restdocs gitter.im/spring-projects/spring-restdocs @springrestdocs About Spring REST Docs

Slide 82

Slide 82 text

@codeJENNerator Game Changers https://flic.kr/p/9Tiv3U

Slide 83

Slide 83 text

@codeJENNerator •Generated code snippets •Tests fail when documentation is missing or out-of- date •Rest APIs with Hypermedia •Ratpack •Dynamic routing doesn’t work with Swagger Game Changers

Slide 84

Slide 84 text

@codeJENNerator Out of the Box

Slide 85

Slide 85 text

@codeJENNerator • Testing Frameworks • MockMVC • RestAssured • WebTestClient • Build Tools • Gradle • Maven Out of the Box

Slide 86

Slide 86 text

@codeJENNerator Out of the Box

Slide 87

Slide 87 text

@codeJENNerator Documentation Format • AsciiDoc • Markdown Sample Projects • Spring Boot • Grails • Slate • TestNG • And more! Out of the Box

Slide 88

Slide 88 text

Example

Slide 89

Slide 89 text

@codeJENNerator • Groovy Spring Boot Project • + Asciidoctor Gradle plugin • + Spring REST Docs WebTestClient to Spock tests • + Add to static assets during build and publish to GitHub pages Spring REST Docs Example

Slide 90

Slide 90 text

@codeJENNerator Groovy Spring REST Docs

Slide 91

Slide 91 text

@codeJENNerator

Slide 92

Slide 92 text

@codeJENNerator Project Reactor and the WebTestClient

Slide 93

Slide 93 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } }

Slide 94

Slide 94 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting

Slide 95

Slide 95 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings

Slide 96

Slide 96 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings Get a greeting by id

Slide 97

Slide 97 text

@codeJENNerator @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } } Setup

Slide 98

Slide 98 text

@codeJENNerator @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } } Setup If context is null, remember to use spock-spring!!

Slide 99

Slide 99 text

@codeJENNerator WebTestClient Call and Assertions void 'test and document creating a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }

Slide 100

Slide 100 text

@codeJENNerator

Slide 101

Slide 101 text

@codeJENNerator Spring REST Docs Gradle Configuration dependencies { asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" } ext { snippetsDir = "$buildDir/generated-snippets" } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test } build { dependsOn asciidoctor }

Slide 102

Slide 102 text

@codeJENNerator Spring REST Docs Gradle Configuration dependencies { asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" } ext { snippetsDir = "$buildDir/generated-snippets" } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test } build { dependsOn asciidoctor } Or mockmvc or restassured

Slide 103

Slide 103 text

@codeJENNerator @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)

Slide 104

Slide 104 text

@codeJENNerator @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)

Slide 105

Slide 105 text

@codeJENNerator @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)

Slide 106

Slide 106 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document creating a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') .consumeWith(document('greetings-post-example', requestFields( fieldWithPath('message') .type(JsonFieldType.STRING) .description("The greeting's message")))) JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }

Slide 107

Slide 107 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document creating a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') .consumeWith(document('greetings-post-example', requestFields( fieldWithPath('message') .type(JsonFieldType.STRING) .description("The greeting's message")))) JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }

Slide 108

Slide 108 text

@codeJENNerator

Slide 109

Slide 109 text

@codeJENNerator

Slide 110

Slide 110 text

@codeJENNerator

Slide 111

Slide 111 text

@codeJENNerator

Slide 112

Slide 112 text

@codeJENNerator

Slide 113

Slide 113 text

@codeJENNerator

Slide 114

Slide 114 text

@codeJENNerator Generated Snippets By Default When Specified curl-request.adoc response-fields.adoc http-request.adoc request-parameters.adoc httpie-request.adoc request-parts.adoc http-response.adoc path-parameters.adoc request body request-parts.adoc response body

Slide 115

Slide 115 text

@codeJENNerator http-request.adoc [source,http,options="nowrap"] ---- POST /greetings/ HTTP/1.1 Content-Type: application/json Accept: application/json Host: localhost:8080 Content-Length: 45 { "message" : "Hello!" } ----

Slide 116

Slide 116 text

@codeJENNerator response-fields.adoc |=== |Path|Type|Description |`id` |`String` |The greeting's id |`message` |`String` |The greeting's message |===

Slide 117

Slide 117 text

@codeJENNerator +

Slide 118

Slide 118 text

@codeJENNerator [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[] Adding The Snippets

Slide 119

Slide 119 text

@codeJENNerator [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[] Adding The Snippets

Slide 120

Slide 120 text

@codeJENNerator [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[] Adding The Snippets

Slide 121

Slide 121 text

@codeJENNerator [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[] Adding The Snippets

Slide 122

Slide 122 text

@codeJENNerator

Slide 123

Slide 123 text

@codeJENNerator

Slide 124

Slide 124 text

@codeJENNerator

Slide 125

Slide 125 text

Publishing Strategies

Slide 126

Slide 126 text

@codeJENNerator • Hook in asciidoctor with the gradle build task • Run the asciidoctor test separately (but make sure to run AFTER the tests) • Send to static resources directory in the current app or send to a remote site (for example Github Pages Publishing Strategies

Slide 127

Slide 127 text

@codeJENNerator

Slide 128

Slide 128 text

@codeJENNerator http://api.example.com/docs

Slide 129

Slide 129 text

@codeJENNerator Publish to GitHub Pages In build.gradle: plugins { id 'org.ajoberstar.git-publish' version '2.0.0' } In publish.gradle: gitPublish { repoUri = '[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git' branch = 'gh-pages' contents { from file('build/asciidoc/html5') } commitMessage = 'Updating the doc example' }

Slide 130

Slide 130 text

@codeJENNerator Publish to GitHub Pages In build.gradle: plugins { id 'org.ajoberstar.git-publish' version '2.0.0' } In publish.gradle: gitPublish { repoUri = '[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git' branch = 'gh-pages' contents { from file('build/asciidoc/html5') } commitMessage = 'Updating the doc example' } If you use this method, remember to deploy docs at the same time as the project!

Slide 131

Slide 131 text

@codeJENNerator

Slide 132

Slide 132 text

@codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example

Slide 133

Slide 133 text

@codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example ./gradlew publish

Slide 134

Slide 134 text

@codeJENNerator Special Use Case

Slide 135

Slide 135 text

@codeJENNerator 
 +@WebMvcTest(controllers = GreetingsController)
 +@AutoConfigureRestDocs(
 + outputDir = "build/generated-snippets",
 + uriHost = “api.example.com”,
 + uriPort = 8080
 ) class BaseControllerSpec extends Specification { 
 // @Rule
 // JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/ docs/generated-snippets')
 
 + @Autowired
 protected MockMvc mockMvc
 //
 // @Autowired
 // private WebApplicationContext context
 //
 // void setup() {
 // this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
 // .apply(documentationConfiguration(this.restDocumentation))
 // .build()
 // } }

Slide 136

Slide 136 text

@codeJENNerator Support for Rest-Assured

Slide 137

Slide 137 text

@codeJENNerator

Slide 138

Slide 138 text

@codeJENNerator

Slide 139

Slide 139 text

@codeJENNerator • Ratpack Example Project • https://github.com/ratpack/example-books • Spring RESTdocs RestAssured • https://github.com/ratpack/example-books/pull/25 Groovier Spring REST Docs Example - Ratpack

Slide 140

Slide 140 text

@codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack

Slide 141

Slide 141 text

@codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842

Slide 142

Slide 142 text

@codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books

Slide 143

Slide 143 text

@codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books Post to create a new book /api/books

Slide 144

Slide 144 text

@codeJENNerator https://github.com/jayway/rest-assured

Slide 145

Slide 145 text

@codeJENNerator abstract class BaseDocumentationSpec extends Specification {
 
 @Shared
 ApplicationUnderTest aut = new ExampleBooksApplicationUnderTest()
 
 @Rule
 JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()
 
 protected RequestSpecification documentationSpec
 
 void setup() {
 this.documentationSpec = new RequestSpecBuilder()
 .addFilter(documentationConfiguration(restDocumentation))
 .build()
 }
 }

Slide 146

Slide 146 text

@codeJENNerator

Slide 147

Slide 147 text

@codeJENNerator Documenting Public APIs

Slide 148

Slide 148 text

@codeJENNerator • https://jennstrater.blogspot.com/2017/01/using-spring- rest-docs-to-document.html • Blog Post https://jennstrater.blogspot.com/2017/01/using-spring- rest-docs-to-document.html

Slide 149

Slide 149 text

@codeJENNerator Related Tools & Libraries

Slide 150

Slide 150 text

@codeJENNerator Swagger2Markup https://github.com/Swagger2Markup/swagger2markup

Slide 151

Slide 151 text

@codeJENNerator img src: http://www.elvenspirit.com/elf/wp-content/uploads/2011/10/IMG_3013.jpg FAIL!

Slide 152

Slide 152 text

@codeJENNerator img src: http://www.elvenspirit.com/elf/wp-content/uploads/2011/10/IMG_3013.jpg FAIL! AssertJ-Swagger • https://github.com/RobWin/assertj-swagger

Slide 153

Slide 153 text

@codeJENNerator Spring Cloud Contract

Slide 154

Slide 154 text

@codeJENNerator Stubs

Slide 155

Slide 155 text

@codeJENNerator https://github.com/fbenz/restdocs-to-postman

Slide 156

Slide 156 text

@codeJENNerator

Slide 157

Slide 157 text

@codeJENNerator Spring REST Docs API specification Integration or or

Slide 158

Slide 158 text

@codeJENNerator • Adding Security and Headers • Documenting Constraints • Hypermedia Support • XML Support • Using Markdown instead of Asciidoc • Third Party Extensions for WireMock, Jersey, Spring Cloud Contracts, AutoRestDocs Read the Docs for More On…

Slide 159

Slide 159 text

@codeJENNerator Outcomes

Slide 160

Slide 160 text

@codeJENNerator • Made it to production! (in 2016) • At the time, the team was happy with Spring REST Docs • Other dev teams like to see the examples Outcomes

Slide 161

Slide 161 text

@codeJENNerator Next Steps

Slide 162

Slide 162 text

@codeJENNerator Join #spring-restdocs on gitter https://gitter.im/spring- projects/spring-restdocs Join the Groovy Community on Slack groovycommunity.com Slides: https://speakerdeck.com/jlstrater/test-driven-docs- javaland-2019 Example Project: https://github.com/jlstrater/groovy- spring-boot-restdocs-example

Slide 163

Slide 163 text

Conclusion

Slide 164

Slide 164 text

@codeJENNerator • API documentation is complex • Choosing the right tool for the job not just about the easiest one to setup • Spring REST Docs is a promising tool to enforce good testing and documentation practices without muddying source code. • I still hate writing boilerplate documentation, but at least it’s a little less painful now. Conclusion

Slide 165

Slide 165 text

w Thank You @codeJENNerator