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:
+ *
+ * - "int32" - 32-bit signed integer
+ * - "int64" - 64-bit signed integer
+ * - "date" - RFC 3339 date
+ * - "date-time" - RFC 3339 date-time
+ *
+ *
+ * @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..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
@@ -66,8 +66,10 @@ public static SchemaObject getAsyncHeaders(AsyncOperation op, StringValueResolve
SchemaObject property = new SchemaObject();
property.setType(SchemaType.STRING);
+
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));
@@ -84,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();
}
@@ -93,8 +95,19 @@ 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);
+ }
+
+ private static String getFormat(
+ List value, StringValueResolver stringValueResolver) {
+ return value.stream()
+ .map(AsyncOperation.Headers.Header::format)
+ .filter(StringUtils::hasText)
+ .flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream())
.sorted()
.findFirst()
.orElse(null);
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..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);
@@ -58,6 +60,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 +71,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
@@ -76,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);
@@ -93,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);
@@ -104,13 +100,14 @@ void getAsyncHeadersWithoutSchemaName() throws Exception {
assertThat(headers)
.isEqualTo(SchemaObject.builder()
.type(SchemaType.OBJECT)
- .title("Headers-501004016")
+ .title("Headers-1585401221")
.properties(Map.of(
"headerResolved",
SchemaObject.builder()
.type(SchemaType.STRING)
.title("headerResolved")
.description("descriptionResolved")
+ .format(null)
.enumValues(List.of("valueResolved"))
.examples(List.of("valueResolved"))
.build()))
@@ -123,9 +120,32 @@ 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);
+
+ // then
+ assertThat(headers)
+ .isEqualTo(SchemaObject.builder()
+ .type(SchemaType.OBJECT)
+ .title("Headers-1612438838")
+ .properties(Map.of(
+ "headerResolved",
+ SchemaObject.builder()
+ .type(SchemaType.STRING)
+ .title("headerResolved")
+ .description("descriptionResolved")
+ .format(null)
+ .enumValues(null)
+ .examples(null)
+ .build()))
+ .build());
+ }
+
+ @Test
+ void getAsyncHeadersWithFormat() throws Exception {
+ // given
+ Method m = ClassWithHeaders.class.getDeclaredMethod("withFormat", String.class);
+ AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
// when
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
@@ -134,11 +154,12 @@ void getAsyncHeadersWithoutValue() throws Exception {
assertThat(headers)
.isEqualTo(SchemaObject.builder()
.type(SchemaType.OBJECT)
- .title("Headers-472917891")
+ .title("Headers-1701213112")
.properties(Map.of(
"headerResolved",
SchemaObject.builder()
.type(SchemaType.STRING)
+ .format("int32Resolved")
.title("headerResolved")
.description("descriptionResolved")
.enumValues(null)
@@ -147,6 +168,20 @@ void getAsyncHeadersWithoutValue() throws Exception {
.build());
}
+ @Test
+ void getAsyncHeadersWithEmptyFormat() throws Exception {
+ // given
+ Method m = ClassWithHeaders.class.getDeclaredMethod("withoutFormat", String.class);
+ AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
+
+ // when
+ SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
+
+ // then
+ SchemaObject headerProperty = (SchemaObject) headers.getProperties().get("headerResolved");
+ assertThat(headerProperty.getFormat()).isNull();
+ }
+
@Test
void generatedHeaderSchemaNameShouldBeUnique() throws Exception {
// given
@@ -156,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);
@@ -286,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");
@@ -351,7 +380,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 +428,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 +496,35 @@ 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")
+ })))
+ @TestOperationBindingProcessor.TestOperationBinding()
+ private void withoutFormat(String payload) {}
+
@AsyncListener(
operation =
@AsyncOperation(
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..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,18 +1,40 @@
// 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;
import io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private KafkaTemplate kafkaTemplate;
+ @AsyncPublisher(
+ operation =
+ @AsyncOperation(
+ channelName = "another-topic",
+ headers =
+ @AsyncOperation.Headers(
+ schemaName = "SpringKafkaDefaultHeaders-AnotherTopic",
+ values = {
+ @AsyncOperation.Headers.Header(
+ name = DEFAULT_CLASSID_FIELD_NAME,
+ description = "Type ID"),
+ @AsyncOperation.Headers.Header(
+ name = "my_uuid_field",
+ 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 f1eda6a7b..60518b6bb 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",
@@ -2132,6 +2171,24 @@
}
]
},
+ "another-topic_send_sendMessage": {
+ "action": "send",
+ "channel": {
+ "$ref": "#/channels/another-topic"
+ },
+ "title": "another-topic_send",
+ "description": "Auto-generated description",
+ "bindings": {
+ "kafka": {
+ "bindingVersion": "0.5.0"
+ }
+ },
+ "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..7af9075df 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,7 +1395,7 @@ 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:
@@ -1518,6 +1546,17 @@ 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:
+ kafka:
+ bindingVersion: 0.5.0
+ messages:
+ - $ref: "#/channels/another-topic/messages/io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto"
avro-topic_receive_receiveExampleAvroPayload:
action: receive
channel: