Skip to content

Commit

Permalink
Read spring header annotation (#787)
Browse files Browse the repository at this point in the history
* feat(core): add HeaderClassExtractor

Co-authored-by: Carlos Tasada <[email protected]>

* feat(core): integrate HeaderClassExtractor

* feat(kafka): add Kafka key to example

* refactor(core): use SchemasService in HeaderClassExtractor

* refactor(core): rename SchemaService to SwaggerSchemaService

* refactor(core): cleanup

* refactor(core): rename to ExtractedSchemas

* fix(gradle): declare variables globally, so that tasks can use them

updateAsyncApiJson used the gradle plugins instead of the variable

* refactor(core): isolate swagger into SwaggerSchemaService

DefaultComponentsService does not use swagger schemas anymore

As a convenient side-effect, the schema title (re-)appeared.

---------

Co-authored-by: Carlos Tasada <[email protected]>
  • Loading branch information
timonback and ctasada authored Jun 21, 2024
1 parent b141389 commit b448ef8
Show file tree
Hide file tree
Showing 56 changed files with 999 additions and 416 deletions.
45 changes: 23 additions & 22 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ plugins {

applyDotEnvFileAsGradleProperties()

var addons = [
'springwolf-common-model-converters' ,
'springwolf-generic-binding' ,
'springwolf-json-schema',
'springwolf-kotlinx-serialization-model-converter'
]
var bindings = [
'springwolf-amqp-binding',
'springwolf-googlepubsub-binding',
'springwolf-jms-binding',
'springwolf-kafka-binding',
'springwolf-sns-binding',
'springwolf-sqs-binding'
]
var plugins = [
'springwolf-amqp',
'springwolf-cloud-stream',
'springwolf-jms',
'springwolf-kafka',
'springwolf-sns',
'springwolf-sqs'
]

allprojects {
apply plugin: 'java'
apply plugin: 'maven-publish'
Expand Down Expand Up @@ -141,28 +164,6 @@ allprojects {
}
}

var addons = [
'springwolf-common-model-converters' ,
'springwolf-generic-binding' ,
'springwolf-json-schema',
'springwolf-kotlinx-serialization-model-converter'
]
var bindings = [
'springwolf-amqp-binding',
'springwolf-googlepubsub-binding',
'springwolf-jms-binding',
'springwolf-kafka-binding',
'springwolf-sns-binding',
'springwolf-sqs-binding'
]
var plugins = [
'springwolf-amqp',
'springwolf-cloud-stream',
'springwolf-jms',
'springwolf-kafka',
'springwolf-sns',
'springwolf-sqs'
]
var publishingEnabled = (
project.name == 'springwolf-asyncapi' ||
project.name == 'springwolf-core' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.springwolf.asyncapi.v3.model.components.ComponentSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.core.asyncapi.components.SwaggerSchemaUtil;
import io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaUtil;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,31 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageObject;
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.core.asyncapi.annotations.AsyncApiPayload;
import io.github.springwolf.core.asyncapi.components.postprocessors.SchemasPostProcessor;
import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.jackson.TypeNameResolver;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import static io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties.ConfigDocket.DEFAULT_CONTENT_TYPE;

@Slf4j
@AllArgsConstructor
public class DefaultComponentsService implements ComponentsService {

private final ModelConverters converter = ModelConverters.getInstance();
private final List<SchemasPostProcessor> schemaPostProcessors;
private final SwaggerSchemaUtil swaggerSchemaUtil;
private final SpringwolfConfigProperties properties;
private final SwaggerSchemaService schemaService;

private final Map<String, Schema> schemas = new HashMap<>();
private final Map<String, SchemaObject> schemas = new HashMap<>();
private final Map<String, Message> messages = new HashMap<>();

public DefaultComponentsService(
List<ModelConverter> externalModelConverters,
List<SchemasPostProcessor> schemaPostProcessors,
SwaggerSchemaUtil swaggerSchemaUtil,
SpringwolfConfigProperties properties) {

externalModelConverters.forEach(converter::addConverter);
this.schemaPostProcessors = schemaPostProcessors;
this.swaggerSchemaUtil = swaggerSchemaUtil;
this.properties = properties;
}

@Override
public Map<String, SchemaObject> getSchemas() {
return schemas.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(), swaggerSchemaUtil.mapSchema(entry.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return schemas;
}

@Override
public SchemaObject resolveSchema(String schemaName) {
if (schemas.containsKey(schemaName)) {
return swaggerSchemaUtil.mapSchema(schemas.get(schemaName));
return schemas.get(schemaName);
}
return null;
}
Expand All @@ -73,36 +38,20 @@ public SchemaObject resolveSchema(String schemaName) {
public String registerSchema(SchemaObject headers) {
log.debug("Registering schema for {}", headers.getTitle());

ObjectSchema headerSchema = new ObjectSchema();
headerSchema.setName(headers.getTitle());
headerSchema.setDescription(headers.getDescription());
Map<String, Schema> properties = headers.getProperties().entrySet().stream()
.map((property) -> Map.entry(property.getKey(), (Schema<?>)
swaggerSchemaUtil.mapToSwagger((SchemaObject) property.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
headerSchema.setProperties(properties);

this.schemas.put(headers.getTitle(), headerSchema);
postProcessSchema(headerSchema, DEFAULT_CONTENT_TYPE);
SchemaObject headerSchema = schemaService.extractSchema(headers);
this.schemas.putIfAbsent(headers.getTitle(), headerSchema);

return headers.getTitle();
}

@Override
public String registerSchema(Class<?> type, String contentType) {
log.debug("Registering schema for {}", type.getSimpleName());
String actualContentType =
StringUtils.isBlank(contentType) ? properties.getDocket().getDefaultContentType() : contentType;

Map<String, Schema> newSchemas = new LinkedHashMap<>(runWithFqnSetting((unused) -> converter.readAll(type)));
SwaggerSchemaService.ExtractedSchemas schemas = schemaService.extractSchema(type, contentType);
schemas.schemas().forEach(this.schemas::putIfAbsent);

String schemaName = getSchemaName(type, newSchemas);

preProcessSchemas(newSchemas, schemaName, type);
newSchemas.forEach(this.schemas::putIfAbsent);
newSchemas.values().forEach(schema -> postProcessSchema(schema, actualContentType));

return schemaName;
return schemas.rootSchemaName();
}

@Override
Expand All @@ -118,124 +67,4 @@ public MessageReference registerMessage(MessageObject message) {

return MessageReference.toComponentMessage(message);
}

private String getSchemaName(Class<?> type, Map<String, Schema> schemas) {
if (schemas.isEmpty()) {
// swagger-parser does not create schemas for primitives
if (type.equals(String.class) || type.equals(Character.class) || type.equals(Byte.class)) {
return registerPrimitive(String.class, new StringSchema());
}
if (Boolean.class.isAssignableFrom(type)) {
return registerPrimitive(Boolean.class, new BooleanSchema());
}
if (Number.class.isAssignableFrom(type)) {
return registerPrimitive(Number.class, new NumberSchema());
}
}

if (schemas.size() == 1) {
return schemas.keySet().stream().findFirst().get();
}

Set<String> resolvedPayloadModelName =
runWithFqnSetting((unused) -> converter.read(type).keySet());
if (!resolvedPayloadModelName.isEmpty()) {
return resolvedPayloadModelName.stream().findFirst().get();
}

return getNameFromClass(type);
}

private void preProcessSchemas(Map<String, Schema> schemas, String schemaName, Class<?> type) {
processCommonModelConverters(schemas);
processAsyncApiPayloadAnnotation(schemas, schemaName, type);
processSchemaAnnotation(schemas, schemaName, type);
}

private void processCommonModelConverters(Map<String, Schema> schemas) {
schemas.values().stream()
.filter(schema -> schema.getType() == null)
.filter(schema -> schema.get$ref() != null)
.forEach(schema -> {
String targetSchemaName = schema.getName();
String sourceSchemaName = StringUtils.substringAfterLast(schema.get$ref(), "/");

Schema<?> actualSchema = schemas.get(sourceSchemaName);

if (actualSchema != null) {
schemas.put(targetSchemaName, actualSchema);
schemas.remove(sourceSchemaName);
}
});
}

private void processSchemaAnnotation(Map<String, Schema> schemas, String schemaName, Class<?> type) {
Schema schemaForType = schemas.get(schemaName);
if (schemaForType != null) {
var schemaAnnotation = type.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
if (schemaAnnotation != null) {
schemaForType.setDescription(schemaAnnotation.description());
}
}
}

private void processAsyncApiPayloadAnnotation(Map<String, Schema> schemas, String schemaName, Class<?> type) {
List<Field> withPayloadAnnotatedFields = Arrays.stream(type.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(AsyncApiPayload.class))
.toList();

if (withPayloadAnnotatedFields.size() == 1) {
Schema envelopSchema = schemas.get(schemaName);
if (envelopSchema != null && envelopSchema.getProperties() != null) {
String fieldName = withPayloadAnnotatedFields.get(0).getName();
Schema actualSchema = (Schema) envelopSchema.getProperties().get(fieldName);
if (actualSchema != null) {
schemas.put(schemaName, actualSchema);
}
}

} else if (withPayloadAnnotatedFields.size() > 1) {
log.warn(
("Found more than one field with @AsyncApiPayload annotation in class {}. "
+ "Falling back and ignoring annotation."),
type.getName());
}
}

private String registerPrimitive(Class<?> type, Schema schema) {
String schemaName = getNameFromClass(type);
schema.setName(schemaName);

this.schemas.put(schemaName, schema);
postProcessSchema(schema, DEFAULT_CONTENT_TYPE);

return schemaName;
}

private <R> R runWithFqnSetting(Function<Void, R> callable) {
boolean previousUseFqn = TypeNameResolver.std.getUseFqn();
TypeNameResolver.std.setUseFqn(properties.isUseFqn());

R result = callable.apply(null);

TypeNameResolver.std.setUseFqn(previousUseFqn);
return result;
}

private String getNameFromClass(Class<?> type) {
if (properties.isUseFqn()) {
return type.getName();
}
return type.getSimpleName();
}

private void postProcessSchema(Schema schema, String contentType) {
for (SchemasPostProcessor processor : schemaPostProcessors) {
processor.process(schema, schemas, contentType);

if (!schemas.containsValue(schema)) {
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.springwolf.asyncapi.v3.bindings.ChannelBinding;
import io.github.springwolf.asyncapi.v3.bindings.MessageBinding;
import io.github.springwolf.asyncapi.v3.bindings.OperationBinding;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;

import java.util.Map;

Expand All @@ -14,5 +15,5 @@ public interface BindingFactory<T> {

Map<String, OperationBinding> buildOperationBinding(T annotation);

Map<String, MessageBinding> buildMessageBinding(T annotation);
Map<String, MessageBinding> buildMessageBinding(T annotation, SchemaObject headerSchema);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.springwolf.core.asyncapi.scanners.bindings.BindingFactory;
import io.github.springwolf.core.asyncapi.scanners.common.ClassLevelAnnotationScanner;
import io.github.springwolf.core.asyncapi.scanners.common.headers.AsyncHeadersBuilder;
import io.github.springwolf.core.asyncapi.scanners.common.headers.HeaderClassExtractor;
import io.github.springwolf.core.asyncapi.scanners.common.payload.PayloadService;
import io.github.springwolf.core.asyncapi.scanners.common.utils.AnnotationScannerUtil;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -31,13 +32,15 @@ public SpringAnnotationClassLevelChannelsScanner(
BindingFactory<ClassAnnotation> bindingFactory,
AsyncHeadersBuilder asyncHeadersBuilder,
PayloadService payloadService,
HeaderClassExtractor headerClassExtractor,
ComponentsService componentsService) {
super(
classAnnotationClass,
methodAnnotationClass,
bindingFactory,
asyncHeadersBuilder,
payloadService,
headerClassExtractor,
componentsService);
}

Expand Down
Loading

0 comments on commit b448ef8

Please sign in to comment.