From dcfac8e5555084aba81676cd75ea869c8ce69a80 Mon Sep 17 00:00:00 2001 From: byte-talented-hubs Date: Mon, 6 May 2024 15:00:15 +0200 Subject: [PATCH] feat(generator): support `@JsonCreator` and `@JsonValue` annotations (#2381) * First implementation, throws if issues with annotations * Cache results * Simplify cache * Fix comment * Rename exception * Change import style * Fix test name * Simplify annotation search --------- Co-authored-by: Anton Platonov Co-authored-by: Soroosh Taefi --- .../plugins/backbone/BackbonePlugin.java | 6 +- .../plugins/backbone/JsonValuePlugin.java | 104 ++++++++++++ .../plugins/backbone/jsonvalue/Endpoint.java | 11 ++ .../backbone/jsonvalue/JsonValueEndpoint.java | 61 ++++++++ .../backbone/jsonvalue/JsonValueTest.java | 25 +++ .../jsonvaluenojsoncreator/Endpoint.java | 11 ++ .../JsonValueNoJsonCreatorEndpoint.java | 30 ++++ .../JsonValueNoJsonCreatorTest.java | 25 +++ .../plugins/backbone/jsonvalue/openapi.json | 148 ++++++++++++++++++ 9 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/JsonValuePlugin.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/Endpoint.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueEndpoint.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueTest.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/Endpoint.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorEndpoint.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorTest.java create mode 100644 packages/java/parser-jvm-plugin-backbone/src/test/resources/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/openapi.json diff --git a/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/BackbonePlugin.java b/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/BackbonePlugin.java index 79565186..5eacbe5d 100644 --- a/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/BackbonePlugin.java +++ b/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/BackbonePlugin.java @@ -6,8 +6,8 @@ public final class BackbonePlugin extends AbstractCompositePlugin { public BackbonePlugin() { super(new EndpointPlugin(), new EndpointExposedPlugin(), - new MethodPlugin(), new MethodParameterPlugin(), - new EntityPlugin(), new PropertyPlugin(), - new TypeSignaturePlugin()); + new JsonValuePlugin(), new MethodPlugin(), + new MethodParameterPlugin(), new EntityPlugin(), + new PropertyPlugin(), new TypeSignaturePlugin()); } } diff --git a/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/JsonValuePlugin.java b/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/JsonValuePlugin.java new file mode 100644 index 00000000..b65be6fb --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/main/java/com/vaadin/hilla/parser/plugins/backbone/JsonValuePlugin.java @@ -0,0 +1,104 @@ +package com.vaadin.hilla.parser.plugins.backbone; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.vaadin.hilla.parser.core.AbstractPlugin; +import com.vaadin.hilla.parser.core.Node; +import com.vaadin.hilla.parser.core.NodeDependencies; +import com.vaadin.hilla.parser.core.NodePath; +import com.vaadin.hilla.parser.models.*; +import com.vaadin.hilla.parser.plugins.backbone.nodes.TypeSignatureNode; +import jakarta.annotation.Nonnull; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Adds support for Jackson's {@code JsonValue} and {@code JsonCreator} + * annotations. + */ +public class JsonValuePlugin + extends AbstractPlugin { + private final Map, Optional>> jsonValues = new HashMap<>(); + + @Override + public void enter(NodePath nodePath) { + } + + @Override + public void exit(NodePath nodePath) { + } + + @Nonnull + @Override + public NodeDependencies scan(@Nonnull NodeDependencies nodeDependencies) { + return nodeDependencies; + } + + @Nonnull + @Override + public Node resolve(@Nonnull Node node, + @Nonnull NodePath parentPath) { + if (node instanceof TypeSignatureNode typeSignatureNode) { + if (typeSignatureNode + .getSource() instanceof ClassRefSignatureModel classRefSignatureModel) { + var cls = (Class) classRefSignatureModel.getClassInfo() + .get(); + // Check if the class has the annotations which qualify for a + // value type. If so, replace the type with the corresponding + // value type. + Optional valueNode = getValueType(cls) + .map(SignatureModel::of).map(TypeSignatureNode::of); + return valueNode.orElse(typeSignatureNode); + } + } + + return node; + } + + private Optional> getValueType(Class cls) { + // Use cached results to avoid recomputing the value type. + return jsonValues.computeIfAbsent(cls, this::findValueType); + } + + private Optional> findValueType(Class cls) { + // First of all, we check that the `@JsonValue` annotation is + // used on a method of the class. + var jsonValue = Arrays.stream(cls.getMethods()) + .filter(method -> method.isAnnotationPresent(JsonValue.class)) + .map(Method::getReturnType).findAny(); + + // Then we check that the class has a `@JsonCreator` annotation + // on a method or on a constructor. This is a basic check, we + // could also check that they use the same type. + var jsonCreator = Stream + .concat(Arrays.stream(cls.getMethods()), + Arrays.stream(cls.getConstructors())) + .filter(executable -> executable + .isAnnotationPresent(JsonCreator.class)) + .findAny(); + + // Classes having only one of those annotation are malformed in Hilla as + // they break the generator or, at least, make data transfer impossible, + // so we throw an exception for those. + if (jsonValue.isPresent() ^ jsonCreator.isPresent()) { + throw new MalformedValueTypeException("Class " + cls.getName() + + " has only one of @JsonValue and @JsonCreator." + + " Hilla only supports classes with both annotations."); + } + + return jsonValue; + } + + // this shouldn't be a runtime exception, but `resolve` doesn't allow + // checked exceptions + public static class MalformedValueTypeException extends RuntimeException { + public MalformedValueTypeException(String message) { + super(message); + } + } +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/Endpoint.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/Endpoint.java new file mode 100644 index 00000000..894b22a1 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/Endpoint.java @@ -0,0 +1,11 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvalue; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Endpoint { +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueEndpoint.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueEndpoint.java new file mode 100644 index 00000000..21362b34 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueEndpoint.java @@ -0,0 +1,61 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvalue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +@Endpoint +public class JsonValueEndpoint { + public record Name(String firstName, String lastName) { + @JsonCreator + public Name(String fullName) { + this(fullName.split(" ")[0], fullName.split(" ")[1]); + } + + @JsonValue + public String fullName() { + return firstName + " " + lastName; + } + } + + public static class Email { + private String value; + + @JsonCreator + public Email(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public record PhoneNumber(@JsonValue int value) { + @JsonCreator + public PhoneNumber { + } + } + + public record Person(Name name, Email email, PhoneNumber phoneNumber) { + } + + public Person getPerson() { + return new Person(new Name("John", "Doe"), + new Email("john.doe@example.com"), new PhoneNumber(1234567890)); + } + + public void setPerson(Person person) { + } + + public Email getEmail() { + return new Email("john.doe@example.com"); + } + + public void setEmail(Email email) { + } +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueTest.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueTest.java new file mode 100644 index 00000000..f7151846 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/JsonValueTest.java @@ -0,0 +1,25 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvalue; + +import com.vaadin.hilla.parser.core.Parser; +import com.vaadin.hilla.parser.plugins.backbone.BackbonePlugin; +import com.vaadin.hilla.parser.plugins.backbone.test.helpers.TestHelper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Set; + +public class JsonValueTest { + private final TestHelper helper = new TestHelper(getClass()); + + @Test + public void should_CorrectlyMapJsonValue() + throws IOException, URISyntaxException { + var openAPI = new Parser().classLoader(getClass().getClassLoader()) + .classPath(Set.of(helper.getTargetDir().toString())) + .endpointAnnotation(Endpoint.class.getName()) + .addPlugin(new BackbonePlugin()).execute(); + + helper.executeParserWithConfig(openAPI); + } +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/Endpoint.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/Endpoint.java new file mode 100644 index 00000000..45cc8a95 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/Endpoint.java @@ -0,0 +1,11 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvaluenojsoncreator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Endpoint { +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorEndpoint.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorEndpoint.java new file mode 100644 index 00000000..4b5b2393 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorEndpoint.java @@ -0,0 +1,30 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvaluenojsoncreator; + +import com.fasterxml.jackson.annotation.JsonValue; + +@Endpoint +public class JsonValueNoJsonCreatorEndpoint { + public static class Email { + private String value; + + public Email(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public Email getEmail() { + return new Email("john.doe@example.com"); + } + + public void setEmail(Email email) { + } +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorTest.java b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorTest.java new file mode 100644 index 00000000..f66b0c80 --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/java/com/vaadin/hilla/parser/plugins/backbone/jsonvaluenojsoncreator/JsonValueNoJsonCreatorTest.java @@ -0,0 +1,25 @@ +package com.vaadin.hilla.parser.plugins.backbone.jsonvaluenojsoncreator; + +import com.vaadin.hilla.parser.core.Parser; +import com.vaadin.hilla.parser.plugins.backbone.BackbonePlugin; +import com.vaadin.hilla.parser.plugins.backbone.JsonValuePlugin.MalformedValueTypeException; +import com.vaadin.hilla.parser.plugins.backbone.test.helpers.TestHelper; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class JsonValueNoJsonCreatorTest { + private final TestHelper helper = new TestHelper(getClass()); + + @Test + public void should_ThrowExceptionWhenOnlyJsonValueIsUsed() { + assertThrows(MalformedValueTypeException.class, () -> { + new Parser().classLoader(getClass().getClassLoader()) + .classPath(Set.of(helper.getTargetDir().toString())) + .endpointAnnotation(Endpoint.class.getName()) + .addPlugin(new BackbonePlugin()).execute(); + }); + } +} diff --git a/packages/java/parser-jvm-plugin-backbone/src/test/resources/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/openapi.json b/packages/java/parser-jvm-plugin-backbone/src/test/resources/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/openapi.json new file mode 100644 index 00000000..3d5244fe --- /dev/null +++ b/packages/java/parser-jvm-plugin-backbone/src/test/resources/com/vaadin/hilla/parser/plugins/backbone/jsonvalue/openapi.json @@ -0,0 +1,148 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "Hilla Application", + "version" : "1.0.0" + }, + "servers" : [ + { + "url" : "http://localhost:8080/connect", + "description" : "Hilla Backend" + } + ], + "tags" : [ + { + "name" : "JsonValueEndpoint", + "x-class-name" : "com.vaadin.hilla.parser.plugins.backbone.jsonvalue.JsonValueEndpoint" + } + ], + "paths" : { + "/JsonValueEndpoint/getEmail" : { + "post" : { + "tags" : [ + "JsonValueEndpoint" + ], + "operationId" : "JsonValueEndpoint_getEmail_POST", + "responses" : { + "200" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "type" : "string", + "nullable" : true + } + } + } + } + } + } + }, + "/JsonValueEndpoint/getPerson" : { + "post" : { + "tags" : [ + "JsonValueEndpoint" + ], + "operationId" : "JsonValueEndpoint_getPerson_POST", + "responses" : { + "200" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "nullable" : true, + "anyOf" : [ + { + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.backbone.jsonvalue.JsonValueEndpoint$Person" + } + ] + } + } + } + } + } + } + }, + "/JsonValueEndpoint/setEmail" : { + "post" : { + "tags" : [ + "JsonValueEndpoint" + ], + "operationId" : "JsonValueEndpoint_setEmail_POST", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "object", + "properties" : { + "email" : { + "type" : "string", + "nullable" : true + } + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "" + } + } + } + }, + "/JsonValueEndpoint/setPerson" : { + "post" : { + "tags" : [ + "JsonValueEndpoint" + ], + "operationId" : "JsonValueEndpoint_setPerson_POST", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "object", + "properties" : { + "person" : { + "nullable" : true, + "anyOf" : [ + { + "$ref" : "#/components/schemas/com.vaadin.hilla.parser.plugins.backbone.jsonvalue.JsonValueEndpoint$Person" + } + ] + } + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "" + } + } + } + } + }, + "components" : { + "schemas" : { + "com.vaadin.hilla.parser.plugins.backbone.jsonvalue.JsonValueEndpoint$Person" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : true + }, + "email" : { + "type" : "string", + "nullable" : true + }, + "phoneNumber" : { + "type" : "integer", + "format" : "int32" + } + } + } + } + } +}