Skip to content

Commit

Permalink
feat(generator): support @JsonCreator and @JsonValue annotations …
Browse files Browse the repository at this point in the history
…(#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 <[email protected]>
Co-authored-by: Soroosh Taefi <[email protected]>
  • Loading branch information
3 people committed May 6, 2024
1 parent f0388da commit dcfac8e
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ public final class BackbonePlugin
extends AbstractCompositePlugin<BackbonePluginConfiguration> {
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());
}
}
Original file line number Diff line number Diff line change
@@ -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<BackbonePluginConfiguration> {
private final Map<Class<?>, Optional<Class<?>>> 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<TypeSignatureNode> valueNode = getValueType(cls)
.map(SignatureModel::of).map(TypeSignatureNode::of);
return valueNode.orElse(typeSignatureNode);
}
}

return node;
}

private Optional<Class<?>> getValueType(Class<?> cls) {
// Use cached results to avoid recomputing the value type.
return jsonValues.computeIfAbsent(cls, this::findValueType);
}

private Optional<Class<?>> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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("[email protected]"), new PhoneNumber(1234567890));
}

public void setPerson(Person person) {
}

public Email getEmail() {
return new Email("[email protected]");
}

public void setEmail(Email email) {
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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("[email protected]");
}

public void setEmail(Email email) {
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
Loading

0 comments on commit dcfac8e

Please sign in to comment.