Pollito Dev
January 16, 2024

Contract-Driven Development 6: Using a better plugin

Posted on January 16, 2024  •  6 minutes  • 1123 words  • Other languages:  Español

Openapi generator plugin to the rescue.

Check the github repo

This is a continuation of Contract-Driven Development 5: Controller validations aren’t working… Why? .

Everything we’ll do here, you can find in in the github repo.

Spring City Explorer - Backend: Branch feature/cdd-6

Investigation time

In case you haven’t read previous post or don’t remember, back in Contract-Driven Development: Crafting Microservices from the Ground Up I said:

[…] the bank bought this super secret all powerful library that given some yaml + configurations in the build.gradle, on build generates lots of boilerplate, related to things such as controller interfaces […]

Cause the bank is very secretive about it, I don’t have much info. Only the dependency line from the build.gradle and that someone said it was made and mantained by NTT DATA Group .

So after the sour taste that the previous post left in my mouth, I went to investigate a little bit about it. I went to read the generated classes, and one thing caught my attention: all the generated classes start with this comment.

/**
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.6.0)
*/

I feel so silly that I haven’t read that before. I’m trying to make a cheap clone of an existing tool, obviously I should’ve started by looking at the tool inner files.

So that opened a whole new rabbit hole in generators, which starting point is OpenAPI Generator

Yeeting Swagger Codegen, adding OpenAPI Generator

For this, I started to create the current branch cdd-6 out of cdd-4, and pretend cdd-5 never really happened, except for the improvements in the OAS yaml files.

Kudos to Khanh Nguyen , I’ve used his blog Generate API contract using OpenAPI Generator Maven plugin as a guide. It is featured in the Presentations/Videos/Tutorials/Books section of the OpenAPI Generator github.

Updating dependencies

A lot of dependencies are not needed anymore, a lot of new ones are. OpenAPI Generator relies on:

Downgrading to Spring Boot 3.1.7

At the moment of writing this blog, we have to compromise downgrading from Spring Boot 3.2.1 to 3.1.7. But for having jakarta packages working, it is a smart interchange.

---

APPLICATION FAILED TO START

---

Description:

Your project setup is incompatible with our requirements due to following reasons:

- Spring Boot [3.2.1] is not compatible with this Spring Cloud release train

Action:

Consider applying the following actions:

- Change Spring Boot version to one of the following versions [3.0.x, 3.1.x] .
  You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].
  If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.
  If you want to disable this check, just set the property [spring.cloud.compatibility-verifier.enabled=false]

Configuring the plugin

Here’s how the plugin code in the pom.xml looks:

<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.2.0</version>
  <executions>
    <execution>
      <id>generation from springcityexplorer</id>
      <goals>
        <goal>generate</goal>
      </goals>
      <configuration>
        <inputSpec>${project.basedir}/src/main/resources/openapi/springcityexplorer.yaml</inputSpec>
        <generatorName>spring</generatorName>
        <output>${project.build.directory}/generated-sources/openapi/springcityexplorer/</output>
        <apiPackage>dev.pollito.springcityexplorer.api</apiPackage>
        <modelPackage>dev.pollito.springcityexplorer.models</modelPackage>
        <configOptions>
          <interfaceOnly>true</interfaceOnly>
          <useSpringBoot3>true</useSpringBoot3>
          <useEnumCaseInsensitive>true</useEnumCaseInsensitive>
        </configOptions>
      </configuration>
    </execution>
  </executions>
</plugin>

Some notes:

Weird behaviour about case-sensitives enums in controller overriden methods

Issue

After compiling the project, creating controllers, and making each controller implements their correspoinding autogenerated interface + overriding the needed methods, everything should be working ok. And some unit test even confirm that.

But when running the application, something weird happens with those controllers methods in which some parameter is an autogenerated enum type.

Let’s analyze the /getArticles behaviour:

curl -X GET "http://localhost:8080/article?country=ar" -H  "accept: application/json"

oas specification

but when executing, we get the following error

{
  "timestamp": "2024-01-17T00:40:29.555+00:00",
  "status": 400,
  "error": "Bad Request",
  "message": "Failed to convert value of type 'java.lang.String' to required type 'dev.pollito.springcityexplorer.models.CountryEnum'; Failed to convert from type [java.lang.String] to type [@io.swagger.v3.oas.annotations.Parameter @jakarta.validation.Valid @org.springframework.web.bind.annotation.RequestParam dev.pollito.springcityexplorer.models.CountryEnum] for value [ar]",
  "path": "/article"
}

And this gets weirder when we go to the autogenerated code for CountryEnum and we see the @JsonCreator method is capable of ignore case values.

@JsonCreator
public static CountryEnum fromValue(String value) {
  for (CountryEnum b : CountryEnum.values()) {
    if (b.value.equalsIgnoreCase(value)) {
      return b;
    }
  }
  throw new IllegalArgumentException("Unexpected value '" + value + "'");
}

So how to fix it? We need to create custom converters for every Enum-like generated object being used in a controller, and register them in WebMvcConfig. I haven’t yet found some way to do this automatically.

Creating converters

public class StringToCountryEnumConverter implements Converter<String, CountryEnum> {
  @Override
  public CountryEnum convert(String source) {
    return CountryEnum.fromValue(source);
  }
}
public class StringToSortOrderEnumConverter implements Converter<String, SortOrderEnum> {
  @Override
  public SortOrderEnum convert(String source) {
    return SortOrderEnum.fromValue(source);
  }
}

Register them

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToCountryEnumConverter());
    registry.addConverter(new StringToSortOrderEnumConverter());
  }

Misc: Brief intro to Pitest

Totally unrelated what’s been archieved in this post, I wanted to add pitest to the project. As it states in its page :

PIT is a state of the art mutation testing system, providing gold standard test coverage for Java and the jvm

At the moment, just see it like some metrics that says how good are our tests. We are not looking for reaching any particular percentage in particular, but we will maybe look further into it in the future.

Add the following plugin in pom.xml

<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.16.3</version>
  <executions>
    <execution>
      <id>pit-report</id>
      <phase>test</phase>
      <goals>
        <goal>mutationCoverage</goal>
      </goals>
    </execution>
  </executions>
  <dependencies>
    <dependency>
      <groupId>org.pitest</groupId>
      <artifactId>pitest-junit5-plugin</artifactId>
      <version>1.2.1</version>
    </dependency>
  </dependencies>
  <configuration>
    <excludedClasses>
      <param>dev.pollito.springcityexplorer.api.*</param>
      <param>dev.pollito.springcityexplorer.models.*</param>
    </excludedClasses>
  </configuration>
</plugin>

and from now, on maven test, it will generate a report that you can find in target/pit-reports/index.html.

pit report

Next steps

Hey, check me out!

You can find me here