Skip to content

Commit f1cc2cc

Browse files
committed
#59 - Add support for multiple producers with the same channel name
1 parent 20d6357 commit f1cc2cc

File tree

10 files changed

+214
-40
lines changed

10 files changed

+214
-40
lines changed

springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ProducerChannelScanner.java

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.asyncapi.v2.binding.OperationBinding;
44
import com.asyncapi.v2.model.channel.ChannelItem;
55
import com.asyncapi.v2.model.channel.operation.Operation;
6+
import com.google.common.collect.ImmutableMap;
67
import io.github.stavshamir.springwolf.asyncapi.types.ProducerData;
78
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
89
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference;
@@ -12,9 +13,12 @@
1213
import lombok.extern.slf4j.Slf4j;
1314
import org.springframework.stereotype.Component;
1415

16+
import java.util.List;
1517
import java.util.Map;
18+
import java.util.Set;
1619

17-
import static java.util.stream.Collectors.toMap;
20+
import static io.github.stavshamir.springwolf.asyncapi.Constants.ONE_OF;
21+
import static java.util.stream.Collectors.*;
1822

1923
@Slf4j
2024
@RequiredArgsConstructor
@@ -26,9 +30,12 @@ public class ProducerChannelScanner implements ChannelsScanner {
2630

2731
@Override
2832
public Map<String, ChannelItem> scan() {
29-
return docket.getProducers().stream()
33+
Map<String, List<ProducerData>> producerDataGroupedByChannelName = docket.getProducers().stream()
3034
.filter(this::allFieldsAreNonNull)
31-
.collect(toMap(ProducerData::getChannelName, this::buildChannel));
35+
.collect(groupingBy(ProducerData::getChannelName));
36+
37+
return producerDataGroupedByChannelName.entrySet().stream()
38+
.collect(toMap(Map.Entry::getKey, entry -> buildChannel(entry.getValue())));
3239
}
3340

3441
private boolean allFieldsAreNonNull(ProducerData producerData) {
@@ -43,26 +50,40 @@ private boolean allFieldsAreNonNull(ProducerData producerData) {
4350
return allNonNull;
4451
}
4552

46-
private ChannelItem buildChannel(ProducerData producerData) {
47-
Class<?> payloadType = producerData.getPayloadType();
48-
Map<String, ? extends OperationBinding> operationBinding = producerData.getBinding();
49-
50-
String modelName = schemasService.register(payloadType);
51-
52-
Message message = Message.builder()
53-
.name(payloadType.getName())
54-
.title(modelName)
55-
.payload(PayloadReference.fromModelName(modelName))
56-
.build();
53+
private ChannelItem buildChannel(List<ProducerData> producerDataList) {
54+
// All bindings in the group are assumed to be the same
55+
// AsyncApi does not support multiple bindings on a single channel
56+
Map<String, ? extends OperationBinding> binding = producerDataList.get(0).getBinding();
5757

5858
Operation operation = Operation.builder()
59-
.message(message)
60-
.bindings(operationBinding)
59+
.message(getMessageObject(producerDataList))
60+
.bindings(binding)
6161
.build();
6262

6363
return ChannelItem.builder()
6464
.subscribe(operation)
6565
.build();
6666
}
6767

68+
private Object getMessageObject(List<ProducerData> producerDataList) {
69+
Set<Message> messages = producerDataList.stream()
70+
.map(this::buildMessage)
71+
.collect(toSet());
72+
73+
return messages.size() == 1
74+
? messages.toArray()[0]
75+
: ImmutableMap.of(ONE_OF, messages);
76+
}
77+
78+
private Message buildMessage(ProducerData producerData) {
79+
Class<?> payloadType = producerData.getPayloadType();
80+
String modelName = schemasService.register(payloadType);
81+
82+
return Message.builder()
83+
.name(payloadType.getName())
84+
.title(modelName)
85+
.payload(PayloadReference.fromModelName(modelName))
86+
.build();
87+
}
88+
6889
}

springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ProducerChannelScannerTest.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import com.asyncapi.v2.binding.kafka.KafkaOperationBinding;
44
import com.asyncapi.v2.model.channel.ChannelItem;
5+
import com.asyncapi.v2.model.channel.operation.Operation;
56
import com.google.common.collect.ImmutableList;
67
import com.google.common.collect.ImmutableMap;
8+
import com.google.common.collect.ImmutableSet;
79
import io.github.stavshamir.springwolf.asyncapi.types.ProducerData;
10+
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
11+
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference;
812
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
913
import io.github.stavshamir.springwolf.schemas.DefaultSchemasService;
1014
import org.junit.Test;
@@ -15,7 +19,9 @@
1519
import org.springframework.test.context.junit4.SpringRunner;
1620

1721
import java.util.Map;
22+
import java.util.Set;
1823

24+
import static io.github.stavshamir.springwolf.asyncapi.Constants.ONE_OF;
1925
import static org.assertj.core.api.Assertions.assertThat;
2026
import static org.mockito.Mockito.when;
2127

@@ -47,6 +53,22 @@ public void allFieldsProducerData() {
4753
// Then the channel should be created correctly
4854
assertThat(producerChannels)
4955
.containsKey(channelName);
56+
57+
Operation operation = Operation.builder()
58+
.bindings(ImmutableMap.of("kafka", new KafkaOperationBinding()))
59+
.message(Message.builder()
60+
.name(ExamplePayloadDto.class.getName())
61+
.title(ExamplePayloadDto.class.getSimpleName())
62+
.payload(PayloadReference.fromModelName(ExamplePayloadDto.class.getSimpleName()))
63+
.build())
64+
.build();
65+
66+
ChannelItem expectedChannel = ChannelItem.builder()
67+
.subscribe(operation)
68+
.build();
69+
70+
assertThat(producerChannels.get(channelName))
71+
.isEqualTo(expectedChannel);
5072
}
5173

5274
@Test
@@ -65,8 +87,66 @@ public void missingFieldProducerData() {
6587
// Then the channel is not created, and no exception is thrown
6688
assertThat(producerChannels).isEmpty();
6789
}
90+
91+
@Test
92+
public void multipleProducersForSameTopic() {
93+
// Given a multiple ProducerData objects for the same topic
94+
String channelName = "example-producer-topic";
95+
96+
ProducerData producerData1 = ProducerData.builder()
97+
.channelName(channelName)
98+
.binding(ImmutableMap.of("kafka", new KafkaOperationBinding()))
99+
.payloadType(ExamplePayloadDto.class)
100+
.build();
101+
102+
ProducerData producerData2 = ProducerData.builder()
103+
.channelName(channelName)
104+
.binding(ImmutableMap.of("kafka", new KafkaOperationBinding()))
105+
.payloadType(AnotherExamplePayloadDto.class)
106+
.build();
107+
108+
when(asyncApiDocket.getProducers()).thenReturn(ImmutableList.of(producerData1, producerData2));
109+
110+
// When scanning for producers
111+
Map<String, ChannelItem> producerChannels = scanner.scan();
112+
113+
// Then one channel is created for the ProducerData objects with multiple messages
114+
assertThat(producerChannels)
115+
.hasSize(1)
116+
.containsKey(channelName);
117+
118+
Set<Message> messages = ImmutableSet.of(
119+
Message.builder()
120+
.name(ExamplePayloadDto.class.getName())
121+
.title(ExamplePayloadDto.class.getSimpleName())
122+
.payload(PayloadReference.fromModelName(ExamplePayloadDto.class.getSimpleName()))
123+
.build(),
124+
Message.builder()
125+
.name(AnotherExamplePayloadDto.class.getName())
126+
.title(AnotherExamplePayloadDto.class.getSimpleName())
127+
.payload(PayloadReference.fromModelName(AnotherExamplePayloadDto.class.getSimpleName()))
128+
.build()
129+
);
130+
131+
Operation operation = Operation.builder()
132+
.bindings(ImmutableMap.of("kafka", new KafkaOperationBinding()))
133+
.message(ImmutableMap.of(ONE_OF, messages))
134+
.build();
135+
136+
ChannelItem expectedChannel = ChannelItem.builder()
137+
.subscribe(operation)
138+
.build();
139+
140+
assertThat(producerChannels.get(channelName))
141+
.isEqualTo(expectedChannel);
142+
}
143+
68144
static class ExamplePayloadDto {
69145
private String foo;
70146
}
71147

148+
static class AnotherExamplePayloadDto {
149+
private String bar;
150+
}
151+
72152
}

springwolf-examples/springwolf-kafka-example/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
}
88

99
sourceCompatibility = '1.8'
10-
version '0.4.0'
10+
version '0.5.0'
1111

1212

1313
repositories {
@@ -23,7 +23,7 @@ repositories {
2323

2424
dependencies {
2525
implementation project(":springwolf-plugins:springwolf-kafka-plugin")
26-
runtimeOnly 'io.github.springwolf:springwolf-ui:0.3.1'
26+
runtimeOnly 'io.github.springwolf:springwolf-ui:0.4.0'
2727

2828
implementation 'org.springframework.boot:spring-boot-starter-web'
2929
implementation 'org.springframework.kafka:spring-kafka'

springwolf-examples/springwolf-kafka-example/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: '3'
22
services:
33
app:
4-
image: stavshamir/springwolf-kafka-example:0.4.0
4+
image: stavshamir/springwolf-kafka-example:0.5.0
55
links:
66
- kafka
77
environment:

springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/configuration/AsyncApiConfiguration.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import com.asyncapi.v2.model.server.Server;
66
import com.google.common.collect.ImmutableMap;
77
import io.github.stavshamir.springwolf.asyncapi.types.ProducerData;
8-
import io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto;
98
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
109
import io.github.stavshamir.springwolf.configuration.EnableAsyncApi;
10+
import io.github.stavshamir.springwolf.example.dtos.AnotherPayloadDto;
11+
import io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto;
1112
import org.springframework.beans.factory.annotation.Value;
1213
import org.springframework.context.annotation.Bean;
1314
import org.springframework.context.annotation.Configuration;
1415

16+
import static io.github.stavshamir.springwolf.example.configuration.KafkaConfiguration.PRODUCER_TOPIC;
17+
1518
@Configuration
1619
@EnableAsyncApi
1720
public class AsyncApiConfiguration {
@@ -29,20 +32,24 @@ public AsyncApiDocket asyncApiDocket() {
2932
.title("Springwolf example project")
3033
.build();
3134

32-
ProducerData exampleProducerData = ProducerData.builder()
33-
.channelName("example-producer-topic")
34-
.binding(ImmutableMap.of("kafka", new KafkaOperationBinding()))
35-
.payloadType(ExamplePayloadDto.class)
36-
.build();
35+
ProducerData exampleProducerData = buildKafkaProducerData(PRODUCER_TOPIC, ExamplePayloadDto.class);
36+
ProducerData anotherProducerData = buildKafkaProducerData(PRODUCER_TOPIC, AnotherPayloadDto.class);
3737

3838
return AsyncApiDocket.builder()
3939
.basePackage("io.github.stavshamir.springwolf.example.consumers")
4040
.info(info)
4141
.server("kafka", Server.builder().protocol("kafka").url(BOOTSTRAP_SERVERS).build())
4242
.producer(exampleProducerData)
43+
.producer(anotherProducerData)
4344
.build();
4445
}
4546

46-
47+
private ProducerData buildKafkaProducerData(String topic, Class<?> payload) {
48+
return ProducerData.builder()
49+
.channelName(topic)
50+
.binding(ImmutableMap.of("kafka", new KafkaOperationBinding()))
51+
.payloadType(payload)
52+
.build();
53+
}
4754

4855
}

springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/configuration/KafkaConfiguration.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@EnableKafka
2525
public class KafkaConfiguration {
2626

27+
public final static String PRODUCER_TOPIC = "example-producer-topic";
2728
private final String BOOTSTRAP_SERVERS;
2829

2930
public KafkaConfiguration(@Value("${kafka.bootstrap.servers}") String bootstrapServers) {
@@ -66,15 +67,22 @@ public ConcurrentKafkaListenerContainerFactory<String, AnotherPayloadDto> anothe
6667
return containerFactory;
6768
}
6869

70+
private Map<String, Object> producerConfiguration() {
71+
return ImmutableMap.of(
72+
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS,
73+
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
74+
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class
75+
);
76+
}
77+
6978
@Bean
7079
public KafkaTemplate<String, ExamplePayloadDto> examplePayloadKafkaTemplate() {
71-
Map<String, Object> configuration = ImmutableMap.of(
72-
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS,
73-
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
74-
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class
75-
);
80+
return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfiguration()));
81+
}
7682

77-
return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(configuration));
83+
@Bean
84+
public KafkaTemplate<String, AnotherPayloadDto> anotherPayloadKafkaTemplate() {
85+
return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfiguration()));
7886
}
7987

8088
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.github.stavshamir.springwolf.example.producers;
2+
3+
import io.github.stavshamir.springwolf.example.dtos.AnotherPayloadDto;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.kafka.core.KafkaTemplate;
6+
import org.springframework.stereotype.Component;
7+
8+
import static io.github.stavshamir.springwolf.example.configuration.KafkaConfiguration.PRODUCER_TOPIC;
9+
10+
@Component
11+
public class AnotherProducer {
12+
13+
@Autowired
14+
private KafkaTemplate<String, AnotherPayloadDto> kafkaTemplate;
15+
16+
public void sendMessage(AnotherPayloadDto msg) {
17+
kafkaTemplate.send(PRODUCER_TOPIC, msg);
18+
}
19+
20+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
import org.springframework.kafka.core.KafkaTemplate;
66
import org.springframework.stereotype.Component;
77

8+
import static io.github.stavshamir.springwolf.example.configuration.KafkaConfiguration.PRODUCER_TOPIC;
9+
810
@Component
911
public class ExampleProducer {
1012

1113
@Autowired
1214
private KafkaTemplate<String, ExamplePayloadDto> kafkaTemplate;
1315

1416
public void sendMessage(ExamplePayloadDto msg) {
15-
kafkaTemplate.send("example-producer-topic", msg);
17+
kafkaTemplate.send(PRODUCER_TOPIC, msg);
1618
}
1719

1820
}

0 commit comments

Comments
 (0)