From b13877f4dffa28116912e0f45d197fafa56d1fe9 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 26 Apr 2024 20:04:49 +0200 Subject: [PATCH 1/5] feat: handle non-string primitive payloads --- .../components/DefaultComponentsService.java | 20 +++-- .../DefaultComponentsServiceTest.java | 64 ++++++++++++- .../kafka/consumers/IntegerConsumer.java | 19 ++++ .../src/test/resources/asyncapi.json | 89 +++++++++++++++++++ 4 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/IntegerConsumer.java diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java index b1ada9e67..e3ad8c2dd 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java @@ -11,6 +11,8 @@ import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.jackson.TypeNameResolver; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.NumberSchema; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; @@ -127,8 +129,17 @@ public MessageReference registerMessage(MessageObject message) { } private String getSchemaName(Class type, Map schemas) { - if (schemas.isEmpty() && type.equals(String.class)) { - return registerString(); + if (schemas.isEmpty()) { + // swagger-parser does create schemas for primitives + if (type.equals(String.class) || type.equals(Character.class) || type.equals(Byte.class)) { + return registerPrimitive(String.class, new StringSchema()); + } + if (Boolean.class.isAssignableFrom(type)) { + return registerPrimitive(Boolean.class, new BooleanSchema()); + } + if (Number.class.isAssignableFrom(type)) { + return registerPrimitive(Number.class, new NumberSchema()); + } } if (schemas.size() == 1) { @@ -200,9 +211,8 @@ private void processAsyncApiPayloadAnnotation(Map schemas, Strin } } - private String registerString() { - String schemaName = getNameFromClass(String.class); - StringSchema schema = new StringSchema(); + private String registerPrimitive(Class type, Schema schema) { + String schemaName = getNameFromClass(type); schema.setName(schemaName); this.schemas.put(schemaName, schema); diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsServiceTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsServiceTest.java index fabf80975..e37950c3a 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsServiceTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsServiceTest.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; import io.github.springwolf.core.asyncapi.components.postprocessors.SchemasPostProcessor; import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties; import io.swagger.v3.core.util.Json; @@ -14,6 +15,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -99,7 +101,7 @@ void getDefinitionWithoutFqnClassName() throws IOException { // when Class clazz = - OneFieldFooWithFqn.class; // swagger seems to cache results. Therefore, a new class must be used. + OneFieldFooWithoutFqn.class; // swagger seems to cache results. Therefore, a new class must be used. componentsServiceWithFqn.registerSchema(clazz, "content-type-not-relevant"); String actualDefinitions = objectMapper.writer(printer).writeValueAsString(componentsServiceWithFqn.getSchemas()); @@ -133,6 +135,64 @@ void postProcessorIsSkippedWhenSchemaWasRemoved() { verifyNoInteractions(schemasPostProcessor2); } + @Nested + class RegisterSchema { + + @Test + void Integer() { + // when + componentsService.registerSchema(Integer.class, "content-type-not-relevant"); + + // then + Map schemas = componentsService.getSchemas(); + assertThat(schemas).hasSize(1).containsKey("java.lang.Number"); + assertThat(schemas.get("java.lang.Number").getType()).isEqualTo("number"); + } + + @Test + void Double() { + // when + componentsService.registerSchema(Double.class, "content-type-not-relevant"); + + // then + Map schemas = componentsService.getSchemas(); + assertThat(schemas).hasSize(1).containsKey("java.lang.Number"); + assertThat(schemas.get("java.lang.Number").getType()).isEqualTo("number"); + } + + @Test + void String() { + // when + componentsService.registerSchema(String.class, "content-type-not-relevant"); + + // then + Map schemas = componentsService.getSchemas(); + assertThat(schemas).hasSize(1).containsKey("java.lang.String"); + assertThat(schemas.get("java.lang.String").getType()).isEqualTo("string"); + } + + @Test + void Byte() { + // when + componentsService.registerSchema(Byte.class, "content-type-not-relevant"); + + // then + Map schemas = componentsService.getSchemas(); + assertThat(schemas).hasSize(1).containsKey("java.lang.String"); + assertThat(schemas.get("java.lang.String").getType()).isEqualTo("string"); + } + + @Test + void Boolean() { + // when + componentsService.registerSchema(Boolean.class, "content-type-not-relevant"); + + // then + Map schemas = componentsService.getSchemas(); + assertThat(schemas).hasSize(1).containsKey("java.lang.Boolean"); + } + } + @Data @NoArgsConstructor @Schema(name = "DifferentName") @@ -143,7 +203,7 @@ private static class ClassWithSchemaAnnotation { @Data @NoArgsConstructor - private static class OneFieldFooWithFqn { + private static class OneFieldFooWithoutFqn { private String s; } } diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/IntegerConsumer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/IntegerConsumer.java new file mode 100644 index 000000000..1eb83f1a3 --- /dev/null +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/IntegerConsumer.java @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.examples.kafka.consumers; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class IntegerConsumer { + private static final String TOPIC = "integer-topic"; + + @KafkaListener(topics = TOPIC) + public void receiveIntegerPayload(Integer integerPayload) { + log.info("Received new message in {}: {}", TOPIC, integerPayload); + } +} diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json index f6d8acae6..74c2b9125 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -53,6 +53,18 @@ } } }, + "integer-topic": { + "messages": { + "java.lang.Number": { + "$ref": "#/components/messages/java.lang.Number" + } + }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + } + }, "multi-payload-topic": { "messages": { "io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto": { @@ -428,6 +440,39 @@ "type": "object" } }, + "SpringKafkaDefaultHeaders-Integer": { + "type": "object", + "properties": { + "__TypeId__": { + "type": "string", + "description": "Spring Type Id Header", + "enum": [ + "java.lang.Number" + ], + "examples": [ + "java.lang.Number" + ] + } + }, + "examples": [ + { + "__TypeId__": "java.lang.Number" + } + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "java.lang.Number" + ], + "type": "string" + } + }, + "type": "object" + } + }, "SpringKafkaDefaultHeaders-Message": { "type": "object", "properties": { @@ -1091,6 +1136,16 @@ "type": "string" } }, + "java.lang.Number": { + "type": "number", + "examples": [ + 1.1 + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "type": "number" + } + }, "java.lang.String": { "type": "string", "examples": [ @@ -1315,6 +1370,24 @@ } } }, + "java.lang.Number": { + "headers": { + "$ref": "#/components/schemas/SpringKafkaDefaultHeaders-Integer" + }, + "payload": { + "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", + "schema": { + "$ref": "#/components/schemas/java.lang.Number" + } + }, + "name": "java.lang.Number", + "title": "Integer", + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + } + }, "java.lang.String": { "headers": { "$ref": "#/components/schemas/SpringKafkaDefaultHeaders-String" @@ -1417,6 +1490,22 @@ } ] }, + "integer-topic_receive_receiveIntegerPayload": { + "action": "receive", + "channel": { + "$ref": "#/channels/integer-topic" + }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + }, + "messages": [ + { + "$ref": "#/channels/integer-topic/messages/java.lang.Number" + } + ] + }, "multi-payload-topic_receive_ExampleClassLevelKafkaListener": { "action": "receive", "channel": { From 0281f46de052617e0530799e5fed3ad1f059312f Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 27 Apr 2024 19:34:33 +0200 Subject: [PATCH 2/5] test: update tests --- .../examples/kafka/SpringContextIntegrationTest.java | 2 +- .../CloudStreamFunctionChannelsScannerIntegrationTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/SpringContextIntegrationTest.java index e51884438..9eaa2e370 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/SpringContextIntegrationTest.java @@ -48,7 +48,7 @@ void testContextWithApplicationProperties() { @Test void testAllChannelsAreFound() { - assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(10); + assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(11); } } diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScannerIntegrationTest.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScannerIntegrationTest.java index d13095d40..de166abe4 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScannerIntegrationTest.java @@ -223,7 +223,7 @@ void testFunctionBinding() { .name(Integer.class.getName()) .title(Integer.class.getSimpleName()) .payload(MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(Integer.class.getSimpleName())) + .schema(SchemaReference.fromSchema(Number.class.getSimpleName())) .build())) .headers(MessageHeaders.of( MessageReference.toSchema(AsyncHeadersNotDocumented.NOT_DOCUMENTED.getTitle()))) @@ -302,7 +302,7 @@ void testKStreamFunctionBinding() { .name(Integer.class.getName()) .title(Integer.class.getSimpleName()) .payload(MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(Integer.class.getName())) + .schema(SchemaReference.fromSchema(Number.class.getName())) .build())) .headers(MessageHeaders.of( MessageReference.toSchema(AsyncHeadersNotDocumented.NOT_DOCUMENTED.getTitle()))) @@ -382,7 +382,7 @@ void testFunctionBindingWithSameTopicName() { .name(Integer.class.getName()) .title(Integer.class.getSimpleName()) .payload(MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(Integer.class.getName())) + .schema(SchemaReference.fromSchema(Number.class.getName())) .build())) .headers(MessageHeaders.of( MessageReference.toSchema(AsyncHeadersNotDocumented.NOT_DOCUMENTED.getTitle()))) From 70d14af1653f1d8764a1aabe29a58aea3befb72d Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 27 Apr 2024 19:35:17 +0200 Subject: [PATCH 3/5] feat: minor code improvement --- .../asyncapi/v3/model/channel/message/MessageHeaders.java | 2 ++ .../core/asyncapi/components/DefaultComponentsService.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/channel/message/MessageHeaders.java b/springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/channel/message/MessageHeaders.java index 7173c9f52..2a4e92a61 100644 --- a/springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/channel/message/MessageHeaders.java +++ b/springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/channel/message/MessageHeaders.java @@ -7,10 +7,12 @@ import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.ToString; @Getter @JsonSerialize(using = MessageHeadersSerializer.class) @EqualsAndHashCode +@ToString public class MessageHeaders { private MultiFormatSchema multiFormatSchema; private SchemaObject schema; diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java index e3ad8c2dd..533063c9f 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java @@ -130,7 +130,7 @@ public MessageReference registerMessage(MessageObject message) { private String getSchemaName(Class type, Map schemas) { if (schemas.isEmpty()) { - // swagger-parser does create schemas for primitives + // swagger-parser does not create schemas for primitives if (type.equals(String.class) || type.equals(Character.class) || type.equals(Byte.class)) { return registerPrimitive(String.class, new StringSchema()); } From f20b21d800037512ee488d5479018c7261142e08 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 27 Apr 2024 19:35:59 +0200 Subject: [PATCH 4/5] fix: handle potential null pointer in resolveSchema --- .../core/asyncapi/components/ComponentsService.java | 2 ++ .../core/asyncapi/scanners/common/payload/PayloadService.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/ComponentsService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/ComponentsService.java index f51322574..1d9d8d26d 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/ComponentsService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/ComponentsService.java @@ -5,6 +5,7 @@ import io.github.springwolf.asyncapi.v3.model.channel.message.MessageObject; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference; import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; +import jakarta.annotation.Nullable; import java.util.Map; @@ -12,6 +13,7 @@ public interface ComponentsService { Map getSchemas(); + @Nullable SchemaObject resolveSchema(String schemaName); String registerSchema(SchemaObject headers); diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java index 69342a90f..3b5dec628 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java @@ -48,7 +48,9 @@ private NamedSchemaObject buildSchema(String contentType, Class payloadType) String componentsSchemaName = this.componentsService.registerSchema(payloadType, contentType); SchemaObject schema = componentsService.resolveSchema(componentsSchemaName); - schema.setTitle(payloadType.getSimpleName()); + if(schema != null) { + schema.setTitle(payloadType.getSimpleName()); + } return new NamedSchemaObject(componentsSchemaName, schema); } From 0e196b396eaa2ab15e373599912851908c5fb15d Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 27 Apr 2024 19:43:15 +0200 Subject: [PATCH 5/5] chore: formatting --- .../core/asyncapi/scanners/common/payload/PayloadService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java index 3b5dec628..08ce2c654 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/payload/PayloadService.java @@ -48,7 +48,7 @@ private NamedSchemaObject buildSchema(String contentType, Class payloadType) String componentsSchemaName = this.componentsService.registerSchema(payloadType, contentType); SchemaObject schema = componentsService.resolveSchema(componentsSchemaName); - if(schema != null) { + if (schema != null) { schema.setTitle(payloadType.getSimpleName()); }