Skip to content

Commit

Permalink
feat: handle non-string primitive payloads (#726)
Browse files Browse the repository at this point in the history
* feat: handle non-string primitive payloads

* test: update tests

* feat: minor code improvement

* fix: handle potential null pointer in resolveSchema

* chore: formatting
  • Loading branch information
timonback authored May 17, 2024
1 parent 99723f2 commit 2d8b462
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,8 +120,17 @@ public MessageReference registerMessage(MessageObject message) {
}

private String getSchemaName(Class<?> type, Map<String, Schema> schemas) {
if (schemas.isEmpty() && type.equals(String.class)) {
return registerString();
if (schemas.isEmpty()) {
// 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());
}
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) {
Expand Down Expand Up @@ -191,9 +202,8 @@ private void processAsyncApiPayloadAnnotation(Map<String, Schema> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
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;
import io.swagger.v3.oas.annotations.media.Schema;
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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<String, SchemaObject> 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<String, SchemaObject> 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<String, SchemaObject> 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<String, SchemaObject> 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<String, SchemaObject> schemas = componentsService.getSchemas();
assertThat(schemas).hasSize(1).containsKey("java.lang.Boolean");
}
}

@Data
@NoArgsConstructor
@Schema(name = "DifferentName")
Expand All @@ -143,7 +203,7 @@ private static class ClassWithSchemaAnnotation {

@Data
@NoArgsConstructor
private static class OneFieldFooWithFqn {
private static class OneFieldFooWithoutFqn {
private String s;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ void testContextWithApplicationProperties() {

@Test
void testAllChannelsAreFound() {
assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(10);
assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(11);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,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())))
Expand Down Expand Up @@ -306,7 +306,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())))
Expand Down Expand Up @@ -386,7 +386,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())))
Expand Down

0 comments on commit 2d8b462

Please sign in to comment.