Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang3 is not a direct dependency in Hilla. If possible, avoid using it.

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<PluginConfiguration> {
@Override
Expand All @@ -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<Object>() {
{
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
Expand All @@ -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);
}

});
});
}
}

Expand All @@ -100,16 +113,14 @@ public Collection<Class<? extends Plugin>> 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;
}
Expand All @@ -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).<Node<?, ?>> map(EntityNode::of);
.map(ClassInfoModel::of).<Node<?, ?>> map(EntityNode::of);

// create a union node for classes annotated with @JsonTypeInfo
if (refClass.getAnnotationsByType(JsonTypeInfo.class).length > 0) {
Expand All @@ -131,19 +142,34 @@ public NodeDependencies scan(@NonNull NodeDependencies nodeDependencies) {

private static Stream<JsonSubTypes.Type> 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<Pair<JsonTypeInfo, JsonSubTypes.Type[]>> getJsonSubTypeInHierarchy(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use a record for better readability and to avoid using Pair.

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();
}

/**
* A node that represents the union of all the mentioned subclasses of a
* class annotated with {@code @JsonSubTypes}.
*/
public static class UnionNode
extends AbstractNode<ClassInfoModel, Schema<?>> {
extends AbstractNode<ClassInfoModel, Schema<?>> {
private UnionNode(@NonNull ClassInfoModel source,
@NonNull ObjectSchema target) {
@NonNull ObjectSchema target) {
super(source, target);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.vaadin.hilla.parser.plugins.subtypes;

public class AdvancedAddEvent extends AddEvent {
private Boolean defer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private fields are not generated, so this field is probably useless in the test. You could make it public to match the other examples.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading
Loading