2

I've been playing with swagger codegen for a springboot project. I am now able to automatically generate the client code in java, ignore the generation of the data models (importing my own in the swagger-codegen-maven-plugin via importMapping). Furthermore, I am also able to:

  • specify tags directly in the springboot code to help organize the openapi contract,
  • use the @Operation annotation to specify method names,
  • use the @ApiResponses annotation to detail the expected response codes and descriptions,etc.

Example below:

@RestController
@RequestMapping(value="/api/external/")
@Tag(name = "addResources", description = "The Add Resources API")
public class ControllerAddResources {

    private final ServiceInterAddResources externalSources;

    public ControllerAddResources(
            ServiceInterAddResources externalSources){
        this.externalSources = externalSources;
    }

    @Operation(operationId="importDataV3", summary = "Import Data V3", description = "Import a Data V3 file from source", tags = { "addResources" })
    @PostMapping(path="/{source}/datav3", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public @ResponseBody String importDataV3(
            @PathVariable String source,
            MultipartFile file) throws  ExceptionInvalidDataV3, IOException{


        return externalSources.importDataV3(source, file);
    }

All of this is built into the openapi contract which is then used by codegen to generate the client library (in this case, I'm using resttemplate), such as the one below:

@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.JavaClientCodegen", date = "2020-03-05T12:15:11.537Z[Europe/Lisbon]")@Component("com.xxx.connector.api.AddResourcesApi")
public class AddResourcesApi {
    private ApiClient apiClient;

    public AddResourcesApi() {
        this(new ApiClient());
    }

    @Autowired
    public AddResourcesApi(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    public ApiClient getApiClient() {
        return apiClient;
    }

    public void setApiClient(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    /**
     * Import Data V3
     * Import a Data V3 file from source
     * <p><b>412</b> - default response
     * <p><b>409</b> - default response
     * <p><b>200</b> - default response
     * @param source The source parameter
     * @param file The file parameter
     * @return String
     * @throws RestClientException if an error occurs while attempting to invoke the API
     */
    public String importDataV3(String source, File file) throws RestClientException {
        Object postBody = null;
        // verify the required parameter 'source' is set
        if (source == null) {
            throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'source' when calling importDataV3");
        }
        // create path and map variables
        final Map<String, Object> uriVariables = new HashMap<String, Object>();
        uriVariables.put("source", source);
        String path = UriComponentsBuilder.fromPath("/api/external/{source}/datav3").buildAndExpand(uriVariables).toUriString();

        final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
        final HttpHeaders headerParams = new HttpHeaders();
        final MultiValueMap<String, Object> formParams = new LinkedMultiValueMap<String, Object>();
        if (file != null)
            formParams.add("file", new FileSystemResource(file));

        final String[] accepts = { 
            "*/*", "application/json"
         };
        final List<MediaType> accept = apiClient.selectHeaderAccept(accepts);
        final String[] contentTypes = { 
            "multipart/form-data"
         };
        final MediaType contentType = apiClient.selectHeaderContentType(contentTypes);

        String[] authNames = new String[] {  };

        ParameterizedTypeReference<String> returnType = new ParameterizedTypeReference<String>() {};
        return apiClient.invokeAPI(path, HttpMethod.POST, queryParams, postBody, headerParams, formParams, accept, contentType, authNames, returnType);
    }

In the generated sources, codegen also includes scaffolds for junit tests, like the one below:

    /**
     * API tests for AddResourcesApi
     */
    @Ignore
    public class AddResourcesApiTest {

        private final AddResourcesApi api = new AddResourcesApi();

        /**
         * Import Data V2
         *
         * Import an Data V2 file from source
         *
         * @throws ApiException
         *          if the Api call fails
         */
        @Test
        public void imporDataV2Test() {
            String source = null;
            File file = null;
            String response = api.importDataV2(source, file);

            // TODO: test validations
        }
        /**
         * Import Data V3
         *
         * Import an Data V3 file from source [%source%]
         *
         * @throws ApiException
         *          if the Api call fails
         */
        @Test
        public void importDataV3Test() {
            String source = null;
            File file = null;
            String response = api.importDataV3(source, file);

            // TODO: test validations
        }
    }

However, the validation code is empty, as expected. Since the client is being continuously generated in a CI/CD environment and being deployed to a dependency management system that I'm running internally (Artifactory), this defeats the purpose of manually writing the tests each time I execute codegen.

Is there a way of specifying the validation code (or validation requisites) directly using java annotations (or a templating mechanism) at the springboot project level? This would allow for fully automated client library generation with testing included.

Paulo Maia
  • 21
  • 1
  • 6
  • 1
    What do you mean by `I am now able to automatically generate the client code in java` ? You mean model-classes or other code as well. Eventually, you need to write business logic even if you're able to generate some models, method names, REST controller interfaces(other than a contract), right?. I don't think you can generate test-cases that covers your business scenarios unless you have some constant logics... you need to override generated test-cases and write them manually to make your CICD fully automated. – Shekhar Rai Mar 05 '20 at 16:53
  • Hi @ShekharRai. I edited my qestion for clarity. I am writing the controllers, models, etc in the springboot project, and using the `springdoc-openapi-ui`'s annotations to give better context to the openapi contract specification. With this specification (openapi.json or yaml), I have a generator project that uses this contract to generate the Java Connector Client. This is done automatically via CI/CD, and allows me to immediately make available a connector for our API that other devs can use, removing the need for them to implement the client code (just adding the dependency in the pom). – Paulo Maia Mar 05 '20 at 17:54
  • @ShekharRai, as you can see in the examples in the question, the client code is quite complete. This means that once I change the springboot project (e.g., the controllers) I don't need to rewrite the client code, as this will be properly generated. For the java client tests, however, this is not the case. What to you mean by "you need to override generated test-cases and write them manually"? How can I do this ? – Paulo Maia Mar 05 '20 at 17:59
  • Yes I can see the generated code - and I believe you're generating them in the `build` stage. what I meant is you should write test-cases before running your pipeline - in your **local machine** – Shekhar Rai Mar 06 '20 at 01:37

0 Answers0