Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Updates to build.gradle

The following packages should be added to the module's build.gradle:

...

implementation

...

Required additions to controllers/endpoints

In order to use the SwaggerApiService we need to add a controller to the module as follows:

Code Block
public class SwaggerUIController {
    SwaggerApiService swaggerApiService;

    public def api() {
        // Generate the API documentation
        Map swaggerApiDoc = swaggerApiService.generateSwaggerApiDoc();

        // This header is required if we are coming through okapi
        header("Access-Control-Allow-Origin", request.getHeader('Origin'));

        // We should now just have the calls we are interested in
        render(status: 200, contentType: "application/json", text: JsonOutput.toJson(swaggerApiDoc));
    }
}

The service used within this controller should be within the K-Int Web Toolkit (KIWT) so that it can be utilised across the modules.

Annotating the Code

To annotate a class use an annotation like the following:

Code Block
languagejava
@Path(value = "/licenses", tags = ["Swagger Controller"], description = "Swagger Api")

Value will be prefixed to the path of all operations specified in this class.
Tags will be associated with the operations so they can be grouped together
Description is a brief descrion of this class

For the methods within a class they will need to be annotated like the following:

Code Block
languagejava
@Operation(
    value = "List the states that a request can end up in after the action has been performed",
    nickname = "availableAction/toStates/{stateModel}/{actionCode}",
    produces = "application/json",
    httpMethod = "GET"
)
@ApiResponses([
    @ApiResponse(code = 200, message = "Success")
])
@ApiImplicitParams([
    @ApiImplicitParam(
        name = "stateModel",
        paramType = "path",
        required = true,
        value = "The state model the action is applicable for",
        dataType = "string",
        defaultValue = "PatronRequest"
    ),
    @ApiImplicitParam(
        name = "actionCode",
        paramType = "path",
        required = true,
        value = "The action that you want to know which states a request could move onto after the action has been performed",
        dataType = "string"
    )
])

Changing class extensions

In order to correctly implement the annotations for various grails methods such as update(), delete(), index() etc. We need to replace the OkapiTenantAwareController on the majority of controller classes with a new controller class extension called OkapiTenantAwareSwaggerController, this explicitly contained all these functions alongside their required annotations as shown below:

Code Block
languagejava
@ApiOperation(
    value = "Creates a new record with the supplied data",
    nickname = "/",
    httpMethod = "POST"
)
@ApiResponses([
    @ApiResponse(code = 201, message = "Created")
])
@ApiImplicitParams([
    @ApiImplicitParam(
        paramType = "body",
        required = true,
        allowMultiple = false,
        value = "The json record that is going to be used for creation",
        defaultValue = "{}",
        dataType = "string"
    )
])
@OkapiPermission(name = "item", permissionGroup = PermissionGroup.WRITE)
@Transactional
public def save() {

Additionally we have an additional class for handling purely GET related controllers, OkapiTenantAwareGetController. OkapiTenantAwareSwaggerController extends this class and it also contains the associated KIWT query params such as match, term, filters etc. so these can be include in the documentation automatically:

...

languagejava

...

'org.springdoc:springdoc-openapi-ui:1.6.5'

Updates to application.yml

Additionally a springdoc field should be added to the module's application.yml. Here is an example for mod-agreements. The “packages-to-scan” would need to be updated to be correct for the module being worked on:

Code Block
springdoc:
  api-docs:
    path: /api
  packages-to-scan: org.olf.agreements.controllers
  swagger-ui:
    path: /docsui
      tryItOutEnabled: true
      operationsSorter: method
      tagsSorter: alpha
      filter: true

New configuration class

A configuration class for OpenAPI should be added within the config folder. This will be used to setup the top level fields of the OpenAPI json - including the Info object and the server objects. Note: this should be within the src/main/java file directory within the module.

An example is shown below:

Code Block
package org.olf.agreements.config;
import java.util.List;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
​
@Configuration
public class OpenAPIConfig {
​
  @Value("${someurl}")
  private String devUrl;
    
  @Value("${prod-url}")
  private String prodUrl;
    
  @Bean
  public OpenAPI myOpenAPI() {
    Server devServer = new Server();
    devServer.setUrl(devUrl);
    devServer.setDescription("Server URL in Development environment");
    
    Server prodServer = new Server();
    prodServer.setUrl(prodUrl);
    prodServer.setDescription("Server URL in Production environment");
    
    Contact contact = new Contact();
    contact.setEmail("some-contact-address@gmail.com");
    contact.setName("some-contact-name");
    contact.setUrl("https://some-contact-url.com");
    
    License mitLicense = new License().name("MIT License").url("https://some-license-url/");
    
    Info info = new Info()
        .title("Tutorial Management API")
        .version("1.0")
        .contact(contact)
        .description("This API exposes endpoints to manage tutorials.").termsOfService("https://www.some-terms-url.com/terms")
        .license(mitLicense);
    
    return new OpenAPI().info(info).servers(List.of(devServer, prodServer));
  }
}

Changes to module controller classes

Existing controller classes will need to be refactored into java removing any grails/groovy specific methods that are currently used. It may also be necessary to expose hidden methods which are currently created automatically by Grails.

An example set of annoationts for a class is as follows:

Code Block
@Tag(name = "Tutorial", description = "Tutorial management APIs")
@RestController
public class TestController {

Each method in controller will need an expected response body implemented directly into the controller. For example:

Code Block
@Data
@AllArgsConstructor
@Builder
@ToString
public static class DummyResponse {
  String code;
}

Below is an example of how the methods within said controller should be annotated, this includes the following annotations:

  • Operation annotation is used to define the OpenAPI Operation object which describes a single API operation on a path.

  • ApiResponses and ApiResponse annotations are used to define the OpenAPI Responses and Response objects respectively. These define the responses which can be returned from the method, including the HTTP response code and content, note that with a 200 response we also have the previously defined DummyResponse as the content for that ApiResponse.

  • Finally for this example we use the GetMapping annotation to define the endpoint for this method

Code Block
@Operation(
  summary = "Retrieve a Tutorial by Id",
  description = "Get a Tutorial object by specifying its id. The response is Tutorial object   with id, title, description and published status.",
  tags = { "one", "two" } )
@ApiResponses({
@ApiResponse(responseCode = "200", content = { @Content(schema = @Schema(implementation = DummyResponse.class), mediaType = "application/json") }),
@ApiResponse(responseCode = "404", content = { @Content(schema = @Schema()) }),
@ApiResponse(responseCode = "500", content = { @Content(schema = @Schema()) }) })
@GetMapping(value="/tutorials/{id}")
public @ResponseBody ResponseEntity<DummyResponse> getTutorialById(@PathVariable("id") long id) {
  DummyResponse dr = new DummyResponse("Wibble");
  return new ResponseEntity(dr, HttpStatus.OK);
}

A full example of this controller implementation is below, along with required package:

Code Block
package org.olf.agreements.controllers;
​
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.responses.*;
​
​
import java.util.ArrayList;
import java.util.List;
​
import org.springframework.http.MediaType;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
​
​
import org.springframework.web.bind.annotation.RequestMapping;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
​
import com.fasterxml.jackson.annotation.JsonView;
​
import java.util.Optional;
​
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Map;
​
​
// See https://github.com/bezkoder/spring-boot-3-rest-api-example
​
@Tag(name = "Tutorial", description = "Tutorial management APIs")
@RestController
public class TestController {
​
  @Data
  @AllArgsConstructor
  @Builder
  @ToString
  public static class DummyResponse {
    String code;
  }
​
  @Operation(
    summary = "Retrieve a Tutorial by Id",
    description = "Get a Tutorial object by specifying its id. The response is Tutorial object with id, title, description and published status.",
    tags = { "one", "two" } )
  @ApiResponses({
    @ApiResponse(responseCode = "200", content = { @Content(schema = @Schema(implementation = DummyResponse.class), mediaType = "application/json") }),
    @ApiResponse(responseCode = "404", content = { @Content(schema = @Schema()) }),
    @ApiResponse(responseCode = "500", content = { @Content(schema = @Schema()) }) })
  @GetMapping(value="/tutorials/{id}")
  public @ResponseBody ResponseEntity<DummyResponse> getTutorialById(@PathVariable("id") long id) {
    DummyResponse dr = new DummyResponse("Wibble");
    return new ResponseEntity(dr, HttpStatus.OK);
  }
}