From a52b4af87670b87895e6a5580ac32717f2d67fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Habarta?= Date: Sun, 29 Nov 2020 10:40:19 +0100 Subject: [PATCH] Tagged unions generated for EXISTING_PROPERTY discriminant (#582) --- typescript-generator-core/pom.xml | 6 ++ .../generator/parser/Jackson2Parser.java | 30 ++++++-- .../generator/JaxrsApplicationTest.java | 3 +- .../generator/TaggedUnionsTest.java | 76 +++++++++++++++++++ 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/typescript-generator-core/pom.xml b/typescript-generator-core/pom.xml index 06f12995d..6fc194891 100644 --- a/typescript-generator-core/pom.xml +++ b/typescript-generator-core/pom.xml @@ -137,6 +137,12 @@ ${jersey.version} test + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + test + org.glassfish.jersey.inject jersey-hk2 diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java index a04b7f2cb..864e5840e 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java @@ -283,6 +283,7 @@ private BeanModel parseBean(SourceType> sourceClass, List class } final String discriminantProperty; + final boolean syntheticDiscriminantProperty; final String discriminantLiteral; final JsonTypeInfo jsonTypeInfo = sourceClass.type.getAnnotation(JsonTypeInfo.class); @@ -290,22 +291,35 @@ private BeanModel parseBean(SourceType> sourceClass, List class if (isSupported(jsonTypeInfo)) { // this is parent discriminantProperty = getDiscriminantPropertyName(jsonTypeInfo); + syntheticDiscriminantProperty = isDiscriminantPropertySynthetic(jsonTypeInfo); discriminantLiteral = isInterfaceOrAbstract(sourceClass.type) ? null : getTypeName(jsonTypeInfo, sourceClass.type); } else if (isSupported(parentJsonTypeInfo = getAnnotationRecursive(sourceClass.type, JsonTypeInfo.class))) { // this is child class discriminantProperty = getDiscriminantPropertyName(parentJsonTypeInfo); + syntheticDiscriminantProperty = isDiscriminantPropertySynthetic(parentJsonTypeInfo); discriminantLiteral = getTypeName(parentJsonTypeInfo, sourceClass.type); } else { // not part of explicit hierarchy discriminantProperty = null; + syntheticDiscriminantProperty = false; discriminantLiteral = null; } - if (discriminantProperty != null && properties.stream().anyMatch(property -> Objects.equals(property.getName(), discriminantProperty))) { - TypeScriptGenerator.getLogger().warning(String.format( - "Class '%s' has duplicate property '%s'. " - + "For more information see 'https://github.com/vojtechhabarta/typescript-generator/issues/392'.", - sourceClass.type.getName(), discriminantProperty)); + if (discriminantProperty != null) { + final PropertyModel foundDiscriminantProperty = properties.stream() + .filter(property -> Objects.equals(property.getName(), discriminantProperty)) + .findFirst() + .orElse(null); + if (foundDiscriminantProperty != null) { + if (syntheticDiscriminantProperty) { + TypeScriptGenerator.getLogger().warning(String.format( + "Class '%s' has duplicate property '%s'. " + + "For more information see 'https://github.com/vojtechhabarta/typescript-generator/issues/392'.", + sourceClass.type.getName(), discriminantProperty)); + } else { + properties.remove(foundDiscriminantProperty); + } + } } final List> taggedUnionClasses; @@ -387,10 +401,14 @@ private Type processIdentity(Type propertyType, BeanProperty beanProperty) { private static boolean isSupported(JsonTypeInfo jsonTypeInfo) { return jsonTypeInfo != null && - jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY && + (jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY || jsonTypeInfo.include() == JsonTypeInfo.As.EXISTING_PROPERTY) && (jsonTypeInfo.use() == JsonTypeInfo.Id.NAME || jsonTypeInfo.use() == JsonTypeInfo.Id.CLASS); } + private boolean isDiscriminantPropertySynthetic(JsonTypeInfo jsonTypeInfo) { + return jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY; + } + private String getDiscriminantPropertyName(JsonTypeInfo jsonTypeInfo) { return jsonTypeInfo.property().isEmpty() ? jsonTypeInfo.use().getDefaultPropertyName() diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java index c517eeb14..da26b303f 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java @@ -53,6 +53,7 @@ import javax.xml.bind.JAXBElement; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; +import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -691,7 +692,7 @@ public static interface AccountResource extends AbstractCrudResource = cz.habarta.typescript.generator.TaggedUnionsTest.Foo | cz.habarta.typescript.generator.TaggedUnionsTest.Bar")); } + @Test + public void testTaggedUnionsWithExistingProperty() { + final Settings settings = TestUtils.settings(); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Geometry2.class)); + final String expected = ( + "\n" + + "interface Geometry2 {\n" + + " shapes: Shape2Union[];\n" + + "}\n" + + "\n" + + "interface Shape2 {\n" + + " kind: 'square' | 'rectangle' | 'circle';\n" + + "}\n" + + "\n" + + "interface Square2 extends Shape2 {\n" + + " kind: 'square';\n" + + " size: number;\n" + + "}\n" + + "\n" + + "interface Rectangle2 extends Shape2 {\n" + + " kind: 'rectangle';\n" + + " width: number;\n" + + " height: number;\n" + + "}\n" + + "\n" + + "interface Circle2 extends Shape2 {\n" + + " kind: 'circle';\n" + + " radius: number;\n" + + "}\n" + + "\n" + + "type Shape2Union = Square2 | Rectangle2 | Circle2;\n" + + "" + ).replace('\'', '"'); + Assert.assertEquals(expected, output); + } + + private static class Geometry2 { + public List shapes; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind") + @JsonSubTypes({ + @JsonSubTypes.Type(Square2.class), + @JsonSubTypes.Type(Rectangle2.class), + @JsonSubTypes.Type(Circle2.class), + }) + private abstract static class Shape2 { + @JsonProperty("kind") + private final String kind; + + public Shape2() { + final JsonTypeName annotation = getClass().getAnnotation(JsonTypeName.class); + if (annotation == null) { + throw new RuntimeException("Annotation @JsonTypeName not specified on " + getClass()); + } + this.kind = annotation.value(); + } + } + + @JsonTypeName("square") + private static class Square2 extends Shape2 { + public double size; + } + + @JsonTypeName("rectangle") + private static class Rectangle2 extends Shape2 { + public double width; + public double height; + } + + @JsonTypeName("circle") + private static class Circle2 extends Shape2 { + public double radius; + } + }