diff --git a/springwolf-asyncapi/src/main/java/io/github/stavshamir/springwolf/asyncapi/v3/model/channel/message/MessageReference.java b/springwolf-asyncapi/src/main/java/io/github/stavshamir/springwolf/asyncapi/v3/model/channel/message/MessageReference.java index e1148773d..fcffe4dcd 100644 --- a/springwolf-asyncapi/src/main/java/io/github/stavshamir/springwolf/asyncapi/v3/model/channel/message/MessageReference.java +++ b/springwolf-asyncapi/src/main/java/io/github/stavshamir/springwolf/asyncapi/v3/model/channel/message/MessageReference.java @@ -48,4 +48,11 @@ public static MessageReference toChannelMessage(String channelName, String messa public static MessageReference toSchema(String schemaName) { return new MessageReference("#/components/schemas/" + schemaName); } + + public static String extractRefName(String ref) { + if (ref.contains("/")) { + return ref.substring(ref.lastIndexOf('/') + 1); + } + return ref; + } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java index bbb446dd2..1881b47af 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java @@ -49,7 +49,7 @@ public AsyncAPI getAsyncAPI() { /** * Does the 'heavy work' of building the AsyncAPI documents once. Stores the resulting - * AsyncAPI document or alternativly a catched exception/error in the instance variable asyncAPIResult. + * AsyncAPI document or alternatively a caught exception/error in the instance variable asyncAPIResult. */ protected synchronized void initAsyncAPI() { if (this.asyncAPIResult != null) { @@ -90,7 +90,7 @@ protected synchronized void initAsyncAPI() { } this.asyncAPIResult = new AsyncAPIResult(asyncAPI, null); - log.debug("AsyncAPI document was build"); + log.debug("AsyncAPI document was built"); } catch (Throwable t) { log.debug("Failed to build AsyncAPI document", t); this.asyncAPIResult = new AsyncAPIResult(null, t); diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/DefaultComponentsService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/DefaultComponentsService.java index ee5e83a1a..6b4924ce5 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/DefaultComponentsService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/DefaultComponentsService.java @@ -77,8 +77,8 @@ public String registerSchema(Class type) { log.debug("Registering schema for {}", type.getSimpleName()); Map schemas = new LinkedHashMap<>(runWithFqnSetting((unused) -> converter.readAll(type))); - String schemaName = getSchemaName(type, schemas); + String schemaName = getSchemaName(type, schemas); preProcessSchemas(schemas, schemaName, type); this.schemas.putAll(schemas); schemas.values().forEach(this::postProcessSchema); @@ -170,6 +170,12 @@ private R runWithFqnSetting(Function callable) { } private void postProcessSchema(Schema schema) { - schemaPostProcessors.forEach(processor -> processor.process(schema, schemas)); + for (SchemasPostProcessor processor : schemaPostProcessors) { + processor.process(schema, schemas); + + if (!schemas.containsValue(schema)) { + break; + } + } } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessor.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessor.java index 1de83a3e4..00dff2abe 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessor.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessor.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.schemas.postprocessor; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; import io.swagger.v3.oas.models.media.Schema; import org.springframework.util.StringUtils; @@ -22,10 +23,17 @@ public class AvroSchemaPostProcessor implements SchemasPostProcessor { @Override public void process(Schema schema, Map definitions) { removeAvroSchemas(definitions); - removeAvroProperties(schema); + removeAvroProperties(schema, definitions); } - private void removeAvroProperties(Schema schema) { + private void removeAvroProperties(Schema schema, Map definitions) { + if (schema.get$ref() != null) { + String schemaName = MessageReference.extractRefName(schema.get$ref()); + if (definitions.containsKey(schemaName)) { + removeAvroProperties(definitions.get(schemaName), definitions); + } + } + Map properties = schema.getProperties(); if (properties != null) { Schema schemaPropertySchema = properties.getOrDefault(SCHEMA_PROPERTY, null); @@ -37,6 +45,8 @@ private void removeAvroProperties(Schema schema) { properties.remove(SPECIFIC_DATA_PROPERTY); } } + + properties.forEach((key, value) -> removeAvroProperties(value, definitions)); } } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java index 6975f2f53..e74956cbd 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java @@ -35,13 +35,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; class DefaultSchemasServiceTest { private final SchemasPostProcessor schemasPostProcessor = Mockito.mock(SchemasPostProcessor.class); + private final SchemasPostProcessor schemasPostProcessor2 = Mockito.mock(SchemasPostProcessor.class); private final ComponentsService componentsService = new DefaultComponentsService( List.of(), - List.of(new ExampleGeneratorPostProcessor(new ExampleJsonGenerator()), schemasPostProcessor), + List.of( + new ExampleGeneratorPostProcessor(new ExampleJsonGenerator()), + schemasPostProcessor, + schemasPostProcessor2), new SwaggerSchemaUtil(), new SpringwolfConfigProperties()); @@ -165,6 +171,22 @@ void postProcessorsAreCalled() { componentsService.registerSchema(FooWithEnum.class); verify(schemasPostProcessor).process(any(), any()); + verify(schemasPostProcessor2).process(any(), any()); + } + + @Test + void postProcessorIsSkippedWhenSchemaWasRemoved() { + doAnswer(invocationOnMock -> { + Map schemas = invocationOnMock.getArgument(1); + schemas.clear(); + return null; + }) + .when(schemasPostProcessor) + .process(any(), any()); + + componentsService.registerSchema(FooWithEnum.class); + + verifyNoInteractions(schemasPostProcessor2); } private String jsonResource(String path) throws IOException { diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessorTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessorTest.java index e0578a996..0bbc2e18e 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessorTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/postprocessor/AvroSchemaPostProcessorTest.java @@ -26,6 +26,7 @@ void avroSchemasAreRemovedTest() { Map.of("foo", new StringSchema(), "schema", avroSchema, "specificData", avroSpecificData))); var definitions = new HashMap(); + definitions.put("schema", schema); definitions.put("customClassRefUnusedInThisTest", new StringSchema()); definitions.put("org.apache.avro.Schema", new io.swagger.v3.oas.models.media.Schema()); definitions.put("org.apache.avro.ConversionJava.lang.Object", new io.swagger.v3.oas.models.media.Schema()); @@ -35,6 +36,48 @@ void avroSchemasAreRemovedTest() { // then assertThat(schema.getProperties()).isEqualTo(Map.of("foo", new StringSchema())); - assertThat(definitions).isEqualTo(Map.of("customClassRefUnusedInThisTest", new StringSchema())); + assertThat(definitions) + .isEqualTo(Map.of("schema", schema, "customClassRefUnusedInThisTest", new StringSchema())); + } + + @Test + void avroSchemasAreRemovedInRefsTest() { + // given + var avroSchema = new io.swagger.v3.oas.models.media.Schema(); + avroSchema.set$ref("#/components/schemas/org.apache.avro.Schema"); + + var avroSpecificData = new io.swagger.v3.oas.models.media.Schema(); + avroSpecificData.set$ref("#/components/schemas/org.apache.avro.specific.SpecificData"); + + var refSchema = new io.swagger.v3.oas.models.media.Schema(); + refSchema.setProperties(new HashMap<>( + Map.of("foo", new StringSchema(), "schema", avroSchema, "specificData", avroSpecificData))); + + var refProperty = new io.swagger.v3.oas.models.media.Schema(); + refProperty.set$ref("#/components/schemas/refSchema"); + var schema = new io.swagger.v3.oas.models.media.Schema(); + schema.setProperties(new HashMap<>(Map.of("ref", refProperty))); + + var definitions = new HashMap(); + definitions.put("schema", schema); + definitions.put("refSchema", refSchema); + definitions.put("customClassRefUnusedInThisTest", new StringSchema()); + definitions.put("org.apache.avro.Schema", new io.swagger.v3.oas.models.media.Schema()); + definitions.put("org.apache.avro.ConversionJava.lang.Object", new io.swagger.v3.oas.models.media.Schema()); + + // when + processor.process(schema, definitions); + + // then + assertThat(schema.getProperties()).isEqualTo(Map.of("ref", refProperty)); + assertThat(refSchema.getProperties()).isEqualTo(Map.of("foo", new StringSchema())); + assertThat(definitions) + .isEqualTo(Map.of( + "schema", + schema, + "refSchema", + refSchema, + "customClassRefUnusedInThisTest", + new StringSchema())); } } diff --git a/springwolf-examples/springwolf-kafka-example/src/main/avro/ExamplePayloadAvroDto.avsc b/springwolf-examples/springwolf-kafka-example/src/main/avro/ExamplePayloadAvroDto.avsc index 23f6dc3a4..d212dcac8 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/avro/ExamplePayloadAvroDto.avsc +++ b/springwolf-examples/springwolf-kafka-example/src/main/avro/ExamplePayloadAvroDto.avsc @@ -1,26 +1,38 @@ { - "type": "record", - "name": "ExamplePayloadAvroDto", "namespace": "io.github.stavshamir.springwolf.example.kafka.dto.avro", - "fields": [{ - "name": "someString", - "type": ["null", "string"], - "default": null - }, { - "name": "someLong", - "type": ["null", "int"], - "default": null - }, { - "name": "someEnum", - "type": { - "type": "enum", - "name": "ExampleEnum", - "symbols": [ - "FOO1", - "FOO2", - "FOO3" - ] + "type": "record", + "name": "AnotherPayloadAvroDto", + "fields": [ + { + "name": "someEnum", + "type": { + "type": "enum", + "name": "ExampleEnum", + "symbols": [ + "FOO1", + "FOO2", + "FOO3" + ] + }, + "default": "FOO1" }, - "default": "FOO1" - }] + { + "name": "ExamplePayloadAvroDto", + "type": { + "type": "record", + "name": "ExamplePayloadAvroDto", + "fields": [ + { + "name": "someString", + "type": ["null", "string"], + "default": null + }, + { + "name": "someLong", + "type": "long" + } + ] + } + } + ] } diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/consumers/AvroConsumer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/consumers/AvroConsumer.java index 8b22d2b53..b036bbff3 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/consumers/AvroConsumer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/consumers/AvroConsumer.java @@ -4,7 +4,7 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.KafkaAsyncOperationBinding; -import io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto; +import io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; @@ -31,7 +31,7 @@ public class AvroConsumer { description = "Requires a running kafka-schema-registry. See docker-compose.yml to start it")) @KafkaAsyncOperationBinding - public void receiveExampleAvroPayload(ExamplePayloadAvroDto payloads) { + public void receiveExampleAvroPayload(AnotherPayloadAvroDto payloads) { log.info("Received new message in avro-topic: {}", payloads.toString()); } } diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java index e733fcb09..4c10198bf 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java @@ -5,6 +5,7 @@ import io.github.stavshamir.springwolf.example.kafka.consumers.AvroConsumer; import io.github.stavshamir.springwolf.example.kafka.consumers.ExampleConsumer; import io.github.stavshamir.springwolf.example.kafka.consumers.ProtobufConsumer; +import io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto; import io.github.stavshamir.springwolf.example.kafka.dto.avro.ExampleEnum; import io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto; import io.github.stavshamir.springwolf.example.kafka.dto.proto.ExamplePayloadProtobufDto; @@ -103,13 +104,14 @@ void producerCanUseSpringwolfConfigurationToSendMessage() { disabledReason = "because it requires a running kafka-schema-registry instance (docker image= ~1GB).") void producerCanUseSpringwolfConfigurationToSendAvroMessage() { // given - ExamplePayloadAvroDto payload = new ExamplePayloadAvroDto("foo", 5, ExampleEnum.FOO1); + ExamplePayloadAvroDto payload = new ExamplePayloadAvroDto("foo", 5L); + AnotherPayloadAvroDto anotherPayload = new AnotherPayloadAvroDto(ExampleEnum.FOO1, payload); // when - springwolfKafkaProducer.send("avro-topic", "key", Map.of(), payload); + springwolfKafkaProducer.send("avro-topic", "key", Map.of(), anotherPayload); // then - verify(avroConsumer, timeout(10000)).receiveExampleAvroPayload(payload); + verify(avroConsumer, timeout(10000)).receiveExampleAvroPayload(anotherPayload); } @Test 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 b5c25abae..3b5f93ba7 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -36,8 +36,8 @@ }, "avro-topic": { "messages": { - "io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto": { - "$ref": "#/components/messages/io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto" + "io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto": { + "$ref": "#/components/messages/io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto" } } }, @@ -428,9 +428,12 @@ "type": "string" } }, - "io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto": { + "io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto": { "type": "object", "properties": { + "examplePayloadAvroDto": { + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto" + }, "someEnum": { "type": "string", "enum": [ @@ -438,10 +441,50 @@ "FOO2", "FOO3" ] + } + }, + "examples": [ + { + "examplePayloadAvroDto": { + "someLong": 0, + "someString": "string" + }, + "someEnum": "FOO1" + } + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "properties": { + "examplePayloadAvroDto": { + "properties": { + "someLong": { + "format": "int64", + "type": "integer" + }, + "someString": { + "type": "string" + } + }, + "type": "object" + }, + "someEnum": { + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ], + "type": "string" + } }, + "type": "object" + } + }, + "io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto": { + "type": "object", + "properties": { "someLong": { "type": "integer", - "format": "int32" + "format": "int64" }, "someString": { "type": "string" @@ -449,7 +492,6 @@ }, "examples": [ { - "someEnum": "FOO1", "someLong": 0, "someString": "string" } @@ -457,16 +499,8 @@ "x-json-schema": { "$schema": "https://json-schema.org/draft-04/schema#", "properties": { - "someEnum": { - "enum": [ - "FOO1", - "FOO2", - "FOO3" - ], - "type": "string" - }, "someLong": { - "format": "int32", + "format": "int64", "type": "integer" }, "someString": { @@ -773,18 +807,18 @@ } } }, - "io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto": { + "io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto": { "headers": { "$ref": "#/components/schemas/HeadersNotDocumented" }, "payload": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", "schema": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto" + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto" } }, - "name": "io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto", - "title": "ExamplePayloadAvroDto", + "name": "io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto", + "title": "AnotherPayloadAvroDto", "bindings": { "kafka": { "bindingVersion": "0.4.0" @@ -946,7 +980,7 @@ }, "messages": [ { - "$ref": "#/channels/avro-topic/messages/io.github.stavshamir.springwolf.example.kafka.dto.avro.ExamplePayloadAvroDto" + "$ref": "#/channels/avro-topic/messages/io.github.stavshamir.springwolf.example.kafka.dto.avro.AnotherPayloadAvroDto" } ] },