Skip to content

Commit

Permalink
POC for Polymorphism (#890)
Browse files Browse the repository at this point in the history
* fix issue #874

* POC: adding polymorphism on payload

* POC: adding polymorphism on payload

* POC: refactoring for support for inline-schema's

* POC: refactoring for support for inline-schema's

* POC: refactoring for support for inline-schema's

* feat(ui): update server model

* feat(kafka): add ConsumerRecord to example

* test(core): align test setup

* test(core): minor changes

* resolving pull-request remarks

* chore: fixes after rebase

* feat(core): extract types using extractableClasses

* feat(ui): handle inline schemas

* test(ui): update ui tests

* feat(core): add inline schemas

also add to schemas section to allow publishing

* feat(ui): update mapping of example

* feat(e2e): refactor publishing and simplify payloadName retrieval

* feat(core): remove empty description

* trim newline on yaml-file in kafka-test

---------

Co-authored-by: David Beck <[email protected]>
  • Loading branch information
dabeck81 and David Beck authored Sep 13, 2024
1 parent 867201f commit 096c8c4
Show file tree
Hide file tree
Showing 80 changed files with 849 additions and 461 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.Message;
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.components.ComponentSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import jakarta.annotation.Nullable;

import java.lang.reflect.Type;
import java.util.Map;

public interface ComponentsService {

Map<String, SchemaObject> getSchemas();

@Nullable
SchemaObject resolveSchema(String schemaName);
ComponentSchema resolvePayloadSchema(Type type, String contentType);

String registerSchema(SchemaObject headers);

String resolvePayloadSchema(Class<?> type, String contentType);

Map<String, Message> getMessages();

MessageReference registerMessage(MessageObject message);

String getSchemaName(Type type);

String getSimpleSchemaName(Type type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.Message;
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.components.ComponentSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -27,11 +29,10 @@ public Map<String, SchemaObject> getSchemas() {
}

@Override
public SchemaObject resolveSchema(String schemaName) {
if (schemas.containsKey(schemaName)) {
return schemas.get(schemaName);
}
return null;
public ComponentSchema resolvePayloadSchema(Type type, String contentType) {
SwaggerSchemaService.Payload payload = schemaService.resolvePayloadSchema(type, contentType);
payload.referencedSchemas().forEach(this.schemas::putIfAbsent);
return payload.payloadSchema();
}

@Override
Expand All @@ -44,16 +45,6 @@ public String registerSchema(SchemaObject headers) {
return headers.getTitle();
}

@Override
public String resolvePayloadSchema(Class<?> type, String contentType) {
log.debug("Registering schema for {}", type.getSimpleName());

SwaggerSchemaService.ExtractedSchemas schemas = schemaService.extractSchema(type, contentType);
schemas.schemas().forEach(this.schemas::putIfAbsent);

return schemas.rootSchemaName();
}

@Override
public Map<String, Message> getMessages() {
return this.messages;
Expand All @@ -67,4 +58,14 @@ public MessageReference registerMessage(MessageObject message) {

return MessageReference.toComponentMessage(message);
}

@Override
public String getSchemaName(Type type) {
return schemaService.getNameFromType(type);
}

@Override
public String getSimpleSchemaName(Type type) {
return schemaService.getSimpleNameFromType(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import io.github.springwolf.asyncapi.v3.model.operation.OperationAction;
import io.github.springwolf.asyncapi.v3.model.schema.MultiFormatSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaReference;
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;
import io.github.springwolf.core.asyncapi.components.ComponentsService;
import io.github.springwolf.core.asyncapi.scanners.bindings.messages.MessageBindingProcessor;
Expand Down Expand Up @@ -99,17 +98,21 @@ protected MessageObject buildMessage(AsyncOperation operationData, Method method
Map<String, MessageBinding> messageBinding =
AsyncAnnotationUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors);

var messagePayload = MessagePayload.of(MultiFormatSchema.builder()
.schema(SchemaReference.fromSchema(payloadSchema.name()))
.build());
var messagePayload = MessagePayload.of(
MultiFormatSchema.builder().schema(payloadSchema.payload()).build());

String description = operationData.message().description();
if (StringUtils.isBlank(description) && payloadSchema.schema() != null) {
description = payloadSchema.schema().getDescription();
if (StringUtils.isBlank(description) && payloadSchema.payload() instanceof SchemaObject) {
String payloadDescription = ((SchemaObject) payloadSchema.payload()).getDescription();
if (StringUtils.isNotBlank(payloadDescription)) {
description = payloadDescription;
}
}
if (StringUtils.isNotBlank(description)) {
description = this.resolver.resolveStringValue(description);
description = TextUtils.trimIndent(description);
} else {
description = null;
}

var builder = MessageObject.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.springwolf.asyncapi.v3.model.schema.MultiFormatSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaReference;
import io.github.springwolf.core.asyncapi.components.ComponentsService;
import io.github.springwolf.core.asyncapi.scanners.bindings.BindingFactory;
import io.github.springwolf.core.asyncapi.scanners.common.headers.AsyncHeadersBuilder;
Expand Down Expand Up @@ -102,9 +101,8 @@ protected MessageObject buildMessage(

Map<String, MessageBinding> messageBinding = bindingFactory.buildMessageBinding(classAnnotation, headerSchema);

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

MessageObject message = MessageObject.builder()
.messageId(payloadSchema.name())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.springwolf.asyncapi.v3.model.schema.MultiFormatSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaReference;
import io.github.springwolf.core.asyncapi.components.ComponentsService;
import io.github.springwolf.core.asyncapi.scanners.bindings.BindingFactory;
import io.github.springwolf.core.asyncapi.scanners.common.headers.AsyncHeadersBuilder;
Expand Down Expand Up @@ -36,9 +35,8 @@ protected MessageObject buildMessage(

Map<String, MessageBinding> messageBinding = bindingFactory.buildMessageBinding(annotation, mergedHeaderSchema);

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

MessageObject message = MessageObject.builder()
.messageId(payloadSchema.name())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Optional;

@RequiredArgsConstructor
Expand All @@ -15,7 +16,7 @@ public class PayloadAsyncOperationService {
private final PayloadService payloadService;

public PayloadSchemaObject extractSchema(AsyncOperation operationData, Method method) {
Optional<Class<?>> payloadType = operationData.payloadType() != Object.class
Optional<Type> payloadType = operationData.payloadType() != Object.class
? Optional.of(operationData.payloadType())
: payloadClassExtractor.extractFrom(method);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Optional;

@RequiredArgsConstructor
Expand All @@ -14,7 +15,7 @@ public class PayloadMethodParameterService implements PayloadMethodService {
private final PayloadService payloadService;

public PayloadSchemaObject extractSchema(Method method) {
Optional<Class<?>> payloadType = payloadClassExtractor.extractFrom(method);
Optional<Type> payloadType = payloadClassExtractor.extractFrom(method);

return payloadType.map(payloadService::buildSchema).orElseGet(payloadService::useUnusedPayload);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.springwolf.core.asyncapi.scanners.common.payload;

import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
import io.github.springwolf.asyncapi.v3.model.components.ComponentSchema;
import jakarta.annotation.Nullable;

/**
* Encapsulates the resolved name for the contained schema.
*
* @param name The fully qualified name or the simple name of the schema.
* @param simpleSchemaName
* @param schema The SchemaObject.
* @param schemaPayload The schema-payload to be inserted in the message, when not null this schema will override the payload of the message.
*/
public record PayloadSchemaObject(String name, @Nullable SchemaObject schema) {
public record PayloadSchemaObject(String name, String simpleSchemaName, @Nullable ComponentSchema schema) {
public String title() {
return schema != null ? schema.getTitle() : name();
return (simpleSchemaName() != null) ? simpleSchemaName() : name();
}

public Object payload() {
if (schema() != null) {
if (schema().getSchema() != null) {
return schema().getSchema();
}
if (schema().getReference() != null) {
return schema().getReference();
}
if (schema().getMultiFormatSchema() != null) {
return schema().getMultiFormatSchema();
}
}
return MessageReference.toSchema(name());
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.springwolf.core.asyncapi.scanners.common.payload.internal;

import lombok.RequiredArgsConstructor;
import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.Payload;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;

@RequiredArgsConstructor
@Slf4j
public class PayloadClassExtractor {
private final TypeToClassConverter typeToClassConverter;
private final Map<String, Integer> extractableClassToArgumentIndex;

public Optional<Class<?>> extractFrom(Method method) {
public PayloadClassExtractor(SpringwolfConfigProperties properties) {
if (properties.getPayload() != null) {
extractableClassToArgumentIndex = properties.getPayload().getExtractableClasses();
} else {
extractableClassToArgumentIndex = Map.of();
}
}

public Optional<Type> extractFrom(Method method) {
String methodName = String.format("%s::%s", method.getDeclaringClass().getSimpleName(), method.getName());
log.debug("Finding payload type for {}", methodName);

return getPayloadParameterIndex(method.getParameterTypes(), method.getParameterAnnotations(), methodName)
.map((parameterPayloadIndex) ->
typeToClassConverter.extractClass(method.getGenericParameterTypes()[parameterPayloadIndex]));
.map((parameterPayloadIndex) -> method.getGenericParameterTypes()[parameterPayloadIndex])
.map(this::extractActualType);
}

private Optional<Integer> getPayloadParameterIndex(
Expand Down Expand Up @@ -56,4 +67,32 @@ private int getPayloadAnnotatedParameterIndex(Annotation[][] parameterAnnotation

return -1;
}

private Type extractActualType(Type parameterType) {
// TODO: add tests / adapt from TypeToClassConverterTest
Type type = parameterType;

while (type instanceof ParameterizedType typeParameterized) {
String typeName = ((ParameterizedType) type).getRawType().getTypeName();
if (!extractableClassToArgumentIndex.containsKey(typeName)) {
break;
}

Integer index = extractableClassToArgumentIndex.get(typeName);
type = typeParameterized.getActualTypeArguments()[index];

if (type instanceof WildcardType) {
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
if (upperBounds.length > 0 && upperBounds[0] != Object.class) {
type = upperBounds[0];
}
if (lowerBounds.length > 0 && lowerBounds[0] != Object.class) {
type = lowerBounds[0];
}
}
}

return type;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.springwolf.core.asyncapi.scanners.common.payload.internal;

import io.github.springwolf.asyncapi.v3.model.components.ComponentSchema;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
import io.github.springwolf.asyncapi.v3.model.schema.SchemaType;
import io.github.springwolf.core.asyncapi.components.ComponentsService;
Expand All @@ -9,6 +10,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Type;
import java.util.Map;

@Slf4j
Expand All @@ -20,34 +22,32 @@ public class PayloadService {
private static final String PAYLOAD_NOT_USED_KEY = "PayloadNotUsed";
public static final PayloadSchemaObject PAYLOAD_NOT_USED = new PayloadSchemaObject(
PAYLOAD_NOT_USED_KEY,
SchemaObject.builder()
PAYLOAD_NOT_USED_KEY,
ComponentSchema.of(SchemaObject.builder()
.type(SchemaType.OBJECT)
.title(PAYLOAD_NOT_USED_KEY)
.description("No payload specified")
.properties(Map.of())
.build());
.build()));

public PayloadSchemaObject buildSchema(Class<?> payloadType) {
public PayloadSchemaObject buildSchema(Type payloadType) {
String contentType = properties.getDocket().getDefaultContentType();

return buildSchema(contentType, payloadType);
}

public PayloadSchemaObject buildSchema(String contentType, Class<?> payloadType) {
String componentsSchemaName = this.componentsService.resolvePayloadSchema(payloadType, contentType);

SchemaObject schema = componentsService.resolveSchema(componentsSchemaName);
if (schema != null) {
schema.setTitle(payloadType.getSimpleName());
}
public PayloadSchemaObject buildSchema(String contentType, Type payloadType) {
String schemaName = componentsService.getSchemaName(payloadType);
String simpleSchemaName = componentsService.getSimpleSchemaName(payloadType);

return new PayloadSchemaObject(componentsSchemaName, schema);
ComponentSchema schema = componentsService.resolvePayloadSchema(payloadType, contentType);
return new PayloadSchemaObject(schemaName, simpleSchemaName, schema);
}

public PayloadSchemaObject useUnusedPayload() {
SchemaObject schema = PAYLOAD_NOT_USED.schema();
if (schema != null) {
this.componentsService.registerSchema(schema);
ComponentSchema schema = PAYLOAD_NOT_USED.schema();
if (schema != null && schema.getSchema() != null) {
this.componentsService.registerSchema(schema.getSchema());
}
return PAYLOAD_NOT_USED;
}
Expand Down
Loading

0 comments on commit 096c8c4

Please sign in to comment.