Skip to content

Commit 49f5a76

Browse files
Haisiatimonback
andauthored
feat: add format attribute to AsyncOperation.Headers.Header (#1506) (#1510)
* 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. * 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" * chore(core): minor simplification * chore(amqp): demonstrate header format in kafka example * chore(amqp): add kafka operation binding --------- Co-authored-by: Timon Back <[email protected]>
1 parent fb63e03 commit 49f5a76

File tree

7 files changed

+236
-29
lines changed

7 files changed

+236
-29
lines changed

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/annotations/AsyncOperation.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@
5555
String description() default "";
5656

5757
String value() default "";
58+
59+
/**
60+
* The format of the header value according to AsyncAPI specification.
61+
* <p>
62+
* Common formats include:
63+
* <ul>
64+
* <li>"int32" - 32-bit signed integer</li>
65+
* <li>"int64" - 64-bit signed integer</li>
66+
* <li>"date" - RFC 3339 date</li>
67+
* <li>"date-time" - RFC 3339 date-time</li>
68+
* </ul>
69+
*
70+
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#dataTypeFormat">AsyncAPI Data Type Format</a>
71+
* @return the format string, empty by default
72+
*/
73+
String format() default "";
5874
}
5975
}
6076
}

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtil.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ public static SchemaObject getAsyncHeaders(AsyncOperation op, StringValueResolve
6666

6767
SchemaObject property = new SchemaObject();
6868
property.setType(SchemaType.STRING);
69+
6970
property.setTitle(propertyName);
7071
property.setDescription(getDescription(headersValues, stringValueResolver));
72+
property.setFormat(getFormat(headersValues, stringValueResolver));
7173
List<String> values = getHeaderValues(headersValues, stringValueResolver);
7274
if (!values.isEmpty()) {
7375
property.setExamples(new ArrayList<>(values));
@@ -84,7 +86,7 @@ private static List<String> getHeaderValues(
8486
return value.stream()
8587
.map(AsyncOperation.Headers.Header::value)
8688
.filter(StringUtils::hasText)
87-
.map(stringValueResolver::resolveStringValue)
89+
.flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream())
8890
.sorted()
8991
.toList();
9092
}
@@ -93,8 +95,19 @@ private static String getDescription(
9395
List<AsyncOperation.Headers.Header> value, StringValueResolver stringValueResolver) {
9496
return value.stream()
9597
.map(AsyncOperation.Headers.Header::description)
96-
.map(stringValueResolver::resolveStringValue)
9798
.filter(StringUtils::hasText)
99+
.flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream())
100+
.sorted()
101+
.findFirst()
102+
.orElse(null);
103+
}
104+
105+
private static String getFormat(
106+
List<AsyncOperation.Headers.Header> value, StringValueResolver stringValueResolver) {
107+
return value.stream()
108+
.map(AsyncOperation.Headers.Header::format)
109+
.filter(StringUtils::hasText)
110+
.flatMap(text -> Optional.ofNullable(stringValueResolver.resolveStringValue(text)).stream())
98111
.sorted()
99112
.findFirst()
100113
.orElse(null);

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ private Schema mapSchemaObjectToSwagger(SchemaObject asyncApiSchema) {
274274
.orElse(null));
275275
swaggerSchema.setTypes(asyncApiSchema.getType());
276276
}
277-
// swaggerSchema.setFormat(asyncApiSchema.getFormat());
277+
swaggerSchema.setFormat(asyncApiSchema.getFormat());
278278
swaggerSchema.setDescription(asyncApiSchema.getDescription());
279279
swaggerSchema.setExamples(asyncApiSchema.getExamples());
280280
swaggerSchema.setEnum(asyncApiSchema.getEnumValues());

springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/annotation/AsyncAnnotationUtilTest.java

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,18 @@
3535
class AsyncAnnotationUtilTest {
3636
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
3737

38+
{
39+
when(stringValueResolver.resolveStringValue(any()))
40+
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
41+
}
42+
3843
@ParameterizedTest
3944
@ValueSource(classes = {ClassWithOperationBindingProcessor.class, ClassWithAbstractOperationBindingProcessor.class})
4045
void getAsyncHeaders(Class<?> classWithOperationBindingProcessor) throws Exception {
4146
// given
4247
Method m = classWithOperationBindingProcessor.getDeclaredMethod("methodWithAnnotation", String.class);
4348
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
4449

45-
when(stringValueResolver.resolveStringValue(any()))
46-
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
47-
4850
// when
4951
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
5052

@@ -58,6 +60,7 @@ void getAsyncHeaders(Class<?> classWithOperationBindingProcessor) throws Excepti
5860
assertThat(headerResolved.getType()).containsExactly("string");
5961
assertThat(headerResolved.getExamples().get(0)).isEqualTo("valueResolved");
6062
assertThat(headerResolved.getDescription()).isEqualTo("descriptionResolved");
63+
assertThat(headerResolved.getFormat()).isEqualTo("int32Resolved");
6164

6265
assertThat(headers.getProperties().containsKey("headerWithoutValueResolved"))
6366
.as(headers.getProperties() + " does not contain key 'headerWithoutValueResolved'")
@@ -68,6 +71,7 @@ void getAsyncHeaders(Class<?> classWithOperationBindingProcessor) throws Excepti
6871
assertThat(headerWithoutValueResolved.getExamples()).isNull();
6972
assertThat(headerWithoutValueResolved.getEnumValues()).isNull();
7073
assertThat(headerWithoutValueResolved.getDescription()).isEqualTo("descriptionResolved");
74+
assertThat(headerWithoutValueResolved.getFormat()).isNull();
7175
}
7276

7377
@Test
@@ -76,10 +80,6 @@ void getAsyncHeadersWithEmptyHeaders() throws Exception {
7680
Method m = ClassWithHeaders.class.getDeclaredMethod("emptyHeaders", String.class);
7781
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
7882

79-
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
80-
when(stringValueResolver.resolveStringValue(any()))
81-
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
82-
8383
// when
8484
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
8585

@@ -93,24 +93,21 @@ void getAsyncHeadersWithoutSchemaName() throws Exception {
9393
Method m = ClassWithHeaders.class.getDeclaredMethod("withoutSchemaName", String.class);
9494
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
9595

96-
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
97-
when(stringValueResolver.resolveStringValue(any()))
98-
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
99-
10096
// when
10197
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
10298

10399
// then
104100
assertThat(headers)
105101
.isEqualTo(SchemaObject.builder()
106102
.type(SchemaType.OBJECT)
107-
.title("Headers-501004016")
103+
.title("Headers-1585401221")
108104
.properties(Map.of(
109105
"headerResolved",
110106
SchemaObject.builder()
111107
.type(SchemaType.STRING)
112108
.title("headerResolved")
113109
.description("descriptionResolved")
110+
.format(null)
114111
.enumValues(List.of("valueResolved"))
115112
.examples(List.of("valueResolved"))
116113
.build()))
@@ -123,9 +120,32 @@ void getAsyncHeadersWithoutValue() throws Exception {
123120
Method m = ClassWithHeaders.class.getDeclaredMethod("withoutValue", String.class);
124121
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
125122

126-
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
127-
when(stringValueResolver.resolveStringValue(any()))
128-
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
123+
// when
124+
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
125+
126+
// then
127+
assertThat(headers)
128+
.isEqualTo(SchemaObject.builder()
129+
.type(SchemaType.OBJECT)
130+
.title("Headers-1612438838")
131+
.properties(Map.of(
132+
"headerResolved",
133+
SchemaObject.builder()
134+
.type(SchemaType.STRING)
135+
.title("headerResolved")
136+
.description("descriptionResolved")
137+
.format(null)
138+
.enumValues(null)
139+
.examples(null)
140+
.build()))
141+
.build());
142+
}
143+
144+
@Test
145+
void getAsyncHeadersWithFormat() throws Exception {
146+
// given
147+
Method m = ClassWithHeaders.class.getDeclaredMethod("withFormat", String.class);
148+
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
129149

130150
// when
131151
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
@@ -134,11 +154,12 @@ void getAsyncHeadersWithoutValue() throws Exception {
134154
assertThat(headers)
135155
.isEqualTo(SchemaObject.builder()
136156
.type(SchemaType.OBJECT)
137-
.title("Headers-472917891")
157+
.title("Headers-1701213112")
138158
.properties(Map.of(
139159
"headerResolved",
140160
SchemaObject.builder()
141161
.type(SchemaType.STRING)
162+
.format("int32Resolved")
142163
.title("headerResolved")
143164
.description("descriptionResolved")
144165
.enumValues(null)
@@ -147,6 +168,20 @@ void getAsyncHeadersWithoutValue() throws Exception {
147168
.build());
148169
}
149170

171+
@Test
172+
void getAsyncHeadersWithEmptyFormat() throws Exception {
173+
// given
174+
Method m = ClassWithHeaders.class.getDeclaredMethod("withoutFormat", String.class);
175+
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
176+
177+
// when
178+
SchemaObject headers = AsyncAnnotationUtil.getAsyncHeaders(operation, stringValueResolver);
179+
180+
// then
181+
SchemaObject headerProperty = (SchemaObject) headers.getProperties().get("headerResolved");
182+
assertThat(headerProperty.getFormat()).isNull();
183+
}
184+
150185
@Test
151186
void generatedHeaderSchemaNameShouldBeUnique() throws Exception {
152187
// given
@@ -156,10 +191,6 @@ void generatedHeaderSchemaNameShouldBeUnique() throws Exception {
156191
Method m2 = ClassWithHeaders.class.getDeclaredMethod("differentHeadersWithoutSchemaName", String.class);
157192
AsyncOperation operation2 = m2.getAnnotation(AsyncListener.class).operation();
158193

159-
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
160-
when(stringValueResolver.resolveStringValue(any()))
161-
.thenAnswer(invocation -> invocation.getArgument(0).toString() + "Resolved");
162-
163194
// when
164195
SchemaObject headers1 = AsyncAnnotationUtil.getAsyncHeaders(operation1, stringValueResolver);
165196
SchemaObject headers2 = AsyncAnnotationUtil.getAsyncHeaders(operation2, stringValueResolver);
@@ -286,8 +317,6 @@ void getServers() throws Exception {
286317
Method m = ClassWithOperationBindingProcessor.class.getDeclaredMethod("methodWithAnnotation", String.class);
287318
AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation();
288319

289-
StringValueResolver stringValueResolver = mock(StringValueResolver.class);
290-
291320
// when
292321
when(stringValueResolver.resolveStringValue("${test.property.server1}")).thenReturn("server1");
293322

@@ -351,7 +380,8 @@ private static class ClassWithOperationBindingProcessor {
351380
@AsyncOperation.Headers.Header(
352381
name = "header",
353382
value = "value",
354-
description = "description"),
383+
description = "description",
384+
format = "int32"),
355385
@AsyncOperation.Headers.Header(
356386
name = "headerWithoutValue",
357387
description = "description")
@@ -398,7 +428,8 @@ private static class ClassWithAbstractOperationBindingProcessor {
398428
@AsyncOperation.Headers.Header(
399429
name = "header",
400430
value = "value",
401-
description = "description"),
431+
description = "description",
432+
format = "int32"),
402433
@AsyncOperation.Headers.Header(
403434
name = "headerWithoutValue",
404435
description = "description")
@@ -465,6 +496,35 @@ private void withoutSchemaName(String payload) {}
465496
@TestOperationBindingProcessor.TestOperationBinding()
466497
private void withoutValue(String payload) {}
467498

499+
@AsyncListener(
500+
operation =
501+
@AsyncOperation(
502+
channelName = "${test.property.test-channel}",
503+
headers =
504+
@AsyncOperation.Headers(
505+
values = {
506+
@AsyncOperation.Headers.Header(
507+
name = "header",
508+
description = "description",
509+
format = "int32")
510+
})))
511+
@TestOperationBindingProcessor.TestOperationBinding()
512+
private void withFormat(String payload) {}
513+
514+
@AsyncListener(
515+
operation =
516+
@AsyncOperation(
517+
channelName = "${test.property.test-channel}",
518+
headers =
519+
@AsyncOperation.Headers(
520+
values = {
521+
@AsyncOperation.Headers.Header(
522+
name = "header",
523+
description = "description")
524+
})))
525+
@TestOperationBindingProcessor.TestOperationBinding()
526+
private void withoutFormat(String payload) {}
527+
468528
@AsyncListener(
469529
operation =
470530
@AsyncOperation(

springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/producers/AnotherProducer.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
// SPDX-License-Identifier: Apache-2.0
22
package io.github.springwolf.examples.kafka.producers;
33

4+
import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding;
5+
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;
6+
import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher;
47
import io.github.springwolf.examples.kafka.configuration.KafkaConfiguration;
58
import io.github.springwolf.examples.kafka.dtos.AnotherPayloadDto;
69
import org.springframework.beans.factory.annotation.Autowired;
710
import org.springframework.kafka.core.KafkaTemplate;
811
import org.springframework.stereotype.Component;
912

13+
import static org.springframework.kafka.support.mapping.AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME;
14+
1015
@Component
1116
public class AnotherProducer {
1217

1318
@Autowired
1419
private KafkaTemplate<String, AnotherPayloadDto> kafkaTemplate;
1520

21+
@AsyncPublisher(
22+
operation =
23+
@AsyncOperation(
24+
channelName = "another-topic",
25+
headers =
26+
@AsyncOperation.Headers(
27+
schemaName = "SpringKafkaDefaultHeaders-AnotherTopic",
28+
values = {
29+
@AsyncOperation.Headers.Header(
30+
name = DEFAULT_CLASSID_FIELD_NAME,
31+
description = "Type ID"),
32+
@AsyncOperation.Headers.Header(
33+
name = "my_uuid_field",
34+
description = "Event identifier",
35+
format = "uuid")
36+
})))
37+
@KafkaAsyncOperationBinding
1638
public void sendMessage(AnotherPayloadDto msg) {
1739
kafkaTemplate.send(KafkaConfiguration.PRODUCER_TOPIC, msg);
1840
}

0 commit comments

Comments
 (0)