Skip to content

Commit

Permalink
First draft to support unexpected enum values
Browse files Browse the repository at this point in the history
  • Loading branch information
brice-laurencin committed Apr 12, 2024
1 parent a19eb1b commit 11dc64f
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum ConfigName {
SKIP_FORM_MODEL("skip-form-model"),
MUTINY("mutiny"),
ADDITIONAL_MODEL_TYPE_ANNOTATIONS("additional-model-type-annotations"),
ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER("additional-enum-type-unexpected-member"),
ADDITIONAL_API_TYPE_ANNOTATIONS("additional-api-type-annotations"),
TYPE_MAPPINGS("type-mappings"),
IMPORT_MAPPINGS("import-mappings"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public class CommonItemConfig {
@ConfigItem(name = "additional-model-type-annotations")
public Optional<String> additionalModelTypeAnnotations;

/**
* Defines if the enums should have an `UNEXPECTED` member to convey values that cannot be parsed. Default is
* <code>false</code>.
*/
@ConfigItem(name = "additional-enum-type-unexpected-member")
public Optional<Boolean> additionalEnumTypeUnexpectedMemberAnnotations;

/**
* The specified annotations will be added to the generated api files
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ protected void generate(final Config config, final Path openApiFilePath, final P
getValues(config, openApiFilePath, CodegenConfig.ConfigName.ADDITIONAL_MODEL_TYPE_ANNOTATIONS, String.class)
.ifPresent(generator::withAdditionalModelTypeAnnotationsConfig);

generator.withAdditionalEnumTypeUnexpectedMemberConfig(
getValues(config, openApiFilePath, CodegenConfig.ConfigName.ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER,
Boolean.class)
.orElse(false));

getValues(config, openApiFilePath, CodegenConfig.ConfigName.ADDITIONAL_API_TYPE_ANNOTATIONS, String.class)
.ifPresent(generator::withAdditionalApiTypeAnnotationsConfig);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class QuteTemplatingEngineAdapter extends AbstractTemplatingEngineAdapter
public static final String IDENTIFIER = "qute";
public static final String[] INCLUDE_TEMPLATES = {
"additionalEnumTypeAnnotations.qute",
"additionalEnumTypeUnexpectedMember.qute",
"additionalModelTypeAnnotations.qute",
"beanValidation.qute",
"beanValidationCore.qute",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ public OpenApiClientGeneratorWrapper withAdditionalModelTypeAnnotationsConfig(fi
return this;
}

public OpenApiClientGeneratorWrapper withAdditionalEnumTypeUnexpectedMemberConfig(
final Boolean additionalEnumTypeUnexpectedMember) {
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMember", additionalEnumTypeUnexpectedMember);
return this;
}

/**
* Sets the global 'additionalApiTypeAnnotations' setting. If not set this setting will default to empty.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Special value if the API response contains some new value not declared in this enum.
* You should react accordingly.
*/
UNEXPECTED({#if e.isContainer}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf("unexpected")){#if e.allowableValues},{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{/if}
{#include additionalEnumTypeAnnotations.qute e=e /}public enum {e.enumName} {
{#if e.allowableValues}
{#if additionalEnumTypeUnexpectedMember}{#include additionalEnumTypeUnexpectedMember.qute e=e/}{/if}
{#if e.withXml}
{#for v in e.allowableValues.enumVars}@XmlEnumValue({#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}{v.value}{#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}) {v.name}({#if e.isEnum}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf({v.value})){#if v_hasNext}, {#else}; {/if}{/for}
{#else}
Expand Down Expand Up @@ -37,6 +38,6 @@
return b;
}
}
{#if e.useNullForUnknownEnumValue}return null;{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
{#if e.useNullForUnknownEnumValue}return null;{#else if additionalEnumTypeUnexpectedMember}return UNEXPECTED;{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.util.EnumSet;
@JsonIgnoreProperties(ignoreUnknown = true)
{#include additionalEnumTypeAnnotations.qute e=e/}public enum {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if} {
{#if e.allowableValues}
{#if additionalEnumTypeUnexpectedMember}{#include additionalEnumTypeUnexpectedMember.qute e=e/}{/if}
{#for v in e.allowableValues.enumVars}{v.name}({e.dataType}.valueOf({v.value})){#if v_hasNext}, {#else};{/if}{/for}
{/if}

Expand Down Expand Up @@ -38,6 +39,6 @@ import java.util.EnumSet;
return b;
}
}
{#if e.useNullForUnknownEnumValue}return null;{#else}throw new IllegalArgumentException("Unexpected value '" + text + "'");{/if}
{#if e.useNullForUnknownEnumValue}return null;{#else if additionalEnumTypeUnexpectedMember}return UNEXPECTED;{#else}throw new IllegalArgumentException("Unexpected value '" + text + "'");{/if}
}
}
}
95 changes: 95 additions & 0 deletions client/integration-tests/enum-unexpected/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-openapi-generator-integration-tests</artifactId>
<groupId>io.quarkiverse.openapi.generator</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-openapi-generator-it-enum-unexpected</artifactId>
<name>Quarkus - Openapi Generator - Integration Tests - Enum Unexpected</name>
<description>Example project for OpenAPI with enum unexpected value</description>

<dependencies>
<dependency>
<groupId>io.quarkiverse.openapi.generator</groupId>
<artifactId>quarkus-openapi-generator</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.0.3
info:
title: echo
version: '1.0.0'
description: ""
paths:
/echo:
post:
summary: Echo
operationId: echo
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Echo'
components:
schemas:
Echo:
type: object
required:
- msgType
properties:
msgType:
type: string
enum:
- 'text'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.0.3
info:
title: echo
version: '1.0.0'
description: ""
paths:
/echo:
post:
summary: Echo
operationId: echo
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Echo'
components:
schemas:
Echo:
type: object
required:
- msgType
properties:
msgType:
type: string
enum:
- 'text'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
quarkus.openapi-generator.codegen.spec.with_enum_unexpected_yaml.additional-enum-type-unexpected-member=true


quarkus.openapi-generator.codegen.spec.without_enum_unexpected_yaml.additional-enum-type-unexpected-member=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.quarkiverse.openapi.generator.it;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.Matchers.is;

import java.util.Collections;
import java.util.Map;

import jakarta.inject.Inject;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.AssertionsForClassTypes;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@QuarkusTestResource(EnumUnexpectedTest.EchoMockServer.class)
class EnumUnexpectedTest {

@RestClient
@Inject
org.openapi.quarkus.with_enum_unexpected_yaml.api.DefaultApi api;

@Test
void apiIsBeingGenerated() {

org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo echo = api.echo();

Assertions.assertThat(echo.getMsgType())
.isEqualTo(org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo.MsgTypeEnum.UNEXPECTED);
}

@Inject
ObjectMapper objectMapper;

@Test
void when_additional_enum_type_unexpected_member_is_true_should_have_extra_member() {
AssertionsForClassTypes.assertThat(org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo.MsgTypeEnum.class)
.hasOnlyPublicFields("TEXT", "UNEXPECTED");
}

@Test
void when_additional_enum_type_unexpected_is_false_should_not_have_extra_member() {
AssertionsForClassTypes.assertThat(org.openapi.quarkus.without_enum_unexpected_yaml.model.Echo.MsgTypeEnum.class)
.hasOnlyPublicFields("TEXT");
}

@Test
void when_additional_enum_type_unexpected_member_is_true_should_parse_unknown_values_to_UNEXPECTED()
throws JsonProcessingException {
org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo actualEcho = objectMapper.readValue("{ \"msgType\": \"NOPE\"}",
org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo.class);
MatcherAssert.assertThat(actualEcho.getMsgType(),
is(org.openapi.quarkus.with_enum_unexpected_yaml.model.Echo.MsgTypeEnum.UNEXPECTED));
}

@Test
void when_additional_enum_type_unexpected_member_is_false_should_fail_parsing_unknown_values()
throws JsonProcessingException {
assertThatThrownBy(() -> {
objectMapper.readValue("{ \"msgType\": \"NOPE\"}",
org.openapi.quarkus.without_enum_unexpected_yaml.model.Echo.class);
}).isInstanceOf(ValueInstantiationException.class);
}

public static class EchoMockServer implements QuarkusTestResourceLifecycleManager {

private WireMockServer wireMockServer;

@Override
public Map<String, String> start() {
configureWiremockServer();
return Collections.singletonMap("org.openapi.quarkus.with_enum_unexpected_yaml.api.DefaultApi/mp-rest/url",
wireMockServer.baseUrl());
}

private void configureWiremockServer() {
var wireMockConfiguration = WireMockConfiguration.wireMockConfig()
.extensions(new ResponseTemplateTransformer(false)).dynamicPort();
wireMockServer = new WireMockServer(wireMockConfiguration);
wireMockServer.start();

wireMockServer.stubFor(post(urlEqualTo("/echo"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{ \"msgType\": \"NOPE\"}")
.withTransformers("response-template")));
}

@Override
public void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}
}
}
1 change: 1 addition & 0 deletions client/integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<module>cookie-authentication</module>
<module>custom-templates</module>
<module>enum-property</module>
<module>enum-unexpected</module>
<module>exclude</module>
<module>generation-input</module>
<module>generation-tests</module>
Expand Down

0 comments on commit 11dc64f

Please sign in to comment.