From b7c7d24d95a3c5e47e2a14d23f9ae78c152c9811 Mon Sep 17 00:00:00 2001 From: Haisia Date: Sun, 9 Nov 2025 10:39:20 +0900 Subject: [PATCH 1/5] feat: add format attribute to AsyncOperation.Headers.Header (#1506) - Add format field to @Header annotation with default empty string - Implement format processing in AsyncAnnotationUtil.getAsyncHeaders() - Add getFormat() helper method following existing pattern - Update tests and hash values due to header structure change The format field allows specifying AsyncAPI data type formats (e.g., int32, int64, date-time) for header values while maintaining type as 'string' per AsyncAPI specification. --- .../asyncapi/annotations/AsyncOperation.java | 16 ++++ .../annotation/AsyncAnnotationUtil.java | 17 ++++ .../asyncapi/schemas/SwaggerSchemaUtil.java | 2 +- .../annotation/AsyncAnnotationUtilTest.java | 91 ++++++++++++++++++- 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/annotations/AsyncOperation.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/annotations/AsyncOperation.java index 71a606f42..d7e99c8dc 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/annotations/AsyncOperation.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/annotations/AsyncOperation.java @@ -55,6 +55,22 @@ String description() default ""; String value() default ""; + + /** + * The format of the header value according to AsyncAPI specification. + *

+ * Common formats include: + *

+ * + * @see AsyncAPI Data Type Format + * @return the format string, empty by default + */ + String format() default ""; } } } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java index e8763f5e7..e198763e5 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java @@ -66,6 +66,12 @@ public static SchemaObject getAsyncHeaders(AsyncOperation op, StringValueResolve SchemaObject property = new SchemaObject(); property.setType(SchemaType.STRING); + + String format = getFormat(headersValues, stringValueResolver); + if (StringUtils.hasText(format)) { + property.setFormat(format); + } + property.setTitle(propertyName); property.setDescription(getDescription(headersValues, stringValueResolver)); List values = getHeaderValues(headersValues, stringValueResolver); @@ -100,6 +106,17 @@ private static String getDescription( .orElse(null); } + private static String getFormat( + List value, StringValueResolver stringValueResolver) { + return value.stream() + .map(AsyncOperation.Headers.Header::format) + .filter(StringUtils::hasText) + .map(stringValueResolver::resolveStringValue) + .sorted() + .findFirst() + .orElse(null); + } + public static Map processOperationBindingFromAnnotation( Method method, List operationBindingProcessors) { return operationBindingProcessors.stream() diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaUtil.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaUtil.java index fb17b35ea..b8311c328 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaUtil.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaUtil.java @@ -274,7 +274,7 @@ private Schema mapSchemaObjectToSwagger(SchemaObject asyncApiSchema) { .orElse(null)); swaggerSchema.setTypes(asyncApiSchema.getType()); } - // swaggerSchema.setFormat(asyncApiSchema.getFormat()); + swaggerSchema.setFormat(asyncApiSchema.getFormat()); swaggerSchema.setDescription(asyncApiSchema.getDescription()); swaggerSchema.setExamples(asyncApiSchema.getExamples()); swaggerSchema.setEnum(asyncApiSchema.getEnumValues()); diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java index d15f8d9a0..947fa91dd 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java @@ -58,6 +58,7 @@ void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Excepti assertThat(headerResolved.getType()).containsExactly("string"); assertThat(headerResolved.getExamples().get(0)).isEqualTo("valueResolved"); assertThat(headerResolved.getDescription()).isEqualTo("descriptionResolved"); + assertThat(headerResolved.getFormat()).isEqualTo("int32Resolved"); assertThat(headers.getProperties().containsKey("headerWithoutValueResolved")) .as(headers.getProperties() + " does not contain key 'headerWithoutValueResolved'") @@ -68,6 +69,7 @@ void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Excepti assertThat(headerWithoutValueResolved.getExamples()).isNull(); assertThat(headerWithoutValueResolved.getEnumValues()).isNull(); assertThat(headerWithoutValueResolved.getDescription()).isEqualTo("descriptionResolved"); + assertThat(headerWithoutValueResolved.getFormat()).isNull(); } @Test @@ -104,7 +106,7 @@ void getAsyncHeadersWithoutSchemaName() throws Exception { assertThat(headers) .isEqualTo(SchemaObject.builder() .type(SchemaType.OBJECT) - .title("Headers-501004016") + .title("Headers-1585401221") .properties(Map.of( "headerResolved", SchemaObject.builder() @@ -134,7 +136,7 @@ void getAsyncHeadersWithoutValue() throws Exception { assertThat(headers) .isEqualTo(SchemaObject.builder() .type(SchemaType.OBJECT) - .title("Headers-472917891") + .title("Headers-1612438838") .properties(Map.of( "headerResolved", SchemaObject.builder() @@ -147,6 +149,55 @@ void getAsyncHeadersWithoutValue() throws Exception { .build()); } + @Test + void getAsyncHeadersWithFormat() throws Exception { + // given + Method m = ClassWithHeaders.class.getDeclaredMethod("withFormat", String.class); + AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); + + StringValueResolver stringValueResolver = mock(StringValueResolver.class); + when(stringValueResolver.resolveStringValue(any())) + .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); + + // when + SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); + + // then + assertThat(headers) + .isEqualTo(SchemaObject.builder() + .type(SchemaType.OBJECT) + .title("Headers-1701213112") + .properties(Map.of( + "headerResolved", + SchemaObject.builder() + .type(SchemaType.STRING) + .format("int32Resolved") + .title("headerResolved") + .description("descriptionResolved") + .enumValues(null) + .examples(null) + .build())) + .build()); + } + + @Test + void getAsyncHeadersWithEmptyFormat() throws Exception { + // given + Method m = ClassWithHeaders.class.getDeclaredMethod("withEmptyFormat", String.class); + AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); + + StringValueResolver stringValueResolver = mock(StringValueResolver.class); + when(stringValueResolver.resolveStringValue(any())) + .thenAnswer(invocation -> invocation.getArgument(0).toString()); + + // when + SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); + + // then + SchemaObject headerProperty = (SchemaObject) headers.getProperties().get("header"); + assertThat(headerProperty.getFormat()).isNull(); + } + @Test void generatedHeaderSchemaNameShouldBeUnique() throws Exception { // given @@ -351,7 +402,8 @@ private static class ClassWithOperationBindingProcessor { @AsyncOperation.Headers.Header( name = "header", value = "value", - description = "description"), + description = "description", + format = "int32"), @AsyncOperation.Headers.Header( name = "headerWithoutValue", description = "description") @@ -398,7 +450,8 @@ private static class ClassWithAbstractOperationBindingProcessor { @AsyncOperation.Headers.Header( name = "header", value = "value", - description = "description"), + description = "description", + format = "int32"), @AsyncOperation.Headers.Header( name = "headerWithoutValue", description = "description") @@ -465,6 +518,36 @@ private void withoutSchemaName(String payload) {} @TestOperationBindingProcessor.TestOperationBinding() private void withoutValue(String payload) {} + @AsyncListener( + operation = + @AsyncOperation( + channelName = "${test.property.test-channel}", + headers = + @AsyncOperation.Headers( + values = { + @AsyncOperation.Headers.Header( + name = "header", + description = "description", + format = "int32") + }))) + @TestOperationBindingProcessor.TestOperationBinding() + private void withFormat(String payload) {} + + @AsyncListener( + operation = + @AsyncOperation( + channelName = "${test.property.test-channel}", + headers = + @AsyncOperation.Headers( + values = { + @AsyncOperation.Headers.Header( + name = "header", + description = "description", + format = "") + }))) + @TestOperationBindingProcessor.TestOperationBinding() + private void withEmptyFormat(String payload) {} + @AsyncListener( operation = @AsyncOperation( From f34bf976068315c09fa8acc2dc1a4831ed4bb81c Mon Sep 17 00:00:00 2001 From: Haisia Date: Sun, 9 Nov 2025 18:14:06 +0900 Subject: [PATCH 2/5] refactor: align getFormat with getDescription pattern and add example - Change getFormat to use resolve->filter order for consistency - Remove unnecessary if block in getAsyncHeaders - Add format example to AnotherProducer in kafka example" --- .../annotation/AsyncAnnotationUtil.java | 6 ++--- .../annotation/AsyncAnnotationUtilTest.java | 4 +++- .../kafka/producers/AnotherProducer.java | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java index e198763e5..1ba67a0e6 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java @@ -68,9 +68,7 @@ public static SchemaObject getAsyncHeaders(AsyncOperation op, StringValueResolve property.setType(SchemaType.STRING); String format = getFormat(headersValues, stringValueResolver); - if (StringUtils.hasText(format)) { - property.setFormat(format); - } + property.setFormat(format); property.setTitle(propertyName); property.setDescription(getDescription(headersValues, stringValueResolver)); @@ -110,8 +108,8 @@ private static String getFormat( List value, StringValueResolver stringValueResolver) { return value.stream() .map(AsyncOperation.Headers.Header::format) - .filter(StringUtils::hasText) .map(stringValueResolver::resolveStringValue) + .filter(StringUtils::hasText) .sorted() .findFirst() .orElse(null); diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java index 947fa91dd..75d3abcd5 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java @@ -69,7 +69,7 @@ void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Excepti assertThat(headerWithoutValueResolved.getExamples()).isNull(); assertThat(headerWithoutValueResolved.getEnumValues()).isNull(); assertThat(headerWithoutValueResolved.getDescription()).isEqualTo("descriptionResolved"); - assertThat(headerWithoutValueResolved.getFormat()).isNull(); + assertThat(headerWithoutValueResolved.getFormat()).isEqualTo("Resolved"); } @Test @@ -113,6 +113,7 @@ void getAsyncHeadersWithoutSchemaName() throws Exception { .type(SchemaType.STRING) .title("headerResolved") .description("descriptionResolved") + .format("Resolved") .enumValues(List.of("valueResolved")) .examples(List.of("valueResolved")) .build())) @@ -143,6 +144,7 @@ void getAsyncHeadersWithoutValue() throws Exception { .type(SchemaType.STRING) .title("headerResolved") .description("descriptionResolved") + .format("Resolved") .enumValues(null) .examples(null) .build())) diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java index a19e26cdf..1d84bb0e6 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.examples.kafka.producers; +import io.github.springwolf.core.asyncapi.annotations.AsyncOperation; +import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher; import io.github.springwolf.examples.kafka.configuration.KafkaConfiguration; import io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +15,27 @@ public class AnotherProducer { @Autowired private KafkaTemplate kafkaTemplate; + @AsyncPublisher( + operation = + @AsyncOperation( + channelName = "another-topic", + headers = + @AsyncOperation.Headers( + schemaName = "SpringKafkaDefaultHeaders-AnotherTopic", + values = { + @AsyncOperation.Headers.Header( + name = "kafka_messageKey", + description = "Message key", + format = "string"), + @AsyncOperation.Headers.Header( + name = "__TypeId__", + description = "Type ID", + format = "string"), + @AsyncOperation.Headers.Header( + name = "my_key", + description = "my_key", + format = "int32") + }))) public void sendMessage(AnotherPayloadDto msg) { kafkaTemplate.send(KafkaConfiguration.PRODUCER_TOPIC, msg); } From 4663a730b66a132d008071f55af48641c6249f9f Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 14 Nov 2025 15:35:19 +0100 Subject: [PATCH 3/5] chore(core): minor simplification --- .../annotation/AsyncAnnotationUtil.java | 10 ++-- .../annotation/AsyncAnnotationUtilTest.java | 49 +++++-------------- 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java index 1ba67a0e6..6a434b1fb 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java @@ -67,11 +67,9 @@ public static SchemaObject getAsyncHeaders(AsyncOperation op, StringValueResolve SchemaObject property = new SchemaObject(); property.setType(SchemaType.STRING); - String format = getFormat(headersValues, stringValueResolver); - property.setFormat(format); - property.setTitle(propertyName); property.setDescription(getDescription(headersValues, stringValueResolver)); + property.setFormat(getFormat(headersValues, stringValueResolver)); List values = getHeaderValues(headersValues, stringValueResolver); if (!values.isEmpty()) { property.setExamples(new ArrayList<>(values)); @@ -88,7 +86,7 @@ private static List getHeaderValues( return value.stream() .map(AsyncOperation.Headers.Header::value) .filter(StringUtils::hasText) - .map(stringValueResolver::resolveStringValue) + .flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream()) .sorted() .toList(); } @@ -97,8 +95,8 @@ private static String getDescription( List value, StringValueResolver stringValueResolver) { return value.stream() .map(AsyncOperation.Headers.Header::description) - .map(stringValueResolver::resolveStringValue) .filter(StringUtils::hasText) + .flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream()) .sorted() .findFirst() .orElse(null); @@ -108,8 +106,8 @@ private static String getFormat( List value, StringValueResolver stringValueResolver) { return value.stream() .map(AsyncOperation.Headers.Header::format) - .map(stringValueResolver::resolveStringValue) .filter(StringUtils::hasText) + .flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream()) .sorted() .findFirst() .orElse(null); diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java index 75d3abcd5..3388cdf7b 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java @@ -35,6 +35,11 @@ class AsyncAnnotationUtilTest { StringValueResolver stringValueResolver = mock(StringValueResolver.class); + { + when(stringValueResolver.resolveStringValue(any())) + .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); + } + @ParameterizedTest @ValueSource(classes = {ClassWithOperationBindingProcessor.class, ClassWithAbstractOperationBindingProcessor.class}) void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Exception { @@ -42,9 +47,6 @@ void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Excepti Method m = classWithOperationBindingProcessor.getDeclaredMethod("methodWithAnnotation", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); @@ -69,7 +71,7 @@ void getAsyncHeaders(Class classWithOperationBindingProcessor) throws Excepti assertThat(headerWithoutValueResolved.getExamples()).isNull(); assertThat(headerWithoutValueResolved.getEnumValues()).isNull(); assertThat(headerWithoutValueResolved.getDescription()).isEqualTo("descriptionResolved"); - assertThat(headerWithoutValueResolved.getFormat()).isEqualTo("Resolved"); + assertThat(headerWithoutValueResolved.getFormat()).isNull(); } @Test @@ -78,10 +80,6 @@ void getAsyncHeadersWithEmptyHeaders() throws Exception { Method m = ClassWithHeaders.class.getDeclaredMethod("emptyHeaders", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); @@ -95,10 +93,6 @@ void getAsyncHeadersWithoutSchemaName() throws Exception { Method m = ClassWithHeaders.class.getDeclaredMethod("withoutSchemaName", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); @@ -113,7 +107,7 @@ void getAsyncHeadersWithoutSchemaName() throws Exception { .type(SchemaType.STRING) .title("headerResolved") .description("descriptionResolved") - .format("Resolved") + .format(null) .enumValues(List.of("valueResolved")) .examples(List.of("valueResolved")) .build())) @@ -126,10 +120,6 @@ void getAsyncHeadersWithoutValue() throws Exception { Method m = ClassWithHeaders.class.getDeclaredMethod("withoutValue", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); @@ -144,7 +134,7 @@ void getAsyncHeadersWithoutValue() throws Exception { .type(SchemaType.STRING) .title("headerResolved") .description("descriptionResolved") - .format("Resolved") + .format(null) .enumValues(null) .examples(null) .build())) @@ -157,10 +147,6 @@ void getAsyncHeadersWithFormat() throws Exception { Method m = ClassWithHeaders.class.getDeclaredMethod("withFormat", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); @@ -185,18 +171,14 @@ void getAsyncHeadersWithFormat() throws Exception { @Test void getAsyncHeadersWithEmptyFormat() throws Exception { // given - Method m = ClassWithHeaders.class.getDeclaredMethod("withEmptyFormat", String.class); + Method m = ClassWithHeaders.class.getDeclaredMethod("withoutFormat", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString()); - // when SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver); // then - SchemaObject headerProperty = (SchemaObject) headers.getProperties().get("header"); + SchemaObject headerProperty = (SchemaObject) headers.getProperties().get("headerResolved"); assertThat(headerProperty.getFormat()).isNull(); } @@ -209,10 +191,6 @@ void generatedHeaderSchemaNameShouldBeUnique() throws Exception { Method m2 = ClassWithHeaders.class.getDeclaredMethod("differentHeadersWithoutSchemaName", String.class); AsyncOperation operation2 = m2.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue(any())) - .thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved"); - // when SchemaObject headers1 = AsyncAnnotationUtil.getAsyncHeaders(operation1, stringValueResolver); SchemaObject headers2 = AsyncAnnotationUtil.getAsyncHeaders(operation2, stringValueResolver); @@ -339,8 +317,6 @@ void getServers() throws Exception { Method m = ClassWithOperationBindingProcessor.class.getDeclaredMethod("methodWithAnnotation", String.class); AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - // when when(stringValueResolver.resolveStringValue("${test.property.server1}")).thenReturn("server1"); @@ -544,11 +520,10 @@ private void withFormat(String payload) {} values = { @AsyncOperation.Headers.Header( name = "header", - description = "description", - format = "") + description = "description") }))) @TestOperationBindingProcessor.TestOperationBinding() - private void withEmptyFormat(String payload) {} + private void withoutFormat(String payload) {} @AsyncListener( operation = From 544deb7495c1924983bc4446036e67dcfbd1b24b Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 14 Nov 2025 15:36:52 +0100 Subject: [PATCH 4/5] chore(amqp): demonstrate header format in kafka example --- .../kafka/producers/AnotherProducer.java | 17 +++--- .../src/test/resources/asyncapi.json | 61 +++++++++++++++++-- .../src/test/resources/asyncapi.yaml | 43 +++++++++++-- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java index 1d84bb0e6..1ffd48cc4 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java @@ -9,6 +9,8 @@ import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; +import static org.springframework.kafka.support.mapping.AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME; + @Component public class AnotherProducer { @@ -24,17 +26,12 @@ public class AnotherProducer { schemaName = "SpringKafkaDefaultHeaders-AnotherTopic", values = { @AsyncOperation.Headers.Header( - name = "kafka_messageKey", - description = "Message key", - format = "string"), - @AsyncOperation.Headers.Header( - name = "__TypeId__", - description = "Type ID", - format = "string"), + name = DEFAULT_CLASSID_FIELD_NAME, + description = "Type ID"), @AsyncOperation.Headers.Header( - name = "my_key", - description = "my_key", - format = "int32") + name = "my_uuid_field", + description = "Event identifier", + format = "uuid") }))) public void sendMessage(AnotherPayloadDto msg) { kafkaTemplate.send(KafkaConfiguration.PRODUCER_TOPIC, msg); 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 f1eda6a7b..1ad59669c 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -496,6 +496,43 @@ "type": "object" } }, + "SpringKafkaDefaultHeaders-AnotherTopic": { + "title": "SpringKafkaDefaultHeaders-AnotherTopic", + "type": "object", + "properties": { + "__TypeId__": { + "type": "string", + "description": "Type ID" + }, + "my_uuid_field": { + "type": "string", + "description": "Event identifier", + "format": "uuid" + } + }, + "examples": [ + { + "__TypeId__": "string", + "my_uuid_field": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "properties": { + "__TypeId__": { + "description": "Type ID", + "type": "string" + }, + "my_uuid_field": { + "description": "Event identifier", + "format": "uuid", + "type": "string" + } + }, + "title": "SpringKafkaDefaultHeaders-AnotherTopic", + "type": "object" + } + }, "SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105": { "title": "SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105", "type": "object", @@ -512,6 +549,7 @@ }, "kafka_offset": { "type": "integer", + "format": "int32", "examples": [ 0 ] @@ -548,6 +586,7 @@ "type": "string" }, "kafka_offset": { + "format": "int32", "type": "integer" }, "kafka_receivedMessageKey": { @@ -1904,7 +1943,7 @@ }, "io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto": { "headers": { - "$ref": "#/components/schemas/SpringKafkaDefaultHeaders-AnotherPayloadDto" + "$ref": "#/components/schemas/SpringKafkaDefaultHeaders-AnotherTopic" }, "payload": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", @@ -1914,11 +1953,7 @@ }, "name": "io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto", "title": "AnotherPayloadDto", - "bindings": { - "kafka": { - "bindingVersion": "0.5.0" - } - } + "bindings": { } }, "io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto": { "headers": { @@ -2132,6 +2167,20 @@ } ] }, + "another-topic_send_sendMessage": { + "action": "send", + "channel": { + "$ref": "#/channels/another-topic" + }, + "title": "another-topic_send", + "description": "Auto-generated description", + "bindings": { }, + "messages": [ + { + "$ref": "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" + } + ] + }, "avro-topic_receive_receiveExampleAvroPayload": { "action": "receive", "channel": { diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml index b0c056945..fefe1a88e 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml @@ -339,6 +339,32 @@ components: type: string title: SpringKafkaDefaultHeaders-AnotherPayloadDto type: object + SpringKafkaDefaultHeaders-AnotherTopic: + title: SpringKafkaDefaultHeaders-AnotherTopic + type: object + properties: + __TypeId__: + type: string + description: Type ID + my_uuid_field: + type: string + description: Event identifier + format: uuid + examples: + - __TypeId__: string + my_uuid_field: 3fa85f64-5717-4562-b3fc-2c963f66afa6 + x-json-schema: + $schema: https://json-schema.org/draft-04/schema# + properties: + __TypeId__: + description: Type ID + type: string + my_uuid_field: + description: Event identifier + format: uuid + type: string + title: SpringKafkaDefaultHeaders-AnotherTopic + type: object SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105: title: SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105 type: object @@ -352,6 +378,7 @@ components: - io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto kafka_offset: type: integer + format: int32 examples: - 0 kafka_receivedMessageKey: @@ -376,6 +403,7 @@ components: - io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto type: string kafka_offset: + format: int32 type: integer kafka_receivedMessageKey: type: string @@ -1367,16 +1395,14 @@ components: bindingVersion: 0.5.0 io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto: headers: - $ref: "#/components/schemas/SpringKafkaDefaultHeaders-AnotherPayloadDto" + $ref: "#/components/schemas/SpringKafkaDefaultHeaders-AnotherTopic" payload: schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 schema: $ref: "#/components/schemas/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" name: io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto title: AnotherPayloadDto - bindings: - kafka: - bindingVersion: 0.5.0 + bindings: {} io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto: headers: $ref: "#/components/schemas/SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105" @@ -1518,6 +1544,15 @@ operations: bindingVersion: 0.5.0 messages: - $ref: "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" + another-topic_send_sendMessage: + action: send + channel: + $ref: "#/channels/another-topic" + title: another-topic_send + description: Auto-generated description + bindings: {} + messages: + - $ref: "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" avro-topic_receive_receiveExampleAvroPayload: action: receive channel: From 2327f9b54893cb9573389d2f1215610d984b9a4e Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 14 Nov 2025 15:48:21 +0100 Subject: [PATCH 5/5] chore(amqp): add kafka operation binding --- .../examples/kafka/producers/AnotherProducer.java | 2 ++ .../src/test/resources/asyncapi.json | 12 ++++++++++-- .../src/test/resources/asyncapi.yaml | 8 ++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java index 1ffd48cc4..5be7fb4e9 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.examples.kafka.producers; +import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding; import io.github.springwolf.core.asyncapi.annotations.AsyncOperation; import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher; import io.github.springwolf.examples.kafka.configuration.KafkaConfiguration; @@ -33,6 +34,7 @@ public class AnotherProducer { description = "Event identifier", format = "uuid") }))) + @KafkaAsyncOperationBinding public void sendMessage(AnotherPayloadDto msg) { kafkaTemplate.send(KafkaConfiguration.PRODUCER_TOPIC, msg); } 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 1ad59669c..60518b6bb 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -1953,7 +1953,11 @@ }, "name": "io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto", "title": "AnotherPayloadDto", - "bindings": { } + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + } }, "io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto": { "headers": { @@ -2174,7 +2178,11 @@ }, "title": "another-topic_send", "description": "Auto-generated description", - "bindings": { }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + }, "messages": [ { "$ref": "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml index fefe1a88e..7af9075df 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.yaml @@ -1402,7 +1402,9 @@ components: $ref: "#/components/schemas/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" name: io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto title: AnotherPayloadDto - bindings: {} + bindings: + kafka: + bindingVersion: 0.5.0 io.github.springwolf.examples.kafka.dtos.ExamplePayloadDto: headers: $ref: "#/components/schemas/SpringKafkaDefaultHeaders-ExamplePayloadDto-546532105" @@ -1550,7 +1552,9 @@ operations: $ref: "#/channels/another-topic" title: another-topic_send description: Auto-generated description - bindings: {} + bindings: + kafka: + bindingVersion: 0.5.0 messages: - $ref: "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto" avro-topic_receive_receiveExampleAvroPayload: