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:
-
jackson-databind-nullable (org.openapitools): Habilita el manejo de Java 8 opcionales y otros tipos que aceptan valores NULL dentro de Jackson, útil para tratar con campos JSON que pueden estar ausentes.
-
springfox-swagger2 (io.springfox): Integra Swagger 2 para la documentación de API en una aplicación Spring Boot. Excluye las anotaciones swagger de io.swagger.core.v3 para evitar conflictos entre diferentes versiones de anotaciones swagger introducidas por otras dependencias.
-
swagger-core-jakarta (io.swagger.core.v3): Incluye la biblioteca swagger-core-jakarta, utilizada para el modelado API, que incluye anotaciones y funcionalidades principales.
-
spring-boot-starter-validation (org.springframework.boot): Integra Hibernate Validator y Validation API, proporcionando una experiencia perfecta para agregar capacidades de validación a las aplicaciones Spring Boot. Garantiza la compatibilidad con otros componentes Spring Boot 3.x
- Sin este, el proyecto se construirá y ejecutará, pero se ignorarán las validaciones.
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:
- Creo que sería bueno dejar el output en ${project.build.directory}/generated-sources/openapi/.
- En el futuro podríamos considerar hacer algo con los valores de apiPackage y modelPackage. Es posible que cuando generemos más código, las clases comiencen a chocar.
- useEnumCaseInsensitive no hace nada si no creamos convertidores personalizados y los registramos en WebMvcConfig. Más sobre eso justo debajo.
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:
- Si echamos un vistazo al OAS, vemos que espera un parámetro de consulta no requerido “country”. Si lo probamos, podemos obtener el siguiente cURL válido
curl -X GET "http://localhost:8080/article?country=ar" -H "accept: application/json"
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.
Próximos pasos
- Generar automáticamente la interfaz del feign client.
- Hacer que las interfaces de servicios utilicen interfaces generadas previamente.