Skip to content

Commit

Permalink
Revert "chore: remove deprecated asyncapidocket bean (#524)" (#536)
Browse files Browse the repository at this point in the history
This reverts commit c4ea459.
  • Loading branch information
timonback authored Jan 19, 2024
1 parent 5ddb8ba commit b03008f
Show file tree
Hide file tree
Showing 23 changed files with 1,337 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.github.stavshamir.springwolf.asyncapi.SpringwolfInitApplicationListener;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.configuration.DefaultAsyncApiDocketService;
import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants;
Expand All @@ -30,6 +31,7 @@
import org.springframework.core.annotation.Order;

import java.util.List;
import java.util.Optional;

/**
* Spring Boot auto-configuration which loads all spring-beans for springwolf core module.
Expand Down Expand Up @@ -80,8 +82,9 @@ public SchemasService schemasService(

@Bean
@ConditionalOnMissingBean
public AsyncApiDocketService asyncApiDocketService(SpringwolfConfigProperties springwolfConfigProperties) {
return new DefaultAsyncApiDocketService(springwolfConfigProperties);
public AsyncApiDocketService asyncApiDocketService(
Optional<AsyncApiDocket> optionalAsyncApiDocket, SpringwolfConfigProperties springwolfConfigProperties) {
return new DefaultAsyncApiDocketService(optionalAsyncApiDocket, springwolfConfigProperties);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncAnnotationChannelsScanner;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ConsumerOperationDataScanner;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProducerOperationDataScanner;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher;
Expand All @@ -28,6 +30,8 @@

import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_SCANNER_ASYNC_LISTENER_ENABLED;
import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_SCANNER_ASYNC_PUBLISHER_ENABLED;
import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_SCANNER_CONSUMER_DATA_ENABLED;
import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_SCANNER_PRODUCER_DATA_ENABLED;

/**
* Spring configuration defining the core scanner beans.
Expand Down Expand Up @@ -61,6 +65,22 @@ public SpringwolfClassScanner springwolfClassScanner(
return new SpringwolfClassScanner(componentClassScanner, beanMethodsScanner);
}

@Bean
@ConditionalOnProperty(name = SPRINGWOLF_SCANNER_CONSUMER_DATA_ENABLED, havingValue = "true", matchIfMissing = true)
@Order(value = ChannelPriority.MANUAL_DEFINED)
public ConsumerOperationDataScanner consumerOperationDataScanner(
AsyncApiDocketService asyncApiDocketService, SchemasService schemasService) {
return new ConsumerOperationDataScanner(asyncApiDocketService, schemasService);
}

@Bean
@ConditionalOnProperty(name = SPRINGWOLF_SCANNER_PRODUCER_DATA_ENABLED, havingValue = "true", matchIfMissing = true)
@Order(value = ChannelPriority.MANUAL_DEFINED)
public ProducerOperationDataScanner producerOperationDataScanner(
AsyncApiDocketService asyncApiDocketService, SchemasService schemasService) {
return new ProducerOperationDataScanner(asyncApiDocketService, schemasService);
}

@Bean
@ConditionalOnProperty(
name = SPRINGWOLF_SCANNER_ASYNC_LISTENER_ENABLED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata;

import com.asyncapi.v2._6_0.model.channel.ChannelItem;
import com.asyncapi.v2._6_0.model.channel.operation.Operation;
import com.asyncapi.v2._6_0.model.server.Server;
import com.asyncapi.v2.binding.channel.ChannelBinding;
import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner;
import io.github.stavshamir.springwolf.asyncapi.types.OperationData;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

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

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

@Slf4j
@Deprecated(forRemoval = true)
public abstract class AbstractOperationDataScanner implements ChannelsScanner {

protected abstract SchemasService getSchemaService();

protected abstract AsyncApiDocketService getAsyncApiDocketService();

/**
* provides a list of all {@link OperationData} items detected by the concrete
* subclass.
*
* @return
*/
protected abstract List<OperationData> getOperationData();

protected abstract OperationData.OperationType getOperationType();

@Override
public Map<String, ChannelItem> scan() {
Map<String, List<OperationData>> operationDataGroupedByChannelName = this.getOperationData().stream()
.filter(this::allFieldsAreNonNull)
.collect(groupingBy(OperationData::getChannelName));

return operationDataGroupedByChannelName.entrySet().stream()
.collect(toMap(Map.Entry::getKey, entry -> buildChannel(entry.getValue())));
}

private boolean allFieldsAreNonNull(OperationData operationData) {
boolean allNonNull = operationData.getChannelName() != null
&& operationData.getPayloadType() != null
&& operationData.getOperationBinding() != null;

if (!allNonNull) {
log.warn("Some data fields are null - this producer will not be documented: {}", operationData);
}

return allNonNull;
}

/**
* Creates an asyncapi {@link ChannelItem} using the given list of {@link OperationData}. Expects, that all {@link OperationData}
* items belong to the same channel. Most properties of the resulting {@link ChannelItem} are extracted from the
* first {@link OperationData} item in the list, assuming that all {@link OperationData} contains the same channel
* informations.
*
* @param operationDataList List of all {@link OperationData} items for a single channel.
* @return the resulting {@link ChannelItem}
*/
private ChannelItem buildChannel(List<OperationData> operationDataList) {
// All bindings in the group are assumed to be the same
// AsyncApi does not support multiple bindings on a single channel
Map<String, ? extends ChannelBinding> channelBinding =
operationDataList.get(0).getChannelBinding();
Map<String, ? extends OperationBinding> operationBinding =
operationDataList.get(0).getOperationBinding();
Map<String, Object> opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null;
Map<String, Object> chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null;
String operationId = operationDataList.get(0).getChannelName() + "_" + this.getOperationType().operationName;
String description = operationDataList.get(0).getDescription();
List<String> servers = operationDataList.get(0).getServers();

if (description.isEmpty()) {
description = "Auto-generated description";
}

Operation operation = Operation.builder()
.description(description)
.operationId(operationId)
.message(getMessageObject(operationDataList))
.bindings(opBinding)
.build();

ChannelItem.ChannelItemBuilder channelBuilder = ChannelItem.builder().bindings(chBinding);
channelBuilder = switch (getOperationType()) {
case PUBLISH -> channelBuilder.publish(operation);
case SUBSCRIBE -> channelBuilder.subscribe(operation);};

// Only set servers if servers are defined. Avoid setting an emtpy list
// because this would generate empty server entries for each channel in the resulting
// async api.
if (servers != null && !servers.isEmpty()) {
validateServers(servers, operationId);
channelBuilder.servers(servers);
}
return channelBuilder.build();
}

private Object getMessageObject(List<OperationData> operationDataList) {
Set<Message> messages =
operationDataList.stream().map(this::buildMessage).collect(toSet());

return toMessageObjectOrComposition(messages);
}

private Message buildMessage(OperationData operationData) {
Class<?> payloadType = operationData.getPayloadType();
String modelName = this.getSchemaService().register(payloadType);
String headerModelName = this.getSchemaService().register(operationData.getHeaders());

/*
* Message information can be obtained via a @AsyncMessage annotation on the method parameter, the Payload
* itself or via the Swagger @Schema annotation on the Payload.
*/

var schema = payloadType.getAnnotation(Schema.class);
String description = schema != null ? schema.description() : null;

var builder = Message.builder()
.name(payloadType.getName())
.title(payloadType.getSimpleName())
.description(description)
.payload(PayloadReference.fromModelName(modelName))
.headers(HeaderReference.fromModelName(headerModelName))
.bindings(operationData.getMessageBinding());

// Retrieve the Message information obtained from the @AsyncMessage annotation. These values have higher
// priority
// so if we find them, we need to override the default values.
processAsyncMessageAnnotation(operationData.getMessage(), builder);

return builder.build();
}

private void processAsyncMessageAnnotation(Message annotationMessage, Message.MessageBuilder builder) {
if (annotationMessage != null) {
builder.messageId(annotationMessage.getMessageId());

var schemaFormat = annotationMessage.getSchemaFormat() != null
? annotationMessage.getSchemaFormat()
: Message.DEFAULT_SCHEMA_FORMAT;
builder.schemaFormat(schemaFormat);

var annotationMessageDescription = annotationMessage.getDescription();
if (StringUtils.hasText(annotationMessageDescription)) {
builder.description(annotationMessageDescription);
}

var name = annotationMessage.getName();
if (StringUtils.hasText(name)) {
builder.name(name);
}

var title = annotationMessage.getTitle();
if (StringUtils.hasText(title)) {
builder.title(title);
}
}
}

/**
* validates the given list of server names (for a specific operation) with the servers defined in the 'servers' part of
* the current AsyncApi.
*
* @param serversFromOperation the server names defined for the current operation
* @param operationId operationId of the current operation - used for exception messages
* @throws IllegalArgumentException if server from operation is not present in AsyncApi's servers definition.
*/
void validateServers(List<String> serversFromOperation, String operationId) {
if (!serversFromOperation.isEmpty()) {
Map<String, Server> asyncApiServers =
getAsyncApiDocketService().getAsyncApiDocket().getServers();
if (asyncApiServers == null || asyncApiServers.isEmpty()) {
throw new IllegalArgumentException(String.format(
"Operation '%s' defines server refs (%s) but there are no servers defined in this AsyncAPI.",
operationId, serversFromOperation));
}
for (String server : serversFromOperation) {
if (!asyncApiServers.containsKey(server)) {
throw new IllegalArgumentException(String.format(
"Operation '%s' defines unknown server ref '%s'. This AsyncApi defines these server(s): %s",
operationId, server, asyncApiServers.keySet()));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata;

import io.github.stavshamir.springwolf.asyncapi.types.OperationData;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Deprecated(forRemoval = true)
public class ConsumerOperationDataScanner extends AbstractOperationDataScanner {

private final AsyncApiDocketService asyncApiDocketService;
private final SchemasService schemasService;

@Override
protected SchemasService getSchemaService() {
return this.schemasService;
}

@Override
protected List<OperationData> getOperationData() {
return new ArrayList<>(asyncApiDocketService.getAsyncApiDocket().getConsumers());
}

@Override
protected AsyncApiDocketService getAsyncApiDocketService() {
return this.asyncApiDocketService;
}

@Override
protected OperationData.OperationType getOperationType() {
return OperationData.OperationType.PUBLISH;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata;

import io.github.stavshamir.springwolf.asyncapi.types.OperationData;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Deprecated(forRemoval = true)
public class ProducerOperationDataScanner extends AbstractOperationDataScanner {

private final AsyncApiDocketService asyncApiDocketService;
private final SchemasService schemasService;

@Override
protected SchemasService getSchemaService() {
return this.schemasService;
}

@Override
protected AsyncApiDocketService getAsyncApiDocketService() {
return this.asyncApiDocketService;
}

@Override
protected List<OperationData> getOperationData() {
return new ArrayList<>(asyncApiDocketService.getAsyncApiDocket().getProducers());
}

@Override
protected OperationData.OperationType getOperationType() {
return OperationData.OperationType.SUBSCRIBE;
}
}
Loading

0 comments on commit b03008f

Please sign in to comment.