Skip to content

Commit

Permalink
feat(core): document primitive types using envelope class and AsyncAp…
Browse files Browse the repository at this point in the history
…iPayload marker

GH-377
  • Loading branch information
timonback committed Jan 1, 2024
1 parent 09b6d7f commit 0199639
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
String[] servers() default {};

/**
* Overwrite the Springwolf auto-detected payload type.
* <p>
* Mapped to {@link OperationData#getPayloadType()}
*/
Class<?> payloadType() default Object.class;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload;

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;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;

/**
* Marker interface to indicate the actual payload class in envelop wrapper classes.
* <p>
* Since primitive data types cannot be documented directly (like String, Integer, etc.), an envelope class is needed:
* <pre>{@code
* public class StringEnvelop {
* @AsyncApiPayload
* @Schema(description = "Payload description using @Schema annotation", maxLength = 10)
* String payload;
* }
* }</pre>
* <p>
* In case the signature of the listener/publisher method can not be changed to use the envelope class,
* the envelope class can be set as {@link AsyncOperation#payloadType()}
* in the {@link AsyncListener} and/or {@link AsyncPublisher} annotation
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD})
@Inherited
public @interface AsyncApiPayload {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.schemas;

import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.AsyncApiPayload;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;
import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties;
import io.github.stavshamir.springwolf.schemas.example.ExampleGenerator;
Expand All @@ -12,8 +13,11 @@
import io.swagger.v3.oas.models.media.StringSchema;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -61,11 +65,17 @@ public String register(AsyncHeaders headers) {
public String register(Class<?> type) {
log.debug("Registering schema for {}", type.getSimpleName());

Map<String, Schema> schemas = runWithFqnSetting((unused) -> converter.readAll(type));
Map<String, Schema> schemas = new LinkedHashMap<>(runWithFqnSetting((unused) -> converter.readAll(type)));
String schemaName = getSchemaName(type, schemas);

preProcessSchemas(schemas, schemaName, type);
this.definitions.putAll(schemas);
schemas.values().forEach(this::postProcessSchema);

return schemaName;
}

private String getSchemaName(Class<?> type, Map<String, Schema> schemas) {
if (schemas.isEmpty() && type.equals(String.class)) {
return registerString();
}
Expand All @@ -74,14 +84,41 @@ public String register(Class<?> type) {
return new ArrayList<>(schemas.keySet()).get(0);
}

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

return type.getSimpleName();
}

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

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) {
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 %s. "
+ "Falling back and ignoring annotation.")
.formatted(type.getName()));
}
}

private String registerString() {
String schemaName = "String";
StringSchema schema = new StringSchema();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.AsyncApiPayload;
import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties;
import io.github.stavshamir.springwolf.schemas.example.ExampleGenerator;
import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator;
Expand Down Expand Up @@ -291,4 +292,51 @@ public class ImplementationTwo {
private Boolean secondTwo;
}
}

@Nested
class AsyncApiPayloadTest {
@Test
void stringEnvelopTest() throws IOException {
schemasService.register(StringEnvelop.class);

String actualDefinitions = objectMapper.writer(printer).writeValueAsString(schemasService.getDefinitions());
String expected = jsonResource("/schemas/api-payload.json");

System.out.println("Got: " + actualDefinitions);
assertEquals(expected, actualDefinitions);

assertThat(actualDefinitions).doesNotContain("otherField");
}

@Test
void illegalEnvelopTest() throws IOException {
schemasService.register(EnvelopWithMultipleAsyncApiPayloadAnnotations.class);

String actualDefinitions = objectMapper.writer(printer).writeValueAsString(schemasService.getDefinitions());

// fallback to EnvelopWithMultipleAsyncApiPayloadAnnotations, which contains the field
assertThat(actualDefinitions).contains("otherField");
}

@Data
@NoArgsConstructor
public class StringEnvelop {
Integer otherField;

@AsyncApiPayload
@Schema(description = "The payload in the envelop", maxLength = 10)
String payload;
}

@Data
@NoArgsConstructor
public class EnvelopWithMultipleAsyncApiPayloadAnnotations {
@AsyncApiPayload
Integer otherField;

@AsyncApiPayload
@Schema(description = "The payload in the envelop", maxLength = 10)
String payload;
}
}
}
8 changes: 8 additions & 0 deletions springwolf-core/src/test/resources/schemas/api-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"StringEnvelop" : {
"maxLength" : 10,
"type" : "string",
"description" : "The payload in the envelop",
"example" : "string"
}
}

0 comments on commit 0199639

Please sign in to comment.