Skip to content

Commit

Permalink
fix: Migration to AsyncAPI v3
Browse files Browse the repository at this point in the history
As per https://www.asyncapi.com/docs/migration/migrating-to-v3

For a channel with multiple messages, you specify multiple key-value pairs. For a channel with just one message, you use a single key-value pair.

That means that the use of 'oneOf' is removed.
  • Loading branch information
ctasada committed Jan 10, 2024
1 parent 433f496 commit 59c2231
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,14 @@ public class MessageObject extends ExtendableObject implements Message {
*/
@JsonProperty(value = "traits")
private List<MessageTrait> traits;

/*
* Override the getMessageId to guarantee that there's always a value. Defaults to 'name'
*/
public String getMessageId() {
if (messageId == null) {
return this.name;
}
return messageId;
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi;

import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.Message;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Slf4j
public class MessageHelper {
private static final String ONE_OF = "oneOf";

private static final Comparator<MessageObject> byMessageName = Comparator.comparing(MessageObject::getName);

private static final Supplier<Set<MessageObject>> messageSupplier = () -> new TreeSet<>(byMessageName);

public static Object toMessageObjectOrComposition(Set<MessageObject> messages) {
return switch (messages.size()) {
case 0 -> throw new IllegalArgumentException("messages must not be empty");
case 1 -> messages.toArray()[0];
default -> Map.of(
ONE_OF, new ArrayList<>(messages.stream().collect(Collectors.toCollection(messageSupplier))));
};
private MessageHelper() {}

public static Map<String, Message> toMessagesMap(Set<MessageObject> messages) {
if (messages.isEmpty()) {
throw new IllegalArgumentException("messages must not be empty");
}

return new ArrayList<>(messages.stream().collect(Collectors.toCollection(messageSupplier)))
.stream().collect(Collectors.toMap(MessageObject::getMessageId, Function.identity()));
}

@SuppressWarnings("unchecked")
Expand All @@ -38,10 +39,11 @@ public static Set<MessageObject> messageObjectToSet(Object messageObject) {
return new HashSet<>(Collections.singletonList(message));
}

if (messageObject instanceof Map) {
List<MessageObject> messages = ((Map<String, List<MessageObject>>) messageObject).get(ONE_OF);
return new HashSet<>(messages);
}
// FIXME
// if (messageObject instanceof Map) {
// List<MessageObject> messages = ((Map<String, List<MessageObject>>) messageObject).get(ONE_OF);
// return new HashSet<>(messages);
// }

log.warn(
"Message object must contain either a Message or a Map<String, Set<Message>, but contained: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersBuilder;
import io.github.stavshamir.springwolf.asyncapi.v3.bindings.ChannelBinding;
import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding;
import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.Message;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject;
import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema;
import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -24,7 +28,7 @@
import java.util.Set;
import java.util.stream.Stream;

import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition;
import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessagesMap;
import static java.util.stream.Collectors.toSet;

@RequiredArgsConstructor
Expand Down Expand Up @@ -75,62 +79,51 @@ private Stream<Map.Entry<String, ChannelObject>> mapClassToChannel(Class<?> comp
}

String channelName = bindingFactory.getChannelName(classAnnotation);
String operationId = channelName + "_publish_" + component.getSimpleName();

ChannelObject channelItem = buildChannelItem(classAnnotation, operationId, annotatedMethods);
ChannelObject channelItem = buildChannelItem(classAnnotation, annotatedMethods);

return Stream.of(Map.entry(channelName, channelItem));
}

private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, String operationId, Set<Method> methods) {
Object message = buildMessageObject(classAnnotation, methods);
Operation operation = buildOperation(classAnnotation, operationId, message);
return buildChannelItem(classAnnotation, operation);
private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Set<Method> methods) {
var messages = buildMessages(classAnnotation, methods);
return buildChannelItem(classAnnotation, messages);
}

private Object buildMessageObject(ClassAnnotation classAnnotation, Set<Method> methods) {
private Map<String, Message> buildMessages(ClassAnnotation classAnnotation, Set<Method> methods) {
Set<MessageObject> messages = methods.stream()
.map((Method method) -> {
Class<?> payloadType = payloadClassExtractor.extractFrom(method);
return buildMessage(classAnnotation, payloadType);
})
.collect(toSet());

return toMessageObjectOrComposition(messages);
return toMessagesMap(messages);
}

private MessageObject buildMessage(ClassAnnotation classAnnotation, Class<?> payloadType) {
Map<String, MessageBinding> messageBinding = bindingFactory.buildMessageBinding(classAnnotation);
String modelName = schemasService.register(payloadType);
String headerModelName = schemasService.register(asyncHeadersBuilder.buildHeaders(payloadType));

MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder()
.schema(SchemaReference.fromSchema(modelName))
.build());

return MessageObject.builder()
.messageId(payloadType.getName())
.name(payloadType.getName())
.title(payloadType.getSimpleName())
.description(null)
// .payload(PayloadReference.fromModelName(modelName)) FIXME
// .headers(HeaderReference.fromModelName(headerModelName)) FIXME
.payload(payload)
.headers(MessageHeaders.of(MessageReference.fromSchema(headerModelName)))
.bindings(messageBinding)
.build();
}

private Operation buildOperation(ClassAnnotation classAnnotation, String operationTitle, Object message) {
Map<String, OperationBinding> operationBinding = bindingFactory.buildOperationBinding(classAnnotation);
Map<String, OperationBinding> opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null;

return Operation.builder()
.description("Auto-generated description")
.title(operationTitle)
// .message(message)
.bindings(opBinding)
.build();
}

private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Operation operation) {
private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Map<String, Message> messages) {
Map<String, ChannelBinding> channelBinding = bindingFactory.buildChannelBinding(classAnnotation);
Map<String, ChannelBinding> chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null;
return ChannelObject.builder()
.bindings(chBinding) /*.publish(operation) FIXME*/
.build();
return ChannelObject.builder().bindings(chBinding).messages(messages).build();
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi;

import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.Message;
import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.messageObjectToSet;
import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition;
import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessagesMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class MessageHelperTest {

@Test
void toMessageObjectOrComposition_emptySet() {
assertThatThrownBy(() -> toMessageObjectOrComposition(Collections.emptySet()))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> toMessagesMap(Collections.emptySet())).isInstanceOf(IllegalArgumentException.class);
}

@Test
void toMessageObjectOrComposition_oneMessage() {
void toMessagesMap_oneMessage() {
MessageObject message = MessageObject.builder().name("foo").build();

Object asObject = toMessageObjectOrComposition(Set.of(message));
var messagesMap = toMessagesMap(Set.of(message));

assertThat(asObject).isInstanceOf(Message.class).isEqualTo(message);
assertThat(messagesMap).containsExactlyInAnyOrderEntriesOf(Map.of("foo", message));
}

@Test
void toMessageObjectOrComposition_multipleMessages() {
void toMessagesMap_multipleMessages() {
MessageObject message1 = MessageObject.builder().name("foo").build();

MessageObject message2 = MessageObject.builder().name("bar").build();

Object asObject = toMessageObjectOrComposition(Set.of(message1, message2));
var messages = toMessagesMap(Set.of(message1, message2));

assertThat(asObject).isInstanceOf(Map.class).isEqualTo(Map.of("oneOf", List.of(message2, message1)));
assertThat(messages).containsExactlyEntriesOf(Map.of("bar", message2, "foo", message1));
}

@Test
void toMessageObjectOrComposition_multipleMessages_remove_duplicates() {
void toMessagesMap_multipleMessages_remove_duplicates() {
MessageObject message1 = MessageObject.builder()
.name("foo")
.description("This is message 1")
Expand All @@ -60,17 +58,17 @@ void toMessageObjectOrComposition_multipleMessages_remove_duplicates() {
.description("This is message 3, but in essence the same payload type as message 2")
.build();

Object asObject = toMessageObjectOrComposition(Set.of(message1, message2, message3));
var messages = toMessagesMap(Set.of(message1, message2, message3));

Map<String, List<Message>> oneOfMap = (Map<String, List<Message>>) asObject;
assertThat(oneOfMap).hasSize(1);
List<Message> deduplicatedMessageList = oneOfMap.get("oneOf");
// we do not have any guarantee wether message2 or message3 won the deduplication.
assertThat(deduplicatedMessageList).hasSize(2).contains(message1).containsAnyOf(message2, message3);
// we do not have any guarantee whether message2 or message3 won the deduplication.
assertThat(messages)
.hasSize(2)
.containsValue(message1)
.containsAnyOf(entry("bar", message2), entry("bar", message3));
}

@Test
void toMessageObjectOrComposition_multipleMessages_should_not_break_deep_equals() {
void toMessagesMap_multipleMessages_should_not_break_deep_equals() {
MessageObject actualMessage1 = MessageObject.builder()
.name("foo")
.description("This is actual message 1")
Expand All @@ -81,7 +79,7 @@ void toMessageObjectOrComposition_multipleMessages_should_not_break_deep_equals(
.description("This is actual message 2")
.build();

Object actualObject = toMessageObjectOrComposition(Set.of(actualMessage1, actualMessage2));
Object actualObject = toMessagesMap(Set.of(actualMessage1, actualMessage2));

MessageObject expectedMessage1 = MessageObject.builder()
.name("foo")
Expand All @@ -93,7 +91,7 @@ void toMessageObjectOrComposition_multipleMessages_should_not_break_deep_equals(
.description("This is expected message 2")
.build();

Object expectedObject = toMessageObjectOrComposition(Set.of(expectedMessage1, expectedMessage2));
Object expectedObject = toMessagesMap(Set.of(expectedMessage1, expectedMessage2));

assertThat(actualObject).isNotEqualTo(expectedObject);
}
Expand All @@ -110,7 +108,7 @@ void messageObjectToSet_notAMessageOrAMap() {
@Test
void messageObjectToSet_Message() {
MessageObject message = MessageObject.builder().name("foo").build();
Object asObject = toMessageObjectOrComposition(Set.of(message));
Object asObject = toMessagesMap(Set.of(message));

Set<MessageObject> messages = messageObjectToSet(asObject);

Expand All @@ -123,7 +121,7 @@ void messageObjectToSet_SetOfMessage() {

MessageObject message2 = MessageObject.builder().name("bar").build();

Object asObject = toMessageObjectOrComposition(Set.of(message1, message2));
Object asObject = toMessagesMap(Set.of(message1, message2));

Set<MessageObject> messages = messageObjectToSet(asObject);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ void shouldMergeDifferentMessageForSameOperation() {

// then expectedMessage only includes message1 and message2.
// Message3 is not included as it is identical in terms of payload type (Message#name) to message 2
Object expectedMessages = MessageHelper.toMessageObjectOrComposition(Set.of(message1, message2));
Object expectedMessages = MessageHelper.toMessagesMap(Set.of(message1, message2));
assertThat(mergedChannels).hasSize(1);
// .hasEntrySatisfying(channelName, it -> {
// assertThat(it.getPublish())
Expand Down Expand Up @@ -188,7 +188,7 @@ void shouldUseOtherMessageIfFirstMessageIsMissing() {
Arrays.asList(Map.entry(channelName, publisherChannel1), Map.entry(channelName, publisherChannel2)));

// then expectedMessage message2
Object expectedMessages = MessageHelper.toMessageObjectOrComposition(Set.of(message2));
Object expectedMessages = MessageHelper.toMessagesMap(Set.of(message2));
assertThat(mergedChannels).hasSize(1).hasEntrySatisfying(channelName, it -> {
// assertThat(it.getPublish()) FIXME
// .isEqualTo(Operation.builder()
Expand Down

0 comments on commit 59c2231

Please sign in to comment.