diff --git a/packages/java/parser-jvm-plugin-subtypes/src/main/java/com/vaadin/hilla/parser/plugins/subtypes/SubTypesPlugin.java b/packages/java/parser-jvm-plugin-subtypes/src/main/java/com/vaadin/hilla/parser/plugins/subtypes/SubTypesPlugin.java index 4ad9adbe3a..9609951c27 100644 --- a/packages/java/parser-jvm-plugin-subtypes/src/main/java/com/vaadin/hilla/parser/plugins/subtypes/SubTypesPlugin.java +++ b/packages/java/parser-jvm-plugin-subtypes/src/main/java/com/vaadin/hilla/parser/plugins/subtypes/SubTypesPlugin.java @@ -20,17 +20,19 @@ 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 org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.jspecify.annotations.NonNull; + import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Stream; /** - * This plugin adds support for {@code @JsonTypeInfo} and {@code @JsonSubTypes}. + * This plugin adds support for {@code @JsonTypeInfo} and + * {@code @JsonSubTypes}. */ public final class SubTypesPlugin extends AbstractPlugin { @Override @@ -50,20 +52,19 @@ public void exit(NodePath nodePath) { if (cls.getAnnotationsByType(JsonTypeInfo.class).length > 0) { var schema = (Schema) unionNode.getTarget(); getJsonSubTypes(cls).map(JsonSubTypes.Type::value) - .forEach(c -> { - schema.addOneOfItem(new Schema() { - { - set$ref("#/components/schemas/" - + c.getName()); - } - }); + .forEach(c -> { + schema.addOneOfItem(new Schema<>() { + { + set$ref("#/components/schemas/" + c.getName()); + } }); + }); } // attach the schema to the openapi EntityPlugin.attachSchemaWithNameToOpenApi(unionNode.getTarget(), - cls.getName() + "Union", - (OpenAPI) nodePath.getParentPath().getNode().getTarget()); + cls.getName() + "Union", + (OpenAPI) nodePath.getParentPath().getNode().getTarget()); } // entity nodes whose superclass has a @JsonSubTypes annotation must @@ -72,23 +73,35 @@ public void exit(NodePath nodePath) { var entityNode = (EntityNode) nodePath.getNode(); var cls = (Class) entityNode.getSource().get(); - Optional.ofNullable(cls.getSuperclass()) - .map(SubTypesPlugin::getJsonSubTypes).stream() - .flatMap(Function.identity()) - .filter(t -> cls.equals(t.value())).findAny() - .ifPresent(t -> { - var schema = (ComposedSchema) entityNode.getTarget(); - schema.getAnyOf().stream() + getJsonSubTypeInHierarchy(cls).ifPresent(foundSubTypeInfo -> { + JsonTypeInfo info = foundSubTypeInfo.getLeft(); + JsonSubTypes.Type[] types = foundSubTypeInfo.getRight(); + + String property = StringUtils.isNotBlank(info.property()) ? + info.property() : + info.use().getDefaultPropertyName(); + + Arrays.stream(types).filter(e -> e.value().equals(cls)) + .findAny().ifPresent(t -> { + var schema = entityNode.getTarget(); + if (schema instanceof ComposedSchema composedSchema) { + composedSchema.getAnyOf().stream() .filter(s -> s instanceof ObjectSchema) - .map(ObjectSchema.class::cast) - .forEach(s -> s.addProperty("@type", - new StringSchema() { - { - setType("string"); - setExample(t.name()); - } - })); + .map(ObjectSchema.class::cast).forEach(s -> { + StringSchema newProperty = new StringSchema(); + newProperty.setType("string"); + newProperty.setExample(t.name()); + s.addProperty(property, newProperty); + }); + } else if (schema instanceof ObjectSchema objectSchema) { + StringSchema newProperty = new StringSchema(); + newProperty.setType("string"); + newProperty.setExample(t.name()); + objectSchema.addProperty(property, newProperty); + } + }); + }); } } @@ -100,16 +113,14 @@ public Collection> getRequiredPlugins() { @NonNull @Override public NodeDependencies scan(@NonNull NodeDependencies nodeDependencies) { - if (!(nodeDependencies.getNode() instanceof TypedNode)) { + if (!(nodeDependencies.getNode() instanceof TypedNode typedNode)) { return nodeDependencies; } - var typedNode = (TypedNode) nodeDependencies.getNode(); - if (!(typedNode.getType() instanceof ClassRefSignatureModel)) { + if (!(typedNode.getType() instanceof ClassRefSignatureModel ref)) { return nodeDependencies; } - var ref = (ClassRefSignatureModel) typedNode.getType(); if (ref.isJDKClass() || ref.isDate() || ref.isIterable()) { return nodeDependencies; } @@ -118,7 +129,7 @@ public NodeDependencies scan(@NonNull NodeDependencies nodeDependencies) { // not used directly Class refClass = (Class) ref.getClassInfo().get(); var subTypes = getJsonSubTypes(refClass).map(JsonSubTypes.Type::value) - .map(ClassInfoModel::of).> map(EntityNode::of); + .map(ClassInfoModel::of).> map(EntityNode::of); // create a union node for classes annotated with @JsonTypeInfo if (refClass.getAnnotationsByType(JsonTypeInfo.class).length > 0) { @@ -131,9 +142,24 @@ public NodeDependencies scan(@NonNull NodeDependencies nodeDependencies) { private static Stream getJsonSubTypes(Class cls) { return Optional.of(cls) - .map(c -> c.getAnnotationsByType(JsonSubTypes.class)) - .filter(a -> a.length > 0).map(a -> a[0]) - .map(JsonSubTypes::value).stream().flatMap(Arrays::stream); + .map(c -> c.getAnnotationsByType(JsonSubTypes.class)) + .filter(a -> a.length > 0).map(a -> a[0]).map(JsonSubTypes::value) + .stream().flatMap(Arrays::stream); + } + + private static Optional> getJsonSubTypeInHierarchy( + Class cls) { + Class current = cls; + while (current != null) { + JsonTypeInfo typeInfo = current.getAnnotation(JsonTypeInfo.class); + JsonSubTypes types = current.getAnnotation(JsonSubTypes.class); + if (typeInfo != null && types != null) { + return Optional.of(Pair.of(typeInfo, types.value())); + } + current = current.getSuperclass(); + } + + return Optional.empty(); } /** @@ -141,9 +167,9 @@ private static Stream getJsonSubTypes(Class cls) { * class annotated with {@code @JsonSubTypes}. */ public static class UnionNode - extends AbstractNode> { + extends AbstractNode> { private UnionNode(@NonNull ClassInfoModel source, - @NonNull ObjectSchema target) { + @NonNull ObjectSchema target) { super(source, target); } diff --git a/packages/java/parser-jvm-plugin-subtypes/src/main/java/module-info.java b/packages/java/parser-jvm-plugin-subtypes/src/main/java/module-info.java index f46af1dd80..3a2ff2109d 100644 --- a/packages/java/parser-jvm-plugin-subtypes/src/main/java/module-info.java +++ b/packages/java/parser-jvm-plugin-subtypes/src/main/java/module-info.java @@ -7,6 +7,7 @@ requires io.swagger.v3.oas.models; requires io.github.classgraph; requires com.vaadin.hilla.parser.core; + requires org.apache.commons.lang3; exports com.vaadin.hilla.parser.plugins.subtypes; } diff --git a/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/AdvancedAddEvent.java b/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/AdvancedAddEvent.java new file mode 100644 index 0000000000..6586601c6a --- /dev/null +++ b/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/AdvancedAddEvent.java @@ -0,0 +1,5 @@ +package com.vaadin.hilla.parser.plugins.subtypes; + +public class AdvancedAddEvent extends AddEvent { + private Boolean defer; +} diff --git a/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/BaseEvent.java b/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/BaseEvent.java index d6cc95affa..07bcb45921 100644 --- a/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/BaseEvent.java +++ b/packages/java/parser-jvm-plugin-subtypes/src/test/java/com/vaadin/hilla/parser/plugins/subtypes/BaseEvent.java @@ -3,10 +3,12 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY) -@JsonSubTypes({ @JsonSubTypes.Type(value = AddEvent.class, name = "add"), - @JsonSubTypes.Type(value = UpdateEvent.class, name = "update"), - @JsonSubTypes.Type(value = DeleteEvent.class, name = "delete") }) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "eventType") +@JsonSubTypes({ @JsonSubTypes.Type(value = BaseEvent.class, name = "base"), + @JsonSubTypes.Type(value = AddEvent.class, name = "add"), + @JsonSubTypes.Type(value = UpdateEvent.class, name = "update"), + @JsonSubTypes.Type(value = DeleteEvent.class, name = "delete"), + @JsonSubTypes.Type(value = AdvancedAddEvent.class, name = "advanced-add") }) public class BaseEvent { public int id; } diff --git a/packages/java/parser-jvm-plugin-subtypes/src/test/resources/com/vaadin/hilla/parser/plugins/subtypes/openapi.json b/packages/java/parser-jvm-plugin-subtypes/src/test/resources/com/vaadin/hilla/parser/plugins/subtypes/openapi.json index 9fd3319b72..ab7996bf70 100644 --- a/packages/java/parser-jvm-plugin-subtypes/src/test/resources/com/vaadin/hilla/parser/plugins/subtypes/openapi.json +++ b/packages/java/parser-jvm-plugin-subtypes/src/test/resources/com/vaadin/hilla/parser/plugins/subtypes/openapi.json @@ -1,39 +1,39 @@ { - "openapi": "3.0.1", - "info": { - "title": "Hilla Application", - "version": "1.0.0" + "openapi" : "3.0.1", + "info" : { + "title" : "Hilla Application", + "version" : "1.0.0" }, - "servers": [ + "servers" : [ { - "url": "http://localhost:8080/connect", - "description": "Hilla Backend" + "url" : "http://localhost:8080/connect", + "description" : "Hilla Backend" } ], - "tags": [ + "tags" : [ { - "name": "SubTypesEndpoint", - "x-class-name": "com.vaadin.hilla.parser.plugins.subtypes.SubTypesEndpoint" + "name" : "SubTypesEndpoint", + "x-class-name" : "com.vaadin.hilla.parser.plugins.subtypes.SubTypesEndpoint" } ], - "paths": { - "/SubTypesEndpoint/receiveEvent": { - "post": { - "tags": [ + "paths" : { + "/SubTypesEndpoint/receiveEvent" : { + "post" : { + "tags" : [ "SubTypesEndpoint" ], - "operationId": "SubTypesEndpoint_receiveEvent_POST", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "event": { - "nullable": true, - "anyOf": [ + "operationId" : "SubTypesEndpoint_receiveEvent_POST", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "object", + "properties" : { + "event" : { + "nullable" : true, + "anyOf" : [ { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" } ] } @@ -42,29 +42,29 @@ } } }, - "responses": { - "200": { - "description": "" + "responses" : { + "200" : { + "description" : "" } } } }, - "/SubTypesEndpoint/sendEvent": { - "post": { - "tags": [ + "/SubTypesEndpoint/sendEvent" : { + "post" : { + "tags" : [ "SubTypesEndpoint" ], - "operationId": "SubTypesEndpoint_sendEvent_POST", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "nullable": true, - "anyOf": [ + "operationId" : "SubTypesEndpoint_sendEvent_POST", + "responses" : { + "200" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "nullable" : true, + "anyOf" : [ { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" } ] } @@ -75,93 +75,119 @@ } } }, - "components": { - "schemas": { - "com.vaadin.hilla.parser.plugins.subtypes.BaseEvent": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" + "components" : { + "schemas" : { + "com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int32" + }, + "eventType" : { + "type" : "string", + "example" : "base" } } }, - "com.vaadin.hilla.parser.plugins.subtypes.BaseEventUnion": { - "type": "object", - "oneOf": [ + "com.vaadin.hilla.parser.plugins.subtypes.BaseEventUnion" : { + "type" : "object", + "oneOf" : [ + { + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + }, { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.AddEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.AddEvent" }, { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.UpdateEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.UpdateEvent" }, { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.DeleteEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.DeleteEvent" + }, + { + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.AdvancedAddEvent" } ] }, - "com.vaadin.hilla.parser.plugins.subtypes.AddEvent": { - "anyOf": [ + "com.vaadin.hilla.parser.plugins.subtypes.AddEvent" : { + "anyOf" : [ { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" }, { - "type": "object", - "properties": { - "item": { - "type": "string", - "nullable": true + "type" : "object", + "properties" : { + "item" : { + "type" : "string", + "nullable" : true }, - "@type": { - "type": "string", - "example": "add" + "eventType" : { + "type" : "string", + "example" : "add" } } } ] }, - "com.vaadin.hilla.parser.plugins.subtypes.UpdateEvent": { - "anyOf": [ + "com.vaadin.hilla.parser.plugins.subtypes.UpdateEvent" : { + "anyOf" : [ { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" }, { - "type": "object", - "properties": { - "oldItem": { - "type": "string", - "nullable": true + "type" : "object", + "properties" : { + "oldItem" : { + "type" : "string", + "nullable" : true }, - "newItem": { - "type": "string", - "nullable": true + "newItem" : { + "type" : "string", + "nullable" : true }, - "@type": { - "type": "string", - "example": "update" + "eventType" : { + "type" : "string", + "example" : "update" } } } ] }, - "com.vaadin.hilla.parser.plugins.subtypes.DeleteEvent": { - "anyOf": [ + "com.vaadin.hilla.parser.plugins.subtypes.DeleteEvent" : { + "anyOf" : [ { - "$ref": "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.BaseEvent" }, { - "type": "object", - "properties": { - "item": { - "type": "string", - "nullable": true + "type" : "object", + "properties" : { + "item" : { + "type" : "string", + "nullable" : true }, - "force": { - "type": "boolean" + "force" : { + "type" : "boolean" }, - "@type": { - "type": "string", - "example": "delete" + "eventType" : { + "type" : "string", + "example" : "delete" + } + } + } + ] + }, + "com.vaadin.hilla.parser.plugins.subtypes.AdvancedAddEvent" : { + "anyOf" : [ + { + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.subtypes.AddEvent" + }, + { + "type" : "object", + "properties" : { + "eventType" : { + "type" : "string", + "example" : "advanced-add" } } }