Pollito Blog
January 16, 2024

Desarrollo basado en contratos 6: Usando un mejor plugin

Posted on January 16, 2024  •  6 minutes  • 1149 words  • Other languages:  English

Openapi generator plugin al rescate.

Consulta el repositorio de github

Esta es una continuación de Desarrollo basado en contratos 5: Las validaciones en el controlador no están funcionando… ¿Por qué? .

Todo lo que haremos aquí, lo puedes encontrar en el repositorio de github.

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

Tiempo de investigación

Por si no has leído el post anterior o no lo recuerdas, allá en Desarrollo impulsado por contratos: creación de microservicios desde cero dije:

[…] el banco compró esta biblioteca súper secreta y poderosa que proporciona algunas configuraciones de yaml + en build.gradle, en la compilación genera muchos textos repetitivos, relacionados con cosas como interfaces de controlador […]

Debido a que el banco es muy reservado al respecto, no tengo mucha información. Solo la línea de dependencia de build.gradle y que alguien dijo que fue creada y mantenida por NTT DATA Group .

Así que después del sabor amargo que me dejó en la boca el post anterior, me puse a investigar un poco al respecto. Fui a leer las clases generadas y una cosa me llamó la atención: todas las clases generadas comienzan con este comentario.

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

Me siento tan tonto por no haber leído eso antes. Estoy intentando hacer un clon barato de una herramienta existente; obviamente debería haber comenzado mirando los archivos internos de la herramienta.

Eso abrió un nuevo rabbit hole de los generadores, cuyo punto de partida es OpenAPI Generator

Sale Swagger Codegen, entra OpenAPI Generator

Para esto, comencé a crear la rama actual cdd-6 a partir de cdd-4, y pretendí que cdd-5 nunca sucedió, excepto por las mejoras en los archivos yaml de OAS.

Felicitaciones a Khanh Nguyen , he usado su blog Generar contrato API usando el complemento OpenAPI Generator Maven como guía. Aparece en la sección Presentaciones/Videos/Tutoriales/Libros del github de Generador OpenAPI.

Actualizando dependencias

Muchas dependencias ya no son necesarias, pero sí muchas nuevas. OpenAPI Generator se basa en:

Bajando a Spring Boot 3.1.7

Al momento de escribir este blog, tenemos que comprometernos con el downgrade de Spring Boot 3.2.1 a 3.1.7. Pero para que los paquetes de Jakarta funcionen, es un intercambio inteligente.

---

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]

Configurando el complemento

Así es como se ve el código del plugin en pom.xml:

<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>

Algunas notas:

Comportamiento extraño sobre enumeraciones que distinguen entre mayúsculas y minúsculas en métodos sobre-escritos por el controlador

Problema

Después de compilar el proyecto, crear controladores y hacer que cada controlador implemente su correspondiente interfaz generada automáticamente + sobreescribiendo los métodos necesarios, todo debería estar funcionando bien. Y algunas pruebas unitarias incluso lo confirman.

Pero al ejecutar la aplicación, sucede algo extraño con los métodos de los controladores en los que algún parámetro es un tipo de enumeración autogenerado.

Analicemos el comportamiento de /getArticles:

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

oas specification

pero al ejecutar nos sale el siguiente 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"
}

Y esto se vuelve más extraño cuando vamos al código generado automáticamente para CountryEnum y vemos que el método @JsonCreator es capaz de ignorar valores de mayúsculas y minúsculas.

@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 + "'");
}

Entonces, ¿cómo solucionarlo? Necesitamos crear convertidores personalizados para cada objeto generado tipo Enum que se utilice en un controlador y registrarlos en WebMvcConfig. Todavía no he encontrado alguna manera de hacer esto automáticamente.

Creando 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);
  }
}

Registrándolos

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

Miscelánea: Breve introducción a Pitest

No tiene ninguna relación con lo que se ha logrado en esta publicación, quería agregar pitest al proyecto. Tal y como afirma en su página :

PIT es un sistema de prueba de mutaciones de última generación que proporciona cobertura de prueba estándar de oro para Java y jvm.

Por el momento, solo considéralo como una métrica que dice qué tan buenas son nuestras pruebas. No buscamos alcanzar ningún porcentaje en particular, pero tal vez lo analicemos más en el futuro.

Agregue el siguiente complemento en 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>

y a partir de ahora, Maven test generará un informe que puede encontrar en target/pit-reports/index.html.

pit report

Próximos pasos

Hey, check me out!

You can find me here