diff --git a/.circleci/config.yml b/.circleci/config.yml index 047f61578..156f0c1df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2.1 jobs: check: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: large environment: CIRCLE_TEST_REPORTS: /home/circleci/junit @@ -58,7 +58,7 @@ jobs: - store_artifacts: { path: ~/artifacts } trial-publish: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: medium environment: CIRCLE_TEST_REPORTS: /home/circleci/junit @@ -104,7 +104,7 @@ jobs: - store_artifacts: { path: ~/artifacts } publish: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: medium environment: CIRCLE_TEST_REPORTS: /home/circleci/junit diff --git a/.circleci/template.sh b/.circleci/template.sh index e80105b40..927020ff1 100644 --- a/.circleci/template.sh +++ b/.circleci/template.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash export CIRCLECI_TEMPLATE=java-library-oss -export JDK=11 +export JDK=17 diff --git a/build.gradle b/build.gradle index 16e5ea3ca..9243dcb28 100644 --- a/build.gradle +++ b/build.gradle @@ -39,9 +39,10 @@ apply plugin: 'com.palantir.git-version' apply plugin: 'com.palantir.consistent-versions' apply plugin: 'com.palantir.baseline-java-versions' apply plugin: 'com.palantir.jdks.latest' +apply plugin: 'com.palantir.baseline-enable-preview-flag' javaVersions { - libraryTarget = 11 + libraryTarget = 17 runtime = 17 } @@ -52,6 +53,7 @@ allprojects { repositories { mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } } + maven { url 'https://jitpack.io' } } configurations.all { @@ -67,6 +69,17 @@ allprojects { } } } + + configurations.configureEach { + resolutionStrategy.dependencySubstitution { + // javapoet hasn't merged contributions in a while. Specifically, I want: + // - records https://github.com/square/javapoet/pull/840 + // - permits https://github.com/square/javapoet/issues/823 + // It appears that @liach has implemented both of these, and merged them to the javapoet fork https://github.com/FabricMC/javapoet. + // I'm using jitpack.io to compile a jar of the specific commit (from master) of that fork. + it.substitute it.module('com.squareup:javapoet') with it.module('com.github.FabricMC:javapoet:d9b7dba158') + } + } } subprojects { diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java index 70331f5f9..cadef991b 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java @@ -136,6 +136,20 @@ default boolean unionsWithUnknownValues() { return false; } + /** Generates sealed interfaces for union types. */ + @Value.Default + default boolean sealedUnions() { + return false; + } + + /** + * If {@link #sealedUnions} is enabled, this controls whether visitors should still be generated (for back-compat). + */ + @Value.Default + default boolean sealedUnionVisitors() { + return false; + } + Optional packagePrefix(); Optional apiVersion(); diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java index 1467c8158..080cb704d 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java @@ -87,6 +87,7 @@ public final class UnionGenerator { // If the member type is not known, a String containing the name of the unknown type is used. private static final TypeName UNKNOWN_MEMBER_TYPE = ClassName.get(String.class); private static final TypeName UNKNOWN_VALUE_TYPE = ClassName.get(Object.class); + private static final String KNOWN_INTERFACE = "Known"; public static JavaFile generateUnionType( TypeMapper typeMapper, @@ -97,7 +98,6 @@ public static JavaFile generateUnionType( com.palantir.conjure.spec.TypeName prefixedTypeName = Packages.getPrefixedName(typeDef.getTypeName(), options.packagePrefix()); ClassName unionClass = ClassName.get(prefixedTypeName.getPackage(), prefixedTypeName.getName()); - ClassName baseClass = unionClass.nestedClass("Base"); ClassName visitorClass = unionClass.nestedClass("Visitor"); ClassName visitorBuilderClass = unionClass.nestedClass("VisitorBuilder"); Map memberTypes = typeDef.getUnion().stream() @@ -105,41 +105,113 @@ public static JavaFile generateUnionType( Function.identity(), entry -> ConjureAnnotations.withSafety( typeMapper.getClassName(entry.getType()), entry.getSafety()))); - List fields = - ImmutableList.of(FieldSpec.builder(baseClass, VALUE_FIELD_NAME, Modifier.PRIVATE, Modifier.FINAL) - .build()); - TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( - typeDef.getTypeName().getName()) - .addAnnotations(ConjureAnnotations.safety(safetyEvaluator.evaluate(TypeDefinition.union(typeDef)))) - .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(UnionGenerator.class)) - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addFields(fields) - .addMethod(generateConstructor(baseClass)) - .addMethod(generateGetValue(baseClass)) - .addMethods(generateStaticFactories(typeMapper, unionClass, typeDef.getUnion())) - .addMethod(generateAcceptVisitMethod(visitorClass)) - .addType(generateVisitor(unionClass, visitorClass, memberTypes, visitorBuilderClass, options)) - .addType(generateVisitorBuilder(unionClass, visitorClass, visitorBuilderClass, memberTypes, options)) - .addTypes(generateVisitorBuilderStageInterfaces(unionClass, visitorClass, memberTypes, options)) - .addType(generateBase(baseClass, visitorClass, memberTypes)) - .addTypes(generateWrapperClasses( - typeMapper, typesMap, baseClass, visitorClass, typeDef.getUnion(), options)) - .addType(generateUnknownWrapper(baseClass, visitorClass, options)) - .addMethod(generateEquals(unionClass)) - .addMethod(MethodSpecs.createEqualTo(unionClass, fields)) - .addMethod(MethodSpecs.createHashCode(fields)) - .addMethod(MethodSpecs.createToString( - unionClass.simpleName(), - fields.stream() - .map(fieldSpec -> FieldName.of(fieldSpec.name)) - .collect(Collectors.toList()))); + if (options.sealedUnions()) { + ClassName unknownWrapperClass = unionClass.nestedClass("Unknown"); + + Map records = + generateRecords(typeMapper, typesMap, unionClass, visitorClass, typeDef.getUnion(), options); + TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder( + typeDef.getTypeName().getName()) + .addAnnotations(ConjureAnnotations.safety(safetyEvaluator.evaluate(TypeDefinition.union(typeDef)))) + .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(UnionGenerator.class)) + .addAnnotation(jacksonJsonTypeInfo(unknownWrapperClass)) + .addAnnotation(generateJacksonSubtypeAnnotation(unionClass, memberTypes, options)) + .addAnnotation(jacksonIgnoreUnknownAnnotation()) + .addModifiers(Modifier.PUBLIC, Modifier.SEALED) + .addType(generateSealedKnownInterface(options, unionClass, records)) + .addMethods(generateStaticFactories(typeMapper, unionClass, typeDef.getUnion(), options)) + .addMethod(generateThrowOnUnknown(unionClass, unknownWrapperClass)) + .addTypes(records.values()) + .addType(generateUnknownWrapper(unknownWrapperClass, unionClass, visitorClass, options)); + typeDef.getDocs().ifPresent(docs -> typeBuilder.addJavadoc("$L", Javadoc.render(docs))); + + if (options.sealedUnionVisitors()) { + typeBuilder + .addMethod(generateAcceptVisitorMethodSignature(visitorClass)) + .addType(generateVisitor(unionClass, visitorClass, memberTypes, visitorBuilderClass, options)) + .addType(generateVisitorBuilder( + unionClass, visitorClass, visitorBuilderClass, memberTypes, options)) + .addTypes( + generateVisitorBuilderStageInterfaces(unionClass, visitorClass, memberTypes, options)); + } + + return JavaFile.builder(prefixedTypeName.getPackage(), typeBuilder.build()) + .skipJavaLangImports(true) + .indent(" ") + .build(); + } else { + ClassName baseClass = unionClass.nestedClass("Base"); + ClassName unknownWrapperClass = baseClass.peerClass(UNKNOWN_WRAPPER_CLASS_NAME); + List fields = + ImmutableList.of(FieldSpec.builder(baseClass, VALUE_FIELD_NAME, Modifier.PRIVATE, Modifier.FINAL) + .build()); + + TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( + typeDef.getTypeName().getName()) + .addAnnotations(ConjureAnnotations.safety(safetyEvaluator.evaluate(TypeDefinition.union(typeDef)))) + .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(UnionGenerator.class)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addFields(fields) + .addMethod(generateConstructor(baseClass)) + .addMethod(generateGetValue(baseClass)) + .addMethods(generateStaticFactories(typeMapper, unionClass, typeDef.getUnion(), options)) + .addMethod(generateAcceptVisitMethod(visitorClass)) + .addType(generateVisitor(unionClass, visitorClass, memberTypes, visitorBuilderClass, options)) + .addType( + generateVisitorBuilder(unionClass, visitorClass, visitorBuilderClass, memberTypes, options)) + .addTypes(generateVisitorBuilderStageInterfaces(unionClass, visitorClass, memberTypes, options)) + .addType(generateBase(baseClass, visitorClass, memberTypes, options)) + .addTypes(generateWrapperClasses( + typeMapper, typesMap, baseClass, visitorClass, typeDef.getUnion(), options)) + .addType(generateUnknownWrapper(unknownWrapperClass, baseClass, visitorClass, options)) + .addMethod(generateEquals(unionClass)) + .addMethod(MethodSpecs.createEqualTo(unionClass, fields)) + .addMethod(MethodSpecs.createHashCode(fields)) + .addMethod(MethodSpecs.createToString( + unionClass.simpleName(), + fields.stream() + .map(fieldSpec -> FieldName.of(fieldSpec.name)) + .collect(Collectors.toList()))); + + typeDef.getDocs().ifPresent(docs -> typeBuilder.addJavadoc("$L", Javadoc.render(docs))); + + return JavaFile.builder(prefixedTypeName.getPackage(), typeBuilder.build()) + .skipJavaLangImports(true) + .indent(" ") + .build(); + } + } - typeDef.getDocs().ifPresent(docs -> typeBuilder.addJavadoc("$L", Javadoc.render(docs))); + private static MethodSpec generateThrowOnUnknown(ClassName unionClass, ClassName unknownWrapperClass) { + ClassName knownInterface = unionClass.nestedClass(KNOWN_INTERFACE); + return MethodSpec.methodBuilder("throwOnUnknown") + .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) + .returns(knownInterface) + .addCode(CodeBlock.of( + "if (this instanceof $T) {\n" + + " throw new $T(\n" + + " \"Unknown variant of the 'Union' union\",\n" + + " $T.of(\"type\", (($T) this).getType()));\n" + + " } else {\n" + + " return ($T) this;\n" + + " }", + unknownWrapperClass, + // TODO(dfox): do we want to take this opporunity to make the IAE (http 400) customizable + SafeIllegalArgumentException.class, + SafeArg.class, + unknownWrapperClass, + knownInterface)) + .build(); + } - return JavaFile.builder(prefixedTypeName.getPackage(), typeBuilder.build()) - .skipJavaLangImports(true) - .indent(" ") + private static TypeSpec generateSealedKnownInterface( + Options options, ClassName unionClass, Map records) { + return TypeSpec.interfaceBuilder(KNOWN_INTERFACE) + .addModifiers(Modifier.PUBLIC, Modifier.SEALED, Modifier.STATIC) + .addPermittedSubclasses(records.keySet().stream() + .map(fieldName -> wrapperClass(unionClass, fieldName, options)) + .toList()) .build(); } @@ -163,7 +235,7 @@ private static MethodSpec generateGetValue(ClassName baseClass) { } private static List generateStaticFactories( - TypeMapper typeMapper, ClassName unionClass, List memberTypeDefs) { + TypeMapper typeMapper, ClassName unionClass, List memberTypeDefs, Options options) { List staticFactories = memberTypeDefs.stream() .map(memberTypeDef -> { FieldName memberName = sanitizeUnknown(memberTypeDef.getFieldName()); @@ -176,11 +248,17 @@ private static List generateStaticFactories( .addParameter(ParameterSpec.builder(memberType, variableName) .addAnnotations(ConjureAnnotations.safety(memberTypeDef.getSafety())) .build()) - .addStatement( - "return new $T(new $T($L))", - unionClass, - wrapperClass(unionClass, memberName), - variableName) + .addCode( + options.sealedUnions() + ? CodeBlock.of( + "return new $T($L);", + wrapperClass(unionClass, memberName, options), + variableName) + : CodeBlock.of( + "return new $T(new $T($L));", + unionClass, + wrapperClass(unionClass, memberName, options), + variableName)) .returns(unionClass); Javadoc.render(memberTypeDef.getDocs(), memberTypeDef.getDeprecated()) .ifPresent(javadoc -> builder.addJavadoc("$L", javadoc)); @@ -188,11 +266,12 @@ private static List generateStaticFactories( return builder.build(); }) .collect(Collectors.toList()); - staticFactories.add(generateUnknownStaticFactory(unionClass, memberTypeDefs)); + staticFactories.add(generateUnknownStaticFactory(unionClass, memberTypeDefs, options)); return staticFactories; } - private static MethodSpec generateUnknownStaticFactory(ClassName unionClass, List memberTypeDefs) { + private static MethodSpec generateUnknownStaticFactory( + ClassName unionClass, List memberTypeDefs, Options options) { String typeParam = "type"; String valueParam = "value"; MethodSpec.Builder builder = MethodSpec.methodBuilder("unknown") @@ -217,16 +296,36 @@ private static MethodSpec generateUnknownStaticFactory(ClassName unionClass, Lis }); // add default case, which actually builds the unknown builder.addCode("default:"); + CodeBlock singletonMap = CodeBlock.of("$T.singletonMap($N, $N)", Collections.class, typeParam, valueParam); + builder.addStatement( - "return new $T(new $T($N, $L))", - unionClass, - wrapperClass(unionClass, FieldName.of("unknown")), - typeParam, - CodeBlock.of("$T.singletonMap($N, $N)", Collections.class, typeParam, valueParam)); + options.sealedUnions() + ? CodeBlock.of( + "return new $T($N, $L)", + wrapperClass(unionClass, FieldName.of("unknown"), options), + typeParam, + singletonMap) + : CodeBlock.of( + "return new $T(new $T($N, $L))", + unionClass, + wrapperClass(unionClass, FieldName.of("unknown"), options), + typeParam, + singletonMap)); builder.endControlFlow(); return builder.build(); } + private static MethodSpec generateAcceptVisitorMethodSignature(ClassName visitorClass) { + ParameterizedTypeName parameterizedVisitorClass = ParameterizedTypeName.get(visitorClass, TYPE_VARIABLE); + ParameterSpec visitor = + ParameterSpec.builder(parameterizedVisitorClass, "visitor").build(); + return MethodSpec.methodBuilder("accept") + .addParameter(visitor) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addTypeVariable(TYPE_VARIABLE) + .build(); + } + private static MethodSpec generateAcceptVisitMethod(ClassName visitorClass) { ParameterizedTypeName parameterizedVisitorClass = ParameterizedTypeName.get(visitorClass, TYPE_VARIABLE); ParameterSpec visitor = @@ -260,38 +359,43 @@ private static TypeSpec generateVisitor( Map memberTypes, ClassName visitorBuilderClass, Options options) { - TypeSpec.Builder visitorBuilder = TypeSpec.interfaceBuilder(visitorClass) - .addModifiers(Modifier.PUBLIC) - .addTypeVariable(TYPE_VARIABLE) - .addMethods(generateMemberVisitMethods(memberTypes)); - MethodSpec.Builder visitUnknownBuilder = MethodSpec.methodBuilder(VISIT_UNKNOWN_METHOD_NAME) + + MethodSpec.Builder visitUnknownMethod = MethodSpec.methodBuilder(VISIT_UNKNOWN_METHOD_NAME) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addParameter(ParameterSpec.builder(UNKNOWN_MEMBER_TYPE, UNKNOWN_TYPE_PARAM_NAME) .addAnnotation(Safe.class) .build()) .returns(TYPE_VARIABLE); if (options.unionsWithUnknownValues()) { - visitUnknownBuilder.addParameter(ParameterSpec.builder(UNKNOWN_VALUE_TYPE, UNKNOWN_VALUE_PARAM_NAME) + visitUnknownMethod.addParameter(ParameterSpec.builder(UNKNOWN_VALUE_TYPE, UNKNOWN_VALUE_PARAM_NAME) .addAnnotations(ConjureAnnotations.safety(SafetyEvaluator.UNKNOWN_UNION_VARINT_SAFETY)) .build()); } - visitorBuilder.addMethod(visitUnknownBuilder.build()); - visitorBuilder - .addMethod(MethodSpec.methodBuilder("builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addTypeVariable(TYPE_VARIABLE) - .addStatement("return new $T<$T>()", visitorBuilderClass, TYPE_VARIABLE) - .returns(ParameterizedTypeName.get( - visitorStageInterfaceName( - unionClass, - sortedStageNameTypePairs(memberTypes) - .findFirst() - .get() - .memberName), - TYPE_VARIABLE)) - .build()) + + MethodSpec.Builder builderStaticFactoryMethod = MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addTypeVariable(TYPE_VARIABLE) + .addStatement("return new $T<$T>()", visitorBuilderClass, TYPE_VARIABLE) + .returns(ParameterizedTypeName.get( + visitorStageInterfaceName( + unionClass, + sortedStageNameTypePairs(memberTypes) + .findFirst() + .get() + .memberName), + TYPE_VARIABLE)); + if (options.sealedUnions()) { + builderStaticFactoryMethod.addAnnotation(Deprecated.class); + builderStaticFactoryMethod.addJavadoc("@Deprecated - prefer Java 17 pattern matching switch expressions."); + } + + return TypeSpec.interfaceBuilder(visitorClass) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addTypeVariable(TYPE_VARIABLE) + .addMethods(generateMemberVisitMethods(memberTypes)) + .addMethod(visitUnknownMethod.build()) + .addMethod(builderStaticFactoryMethod.build()) .build(); - return visitorBuilder.build(); } private static List generateMemberVisitMethods(Map memberTypes) { @@ -327,7 +431,8 @@ private static TypeSpec generateVisitorBuilder( Options options) { TypeVariableName visitResultType = TypeVariableName.get("T"); return TypeSpec.classBuilder(visitorBuilder) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addModifiers( + options.sealedUnions() ? Modifier.PUBLIC : Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) .addTypeVariable(visitResultType) .addSuperinterfaces(allVisitorBuilderStages(enclosingClass, memberTypeMap, visitResultType)) .addFields(allVisitorBuilderFields(memberTypeMap, visitResultType, options)) @@ -589,7 +694,7 @@ private static List generateVisitorBuilderStageInterfaces( ClassName nextStageClassName = visitorStageInterfaceName(enclosingClass, nextBuilderStageName); interfaces.add(TypeSpec.interfaceBuilder(visitorStageInterfaceName(enclosingClass, member.memberName)) .addTypeVariable(visitResultType) - .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addMethod(visitorBuilderSetterPrototype(member, visitResultType, nextStageClassName, options) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .build()) @@ -601,7 +706,7 @@ private static List generateVisitorBuilderStageInterfaces( } interfaces.add(TypeSpec.interfaceBuilder(visitorStageInterfaceName(enclosingClass, COMPLETED)) .addTypeVariable(visitResultType) - .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addMethod(MethodSpec.methodBuilder("build") .returns(ParameterizedTypeName.get(visitorClass, visitResultType)) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) @@ -675,35 +780,21 @@ private static MethodSpec.Builder visitorBuilderUnknownThrowPrototype( } private static TypeSpec generateBase( - ClassName baseClass, ClassName visitorClass, Map memberTypes) { - ClassName unknownWrapperClass = baseClass.peerClass(UNKNOWN_WRAPPER_CLASS_NAME); + ClassName baseClass, ClassName visitorClass, Map memberTypes, Options options) { + ClassName unknownWrapperClass; + FieldName memberTypeName = FieldName.of("unknown"); + if (options.sealedUnions()) { + unknownWrapperClass = baseClass.peerClass(StringUtils.capitalize(memberTypeName.get())); + } else { + unknownWrapperClass = baseClass.peerClass(StringUtils.capitalize(memberTypeName.get()) + "Wrapper"); + } TypeSpec.Builder baseBuilder = TypeSpec.interfaceBuilder(baseClass) .addModifiers(Modifier.PRIVATE) - .addAnnotation(AnnotationSpec.builder(JsonTypeInfo.class) - .addMember("use", "JsonTypeInfo.Id.NAME") - .addMember("include", "JsonTypeInfo.As.EXISTING_PROPERTY") - .addMember("property", "\"type\"") - .addMember("visible", "$L", true) - .addMember("defaultImpl", "$T.class", unknownWrapperClass) - .build()); + .addAnnotation(jacksonJsonTypeInfo(unknownWrapperClass)); if (!memberTypes.isEmpty()) { - List subAnnotations = memberTypes.entrySet().stream() - .map(entry -> AnnotationSpec.builder(JsonSubTypes.Type.class) - .addMember( - "value", - "$T.class", - peerWrapperClass( - baseClass, - sanitizeUnknown(entry.getKey().getFieldName()))) - .build()) - .collect(Collectors.toList()); - AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(JsonSubTypes.class); - subAnnotations.forEach(subAnnotation -> annotationBuilder.addMember("value", "$L", subAnnotation)); - baseBuilder.addAnnotation(annotationBuilder.build()); + baseBuilder.addAnnotation(generateJacksonSubtypeAnnotation(baseClass, memberTypes, options)); } - baseBuilder.addAnnotation(AnnotationSpec.builder(JsonIgnoreProperties.class) - .addMember("ignoreUnknown", "$L", true) - .build()); + baseBuilder.addAnnotation(jacksonIgnoreUnknownAnnotation()); ParameterizedTypeName parameterizedVisitorClass = ParameterizedTypeName.get(visitorClass, TYPE_VARIABLE); ParameterSpec visitor = ParameterSpec.builder(parameterizedVisitorClass, "visitor").build(); @@ -716,10 +807,130 @@ private static TypeSpec generateBase( return baseBuilder.build(); } + private static AnnotationSpec jacksonJsonTypeInfo(ClassName unknownWrapperClass) { + return AnnotationSpec.builder(JsonTypeInfo.class) + .addMember("use", "JsonTypeInfo.Id.NAME") + .addMember("include", "JsonTypeInfo.As.EXISTING_PROPERTY") + .addMember("property", "\"type\"") + .addMember("visible", "$L", true) + .addMember("defaultImpl", "$T.class", unknownWrapperClass) + .build(); + } + + private static AnnotationSpec jacksonIgnoreUnknownAnnotation() { + return AnnotationSpec.builder(JsonIgnoreProperties.class) + .addMember("ignoreUnknown", "$L", true) + .build(); + } + + private static AnnotationSpec generateJacksonSubtypeAnnotation( + ClassName baseClass, Map memberTypes, Options options) { + List subAnnotations = memberTypes.entrySet().stream() + .map(entry -> { + ClassName result; + FieldName memberTypeName = sanitizeUnknown(entry.getKey().getFieldName()); + if (options.sealedUnions()) { + result = baseClass.peerClass(StringUtils.capitalize(memberTypeName.get())); + } else { + result = baseClass.peerClass(StringUtils.capitalize(memberTypeName.get()) + "Wrapper"); + } + return AnnotationSpec.builder(JsonSubTypes.Type.class) + .addMember("value", "$T.class", result) + .build(); + }) + .collect(Collectors.toList()); + AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(JsonSubTypes.class); + subAnnotations.forEach(subAnnotation -> annotationBuilder.addMember("value", "$L", subAnnotation)); + return annotationBuilder.build(); + } + + private static Map generateRecords( + TypeMapper typeMapper, + Map typesMap, + ClassName superInterface, + ClassName visitorClass, + List memberTypeDefs, + Options options) { + return memberTypeDefs.stream() + .collect(StableCollectors.toLinkedMap( + memberTypeDef -> sanitizeUnknown(memberTypeDef.getFieldName()), memberTypeDef -> { + FieldName memberName = sanitizeUnknown(memberTypeDef.getFieldName()); + ClassName wrapperClass; + if (options.sealedUnions()) { + wrapperClass = superInterface.peerClass(StringUtils.capitalize(memberName.get())); + } else { + wrapperClass = + superInterface.peerClass(StringUtils.capitalize(memberName.get()) + "Wrapper"); + } + boolean isDeprecated = memberTypeDef.getDeprecated().isPresent(); + TypeName memberType = typeMapper.getClassName(memberTypeDef.getType()); + + TypeSpec.Builder typeBuilder = TypeSpec.recordBuilder(wrapperClass) + .addRecordComponent(ParameterSpec.builder(memberType, VALUE_FIELD_NAME) + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addSuperinterface(superInterface) + .addSuperinterface(superInterface.nestedClass(KNOWN_INTERFACE)) + .addAnnotation(AnnotationSpec.builder(JsonTypeName.class) + .addMember("value", "$S", memberTypeDef.getFieldName()) + .build()) + .addMethod(MethodSpec.constructorBuilder() + .addAnnotation(ConjureAnnotations.propertiesJsonCreator()) + .addParameter(ParameterSpec.builder(memberType, VALUE_FIELD_NAME) + .addAnnotation(wrapperConstructorParameterAnnotation( + memberTypeDef, typesMap)) + .addAnnotation(Nonnull.class) + .build()) + // TODO(dfox): is this null check strictly necessary? + .addStatement( + "$L", + Expressions.requireNonNull( + VALUE_FIELD_NAME, + String.format("%s cannot be null", memberName.get()))) + .addStatement("this.$1L = $1L", VALUE_FIELD_NAME) + .build()) + .addMethod(MethodSpec.methodBuilder("getType") + .addModifiers(Modifier.PRIVATE) + .addAnnotation(AnnotationSpec.builder(JsonProperty.class) + .addMember("value", "$S", "type") + .addMember("index", "$L", 0) + .build()) + .addStatement("return $S", memberTypeDef.getFieldName()) + .returns(String.class) + .build()) + .addMethod(MethodSpec.methodBuilder("getValue") + .addModifiers(Modifier.PRIVATE) + .addAnnotation(AnnotationSpec.builder(JsonProperty.class) + .addMember( + "value", + "$S", + memberTypeDef + .getFieldName() + .get()) + .build()) + .addStatement("return $L", VALUE_FIELD_NAME) + .returns(memberType) + .build()) + .addMethods( + !options.sealedUnions() || options.sealedUnionVisitors() + ? List.of(createWrapperAcceptMethod( + visitorClass, + visitMethodName(memberName.get()), + VALUE_FIELD_NAME, + isDeprecated, + options)) + : List.of()) + .addMethod(MethodSpecs.createToString( + wrapperClass.simpleName(), List.of(FieldName.of(VALUE_FIELD_NAME)))); + + return typeBuilder.build(); + })); + } + private static List generateWrapperClasses( TypeMapper typeMapper, Map typesMap, - ClassName baseClass, + ClassName superInterface, ClassName visitorClass, List memberTypeDefs, Options options) { @@ -728,15 +939,23 @@ private static List generateWrapperClasses( boolean isDeprecated = memberTypeDef.getDeprecated().isPresent(); FieldName memberName = sanitizeUnknown(memberTypeDef.getFieldName()); TypeName memberType = typeMapper.getClassName(memberTypeDef.getType()); - ClassName wrapperClass = peerWrapperClass(baseClass, memberName); + ClassName wrapperClass; + if (options.sealedUnions()) { + wrapperClass = superInterface.peerClass(StringUtils.capitalize(memberName.get())); + } else { + wrapperClass = superInterface.peerClass(StringUtils.capitalize(memberName.get()) + "Wrapper"); + } List fields = ImmutableList.of( FieldSpec.builder(memberType, VALUE_FIELD_NAME, Modifier.PRIVATE, Modifier.FINAL) .build()); TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(wrapperClass) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) - .addSuperinterface(baseClass) + .addModifiers( + options.sealedUnions() ? Modifier.PUBLIC : Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL) + .addSuperinterface(superInterface) .addAnnotation(AnnotationSpec.builder(JsonTypeName.class) .addMember("value", "$S", memberTypeDef.getFieldName()) .build()) @@ -776,12 +995,15 @@ private static List generateWrapperClasses( .addStatement("return $L", VALUE_FIELD_NAME) .returns(memberType) .build()) - .addMethod(createWrapperAcceptMethod( - visitorClass, - visitMethodName(memberName.get()), - VALUE_FIELD_NAME, - isDeprecated, - options)) + .addMethods( + !options.sealedUnions() || options.sealedUnionVisitors() + ? List.of(createWrapperAcceptMethod( + visitorClass, + visitMethodName(memberName.get()), + VALUE_FIELD_NAME, + isDeprecated, + options)) + : List.of()) .addMethod(MethodSpecs.createEquals(wrapperClass)) .addMethod(MethodSpecs.createEqualTo(wrapperClass, fields)) .addMethod(MethodSpecs.createHashCode(fields)) @@ -807,7 +1029,8 @@ private static AnnotationSpec wrapperConstructorParameterAnnotation( return builder.build(); } - private static TypeSpec generateUnknownWrapper(ClassName baseClass, ClassName visitorClass, Options options) { + private static TypeSpec generateUnknownWrapper( + ClassName unknownWrapperClass, ClassName superinterface, ClassName visitorClass, Options options) { ParameterizedTypeName genericMapType = ParameterizedTypeName.get(Map.class, String.class, Object.class); ParameterizedTypeName genericHashMapType = ParameterizedTypeName.get(HashMap.class, String.class, Object.class); ParameterSpec typeParameter = ParameterSpec.builder(String.class, "type") @@ -819,15 +1042,15 @@ private static TypeSpec generateUnknownWrapper(ClassName baseClass, ClassName vi .build()) .build(); - ClassName wrapperClass = baseClass.peerClass(UNKNOWN_WRAPPER_CLASS_NAME); List fields = ImmutableList.of( FieldSpec.builder(UNKNOWN_MEMBER_TYPE, "type", Modifier.PRIVATE, Modifier.FINAL) .build(), FieldSpec.builder(genericMapType, VALUE_FIELD_NAME, Modifier.PRIVATE, Modifier.FINAL) .build()); - TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(wrapperClass) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) - .addSuperinterface(baseClass) + TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(unknownWrapperClass) + .addModifiers( + options.sealedUnions() ? Modifier.PUBLIC : Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addSuperinterface(superinterface) .addFields(fields) .addMethod(MethodSpec.constructorBuilder() .addModifiers(Modifier.PRIVATE) @@ -871,13 +1094,16 @@ private static TypeSpec generateUnknownWrapper(ClassName baseClass, ClassName vi AnnotationSpec.builder(JsonAnySetter.class).build()) .addStatement("$L.put(key, val)", VALUE_FIELD_NAME) .build()) - .addMethod(createWrapperAcceptMethod( - visitorClass, VISIT_UNKNOWN_METHOD_NAME, typeParameter.name, false, options)) - .addMethod(MethodSpecs.createEquals(wrapperClass)) - .addMethod(MethodSpecs.createEqualTo(wrapperClass, fields)) + .addMethods( + !options.sealedUnions() || options.sealedUnionVisitors() + ? List.of(createWrapperAcceptMethod( + visitorClass, VISIT_UNKNOWN_METHOD_NAME, typeParameter.name, false, options)) + : List.of()) + .addMethod(MethodSpecs.createEquals(unknownWrapperClass)) + .addMethod(MethodSpecs.createEqualTo(unknownWrapperClass, fields)) .addMethod(MethodSpecs.createHashCode(fields)) .addMethod(MethodSpecs.createToString( - wrapperClass.simpleName(), + unknownWrapperClass.simpleName(), fields.stream() .map(fieldSpec -> FieldName.of(fieldSpec.name)) .collect(Collectors.toList()))); @@ -913,15 +1139,16 @@ private static MethodSpec createWrapperAcceptMethod( return methodBuilder.build(); } - private static ClassName wrapperClass(ClassName unionClass, FieldName memberTypeName) { - return ClassName.get( - unionClass.packageName(), - unionClass.simpleName(), - StringUtils.capitalize(memberTypeName.get()) + "Wrapper"); - } - - private static ClassName peerWrapperClass(ClassName peerClass, FieldName memberTypeName) { - return peerClass.peerClass(StringUtils.capitalize(memberTypeName.get()) + "Wrapper"); + private static ClassName wrapperClass(ClassName unionClass, FieldName memberTypeName, Options options) { + if (options.sealedUnions()) { + return ClassName.get( + unionClass.packageName(), unionClass.simpleName(), StringUtils.capitalize(memberTypeName.get())); + } else { + return ClassName.get( + unionClass.packageName(), + unionClass.simpleName(), + StringUtils.capitalize(memberTypeName.get()) + "Wrapper"); + } } private static String visitMethodName(String fieldName) { @@ -937,11 +1164,14 @@ private static String variableName() { } private static String sanitizeUnknown(String input) { - return "unknown".equalsIgnoreCase(input) ? input + '_' : input; + if ("unknown".equalsIgnoreCase(input) || "known".equalsIgnoreCase(input)) { + return input + '_'; + } + return input; } - private static FieldName sanitizeUnknown(FieldName input) { - return "unknown".equalsIgnoreCase(input.get()) ? FieldName.of(input.get() + '_') : input; + private static FieldName sanitizeUnknown(FieldName fieldName) { + return FieldName.of(sanitizeUnknown(fieldName.get())); } private UnionGenerator() {} diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java index c143a6f17..56c2c2152 100644 --- a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java @@ -71,6 +71,27 @@ public void testObjectGenerator_allExamples() throws IOException { assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); } + @Test + public void testSealedUnions() throws IOException { + ConjureDefinition def = + Conjure.parse(ImmutableList.of(new File("src/test/resources/example-sealed-unions.yml"))); + List files = new GenerationCoordinator( + MoreExecutors.directExecutor(), + ImmutableSet.of(new ObjectGenerator(Options.builder() + .useImmutableBytes(true) + .strictObjects(true) + .nonNullCollections(true) + .excludeEmptyOptionals(true) + .unionsWithUnknownValues(true) + .sealedUnions(true) + .sealedUnionVisitors(true) + .packagePrefix("withvisitors") + .build()))) + .emit(def, tempDir); + + assertThatFilesAreTheSame(files, "src/test/resources/sealedunions"); + } + @Test public void testObjectGenerator_byteBufferCompatibility() throws IOException { ConjureDefinition def = @@ -217,7 +238,7 @@ private void assertThatFilesAreTheSame(List files, String referenceFilesFo for (Path file : files) { Path relativized = tempDir.toPath().relativize(file); Path expectedFile = Paths.get(referenceFilesFolder, relativized.toString()); - if (Boolean.valueOf(System.getProperty("recreate", "false"))) { + if (!System.getenv().containsKey("CI") || Boolean.valueOf(System.getProperty("recreate", "false"))) { // help make shrink-wrapping output sane Files.createDirectories(expectedFile.getParent()); Files.deleteIfExists(expectedFile); diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2.java new file mode 100644 index 000000000..a4868392d --- /dev/null +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2.java @@ -0,0 +1,215 @@ +/* + * (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.conjure.java.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.palantir.conjure.java.types.Union2.Bar; +import com.palantir.conjure.java.types.Union2.Baz; +import com.palantir.conjure.java.types.Union2.Foo; +import com.palantir.conjure.java.types.Union2.UnknownVariant; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * This is hand-rolled, I just want to make sure it's compatible. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnknownVariant.class) +@JsonSubTypes({@JsonSubTypes.Type(Foo.class), @JsonSubTypes.Type(Bar.class), @JsonSubTypes.Type(Baz.class)}) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface Union2 { + + sealed interface Known permits Foo, Bar, Baz {} + + static Union2 foo(String value) { + return new Foo(value); + } + + /** + * @deprecated Int is deprecated. + */ + @Deprecated + static Union2 bar(int value) { + return new Bar(value); + } + + /** + * 64-bit integer. + * @deprecated Prefer foo. + */ + @Deprecated + static Union2 baz(long value) { + return new Baz(value); + } + + static Union2 unknown(@Safe String type, Object value) { + return switch (Preconditions.checkNotNull(type, "Type is required")) { + case "foo" -> throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: foo"); + case "bar" -> throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: bar"); + case "baz" -> throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: baz"); + default -> new UnknownVariant(type, Collections.singletonMap(type, value)); + }; + } + + default Known throwOnUnknown() { + if (this instanceof UnknownVariant) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", + SafeArg.of("unknownType", ((UnknownVariant) this).getType())); + } else { + return (Known) this; + } + } + + @JsonTypeName("foo") + record Foo(String value) implements Union2, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Foo(@JsonSetter("foo") @Nonnull String value) { + Preconditions.checkNotNull(value, "foo cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + @SuppressWarnings("UnusedMethod") + private String getType() { + return "foo"; + } + + @JsonProperty("foo") + public String getValue() { + return value; + } + } + + @JsonTypeName("bar") + record Bar(int value) implements Union2, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Bar(@JsonSetter("bar") @Nonnull int value) { + Preconditions.checkNotNull(value, "bar cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + @SuppressWarnings("UnusedMethod") + private String getType() { + return "bar"; + } + + @JsonProperty("bar") + public int getValue() { + return value; + } + } + + @JsonTypeName("baz") + record Baz(long value) implements Union2, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Baz(@JsonSetter("baz") @Nonnull long value) { + Preconditions.checkNotNull(value, "baz cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + @SuppressWarnings("UnusedMethod") + private String getType() { + return "baz"; + } + + @JsonProperty("baz") + public long getValue() { + return value; + } + } + + final class UnknownVariant implements Union2 { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private UnknownVariant(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private UnknownVariant(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + @SuppressWarnings("UnusedMethod") + private String getType() { + return type; + } + + @JsonAnyGetter + public Map getValue() { + return value; + } + + @SuppressWarnings("UnusedMethod") + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof UnknownVariant && equalTo((UnknownVariant) other)); + } + + private boolean equalTo(UnknownVariant other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "UnknownWrapper{type: " + type + ", value: " + value + '}'; + } + } +} diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2Tests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2Tests.java new file mode 100644 index 000000000..7f3a43bef --- /dev/null +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/Union2Tests.java @@ -0,0 +1,91 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.conjure.java.types; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.palantir.conjure.java.serialization.ObjectMappers; +import org.junit.jupiter.api.Test; + +class Union2Tests { + private static final ObjectMapper MAPPER = ObjectMappers.newServerObjectMapper(); + + @Test + public void testCannotCreateUnknownTypeFromKnownType() { + assertThatThrownBy(() -> Union2.unknown("bar", "value")); + } + + @Test + void record_equality() { + Union2 foo = Union2.foo("hello"); + Union2 helloAgain = Union2.foo("hello"); + Union2 bar = Union2.bar(123); + // Amusingly, java.lang.Record#equals says "the precise algorithm used in the implicitly provided + // implementation is unspecified and is subject to change" + assertThat(foo.equals(bar)).isFalse(); + assertThat(foo.equals(helloAgain)).isTrue(); + } + + // These tests require Java 17 AND --enable-preview, see https://github.com/palantir/gradle-baseline/pull/2319 + @Test + void switch_statement_compiles() { + Union2 myUnion = Union2.foo("hello"); + + // syntax requires java 19 (see JEP 405) + // if (myUnion instanceof Union2.Foo(String value)) { + // System.out.println(value); + // } + + switch (myUnion) { + case Union2.Foo foo -> System.out.println(foo.getValue()); + case Union2.Bar bar -> System.out.println(bar.getValue()); + case Union2.Baz baz -> System.out.println(baz.getValue()); + case Union2.UnknownVariant unknownWrapper -> System.out.println(unknownWrapper); + } + } + + @Test + void throwOnUnknown_allows_narrowing_to_a_specific_subtype() { + Union2 myUnion = Union2.foo("hello"); + Union2.Known narrowedSubtype = myUnion.throwOnUnknown(); + switch (narrowedSubtype) { + case Union2.Foo foo -> System.out.println(foo.getValue()); + case Union2.Bar bar -> System.out.println(bar.getValue()); + case Union2.Baz baz -> System.out.println(baz.getValue()); + } + } + + @Test + void serialization() throws JsonProcessingException { + ObjectMapper mapper = ObjectMappers.newServerObjectMapper(); + Union2 foo = Union2.foo("hello"); + assertThat(mapper.writeValueAsString(foo)).isEqualTo("{\"type\":\"foo\",\"foo\":\"hello\"}"); + Union2 unknownVariant = Union2.unknown("asdf", 12345); + assertThat(mapper.writeValueAsString(unknownVariant)).isEqualTo("{\"type\":\"asdf\",\"asdf\":12345}"); + } + + @Test + void deserialization() throws JsonProcessingException { + ObjectMapper mapper = ObjectMappers.newServerObjectMapper(); + assertThat(mapper.readValue("{\"type\":\"foo\",\"foo\":\"hello\"}", Union2.class)) + .isEqualTo(Union2.foo("hello")); + assertThat(mapper.readValue("{\"type\":\"asdf\",\"asdf\":12345}", Union2.class)) + .isEqualTo(Union2.unknown("asdf", 12345)); + } +} diff --git a/conjure-java-core/src/test/resources/example-sealed-unions.yml b/conjure-java-core/src/test/resources/example-sealed-unions.yml new file mode 100644 index 000000000..748b942d0 --- /dev/null +++ b/conjure-java-core/src/test/resources/example-sealed-unions.yml @@ -0,0 +1,63 @@ +types: + imports: + ExternalLong: + base-type: safelong + external: + java: java.lang.Long + definitions: + default-package: com.palantir.product + objects: + StringExample: + fields: + string: string + ListAlias: + alias: list + OptionalAlias: + alias: optional + safety: safe + SetAlias: + alias: set + MapAliasExample: + alias: map + UnionTypeExample: + docs: A type which can either be a StringExample, a set of strings, or an integer. + union: + stringExample: + type: StringExample + docs: Docs for when UnionTypeExample is of type StringExample. + thisFieldIsAnInteger: integer + alsoAnInteger: integer + if: integer # some 'bad' member names! + new: integer + interface: integer + completed: integer + unknown: integer + known: string + optional: optional + list: list + set: set + map: map + optionalAlias: OptionalAlias + listAlias: ListAlias + setAlias: SetAlias + mapAlias: MapAliasExample + UnionWithUnknownString: + union: + unknown: string + EmptyUnionTypeExample: + union: {} + SampleConjureUnion: + union: + foo: string + bar: + type: integer + deprecated: Int is deprecated. + baz: + type: ExternalLong + docs: 64-bit integer. + deprecated: Prefer `foo`. + SingleUnion: + union: + foo: + type: string + safety: safe diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/EmptyUnionTypeExample.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/EmptyUnionTypeExample.java new file mode 100644 index 000000000..7707ec1c9 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/EmptyUnionTypeExample.java @@ -0,0 +1,173 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = EmptyUnionTypeExample.Unknown.class) +@JsonSubTypes +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface EmptyUnionTypeExample { + static EmptyUnionTypeExample unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + default: + return new Unknown(type, Collections.singletonMap(type, value)); + } + } + + default Known throwOnUnknown() { + if (this instanceof Unknown) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", SafeArg.of("type", ((Unknown) this).getType())); + } else { + return (Known) this; + } + } + + void accept(Visitor visitor); + + sealed interface Known {} + + final class Unknown implements EmptyUnionTypeExample { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private Unknown(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private Unknown(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Unknown && equalTo((Unknown) other)); + } + + private boolean equalTo(Unknown other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "Unknown{type: " + type + ", value: " + value + '}'; + } + } + + interface Visitor { + T visitUnknown(@Safe String unknownType, Object unknownValue); + + /** + * @Deprecated - prefer Java 17 pattern matching switch expressions. + */ + @Deprecated + static UnknownStageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + final class VisitorBuilder implements UnknownStageVisitorBuilder, Completed_StageVisitorBuilder { + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'EmptyUnionTypeExample' union", SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + interface Completed_StageVisitorBuilder { + Visitor build(); + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/ListAlias.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/ListAlias.java new file mode 100644 index 000000000..18a14d515 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/ListAlias.java @@ -0,0 +1,53 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import java.util.Collections; +import java.util.List; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.AliasGenerator") +public final class ListAlias { + private static final ListAlias EMPTY = new ListAlias(); + + private final List value; + + private ListAlias(@Nonnull List value) { + this.value = Preconditions.checkNotNull(value, "value cannot be null"); + } + + private ListAlias() { + this(Collections.emptyList()); + } + + @JsonValue + public List get() { + return value; + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof ListAlias && this.value.equals(((ListAlias) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ListAlias of(@Nonnull List value) { + return new ListAlias(value); + } + + public static ListAlias empty() { + return EMPTY; + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/MapAliasExample.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/MapAliasExample.java new file mode 100644 index 000000000..27ad19291 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/MapAliasExample.java @@ -0,0 +1,54 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.AliasGenerator") +public final class MapAliasExample { + private static final MapAliasExample EMPTY = new MapAliasExample(); + + private final Map value; + + private MapAliasExample(@Nonnull Map value) { + this.value = Preconditions.checkNotNull(value, "value cannot be null"); + } + + private MapAliasExample() { + this(Collections.emptyMap()); + } + + @JsonValue + public Map get() { + return value; + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return this == other + || (other instanceof MapAliasExample && this.value.equals(((MapAliasExample) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static MapAliasExample of(@Nonnull Map value) { + return new MapAliasExample(value); + } + + public static MapAliasExample empty() { + return EMPTY; + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/OptionalAlias.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/OptionalAlias.java new file mode 100644 index 000000000..152c38534 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/OptionalAlias.java @@ -0,0 +1,55 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import java.util.Optional; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Safe +@Generated("com.palantir.conjure.java.types.AliasGenerator") +public final class OptionalAlias { + private static final OptionalAlias EMPTY = new OptionalAlias(); + + private final Optional<@Safe String> value; + + private OptionalAlias(@Nonnull Optional<@Safe String> value) { + this.value = Preconditions.checkNotNull(value, "value cannot be null"); + } + + private OptionalAlias() { + this(Optional.empty()); + } + + @JsonValue + @Safe + public Optional<@Safe String> get() { + return value; + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof OptionalAlias && this.value.equals(((OptionalAlias) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static OptionalAlias of(@Safe @Nonnull Optional<@Safe String> value) { + return new OptionalAlias(value); + } + + public static OptionalAlias empty() { + return EMPTY; + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SampleConjureUnion.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SampleConjureUnion.java new file mode 100644 index 000000000..41b0bef7a --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SampleConjureUnion.java @@ -0,0 +1,372 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = SampleConjureUnion.Unknown.class) +@JsonSubTypes({@JsonSubTypes.Type(Foo.class), @JsonSubTypes.Type(Bar.class), @JsonSubTypes.Type(Baz.class)}) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface SampleConjureUnion { + static SampleConjureUnion foo(String value) { + return new Foo(value); + } + + /** + * @deprecated Int is deprecated. + */ + @Deprecated + static SampleConjureUnion bar(int value) { + return new Bar(value); + } + + /** + * 64-bit integer. + * @deprecated Prefer foo. + */ + @Deprecated + static SampleConjureUnion baz(long value) { + return new Baz(value); + } + + static SampleConjureUnion unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + case "foo": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: foo"); + case "bar": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: bar"); + case "baz": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: baz"); + default: + return new Unknown(type, Collections.singletonMap(type, value)); + } + } + + default Known throwOnUnknown() { + if (this instanceof Unknown) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", SafeArg.of("type", ((Unknown) this).getType())); + } else { + return (Known) this; + } + } + + void accept(Visitor visitor); + + sealed interface Known permits Foo, Bar, Baz {} + + @JsonTypeName("foo") + record Foo(String value) implements SampleConjureUnion, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Foo(@JsonSetter("foo") @Nonnull String value) { + Preconditions.checkNotNull(value, "foo cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "foo"; + } + + @JsonProperty("foo") + private String getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitFoo(value); + } + + @Override + public String toString() { + return "Foo{value: " + value + '}'; + } + } + + @JsonTypeName("bar") + record Bar(int value) implements SampleConjureUnion, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Bar(@JsonSetter("bar") @Nonnull int value) { + Preconditions.checkNotNull(value, "bar cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "bar"; + } + + @JsonProperty("bar") + private int getValue() { + return value; + } + + @Override + @SuppressWarnings("deprecation") + public T accept(Visitor visitor) { + return visitor.visitBar(value); + } + + @Override + public String toString() { + return "Bar{value: " + value + '}'; + } + } + + @JsonTypeName("baz") + record Baz(long value) implements SampleConjureUnion, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Baz(@JsonSetter("baz") @Nonnull long value) { + Preconditions.checkNotNull(value, "baz cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "baz"; + } + + @JsonProperty("baz") + private long getValue() { + return value; + } + + @Override + @SuppressWarnings("deprecation") + public T accept(Visitor visitor) { + return visitor.visitBaz(value); + } + + @Override + public String toString() { + return "Baz{value: " + value + '}'; + } + } + + final class Unknown implements SampleConjureUnion { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private Unknown(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private Unknown(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Unknown && equalTo((Unknown) other)); + } + + private boolean equalTo(Unknown other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "Unknown{type: " + type + ", value: " + value + '}'; + } + } + + interface Visitor { + T visitFoo(String value); + + /** + * @deprecated Int is deprecated. + */ + @Deprecated + T visitBar(int value); + + /** + * 64-bit integer. + * @deprecated Prefer foo. + */ + @Deprecated + T visitBaz(long value); + + T visitUnknown(@Safe String unknownType, Object unknownValue); + + /** + * @Deprecated - prefer Java 17 pattern matching switch expressions. + */ + @Deprecated + static BarStageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + final class VisitorBuilder + implements BarStageVisitorBuilder, + BazStageVisitorBuilder, + FooStageVisitorBuilder, + UnknownStageVisitorBuilder, + Completed_StageVisitorBuilder { + private IntFunction barVisitor; + + private Function bazVisitor; + + private Function fooVisitor; + + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public BazStageVisitorBuilder bar(@Nonnull IntFunction barVisitor) { + Preconditions.checkNotNull(barVisitor, "barVisitor cannot be null"); + this.barVisitor = barVisitor; + return this; + } + + @Override + public FooStageVisitorBuilder baz(@Nonnull Function bazVisitor) { + Preconditions.checkNotNull(bazVisitor, "bazVisitor cannot be null"); + this.bazVisitor = bazVisitor; + return this; + } + + @Override + public UnknownStageVisitorBuilder foo(@Nonnull Function fooVisitor) { + Preconditions.checkNotNull(fooVisitor, "fooVisitor cannot be null"); + this.fooVisitor = fooVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'SampleConjureUnion' union", SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final IntFunction barVisitor = this.barVisitor; + final Function bazVisitor = this.bazVisitor; + final Function fooVisitor = this.fooVisitor; + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitBar(int value) { + return barVisitor.apply(value); + } + + @Override + public T visitBaz(long value) { + return bazVisitor.apply(value); + } + + @Override + public T visitFoo(String value) { + return fooVisitor.apply(value); + } + + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + interface BarStageVisitorBuilder { + BazStageVisitorBuilder bar(@Nonnull IntFunction barVisitor); + } + + interface BazStageVisitorBuilder { + FooStageVisitorBuilder baz(@Nonnull Function bazVisitor); + } + + interface FooStageVisitorBuilder { + UnknownStageVisitorBuilder foo(@Nonnull Function fooVisitor); + } + + interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + interface Completed_StageVisitorBuilder { + Visitor build(); + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SetAlias.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SetAlias.java new file mode 100644 index 000000000..ed2a43477 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SetAlias.java @@ -0,0 +1,53 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import java.util.Collections; +import java.util.Set; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.AliasGenerator") +public final class SetAlias { + private static final SetAlias EMPTY = new SetAlias(); + + private final Set value; + + private SetAlias(@Nonnull Set value) { + this.value = Preconditions.checkNotNull(value, "value cannot be null"); + } + + private SetAlias() { + this(Collections.emptySet()); + } + + @JsonValue + public Set get() { + return value; + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof SetAlias && this.value.equals(((SetAlias) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static SetAlias of(@Nonnull Set value) { + return new SetAlias(value); + } + + public static SetAlias empty() { + return EMPTY; + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SingleUnion.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SingleUnion.java new file mode 100644 index 000000000..a73a7b591 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/SingleUnion.java @@ -0,0 +1,233 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = SingleUnion.Unknown.class) +@JsonSubTypes(@JsonSubTypes.Type(Foo.class)) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface SingleUnion { + static SingleUnion foo(@Safe String value) { + return new Foo(value); + } + + static SingleUnion unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + case "foo": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: foo"); + default: + return new Unknown(type, Collections.singletonMap(type, value)); + } + } + + default Known throwOnUnknown() { + if (this instanceof Unknown) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", SafeArg.of("type", ((Unknown) this).getType())); + } else { + return (Known) this; + } + } + + void accept(Visitor visitor); + + sealed interface Known permits Foo {} + + @JsonTypeName("foo") + record Foo(String value) implements SingleUnion, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Foo(@JsonSetter("foo") @Nonnull String value) { + Preconditions.checkNotNull(value, "foo cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "foo"; + } + + @JsonProperty("foo") + private String getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitFoo(value); + } + + @Override + public String toString() { + return "Foo{value: " + value + '}'; + } + } + + final class Unknown implements SingleUnion { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private Unknown(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private Unknown(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Unknown && equalTo((Unknown) other)); + } + + private boolean equalTo(Unknown other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "Unknown{type: " + type + ", value: " + value + '}'; + } + } + + interface Visitor { + T visitFoo(@Safe String value); + + T visitUnknown(@Safe String unknownType, Object unknownValue); + + /** + * @Deprecated - prefer Java 17 pattern matching switch expressions. + */ + @Deprecated + static FooStageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + final class VisitorBuilder + implements FooStageVisitorBuilder, UnknownStageVisitorBuilder, Completed_StageVisitorBuilder { + private Function<@Safe String, T> fooVisitor; + + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public UnknownStageVisitorBuilder foo(@Nonnull Function<@Safe String, T> fooVisitor) { + Preconditions.checkNotNull(fooVisitor, "fooVisitor cannot be null"); + this.fooVisitor = fooVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'SingleUnion' union", SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final Function<@Safe String, T> fooVisitor = this.fooVisitor; + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitFoo(String value) { + return fooVisitor.apply(value); + } + + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + interface FooStageVisitorBuilder { + UnknownStageVisitorBuilder foo(@Nonnull Function<@Safe String, T> fooVisitor); + } + + interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + interface Completed_StageVisitorBuilder { + Visitor build(); + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/StringExample.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/StringExample.java new file mode 100644 index 000000000..2f2e8046b --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/StringExample.java @@ -0,0 +1,107 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@JsonDeserialize(builder = StringExample.Builder.class) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class StringExample { + private final String string; + + private StringExample(String string) { + validateFields(string); + this.string = string; + } + + @JsonProperty("string") + public String getString() { + return this.string; + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof StringExample && equalTo((StringExample) other)); + } + + private boolean equalTo(StringExample other) { + return this.string.equals(other.string); + } + + @Override + public int hashCode() { + return this.string.hashCode(); + } + + @Override + public String toString() { + return "StringExample{string: " + string + '}'; + } + + public static StringExample of(String string) { + return builder().string(string).build(); + } + + private static void validateFields(String string) { + List missingFields = null; + missingFields = addFieldIfMissing(missingFields, string, "string"); + if (missingFields != null) { + throw new SafeIllegalArgumentException( + "Some required fields have not been set", SafeArg.of("missingFields", missingFields)); + } + } + + private static List addFieldIfMissing(List prev, Object fieldValue, String fieldName) { + List missingFields = prev; + if (fieldValue == null) { + if (missingFields == null) { + missingFields = new ArrayList<>(1); + } + missingFields.add(fieldName); + } + return missingFields; + } + + public static Builder builder() { + return new Builder(); + } + + @Generated("com.palantir.conjure.java.types.BeanBuilderGenerator") + public static final class Builder { + boolean _buildInvoked; + + private String string; + + private Builder() {} + + public Builder from(StringExample other) { + checkNotBuilt(); + string(other.getString()); + return this; + } + + @JsonSetter("string") + public Builder string(@Nonnull String string) { + checkNotBuilt(); + this.string = Preconditions.checkNotNull(string, "string cannot be null"); + return this; + } + + public StringExample build() { + checkNotBuilt(); + this._buildInvoked = true; + return new StringExample(string); + } + + private void checkNotBuilt() { + Preconditions.checkState(!_buildInvoked, "Build has already been called"); + } + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionTypeExample.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionTypeExample.java new file mode 100644 index 000000000..67858e3c0 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionTypeExample.java @@ -0,0 +1,1226 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.Nulls; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +/** + * A type which can either be a StringExample, a set of strings, or an integer. + */ +@Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnionTypeExample.Unknown.class) +@JsonSubTypes({ + @JsonSubTypes.Type(StringExample.class), + @JsonSubTypes.Type(ThisFieldIsAnInteger.class), + @JsonSubTypes.Type(AlsoAnInteger.class), + @JsonSubTypes.Type(If.class), + @JsonSubTypes.Type(New.class), + @JsonSubTypes.Type(Interface.class), + @JsonSubTypes.Type(Completed.class), + @JsonSubTypes.Type(Unknown_.class), + @JsonSubTypes.Type(Known_.class), + @JsonSubTypes.Type(Optional.class), + @JsonSubTypes.Type(List.class), + @JsonSubTypes.Type(Set.class), + @JsonSubTypes.Type(Map.class), + @JsonSubTypes.Type(OptionalAlias.class), + @JsonSubTypes.Type(ListAlias.class), + @JsonSubTypes.Type(SetAlias.class), + @JsonSubTypes.Type(MapAlias.class) +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface UnionTypeExample { + /** + * Docs for when UnionTypeExample is of type StringExample. + */ + static UnionTypeExample stringExample(withvisitors.com.palantir.product.StringExample value) { + return new StringExample(value); + } + + static UnionTypeExample thisFieldIsAnInteger(int value) { + return new ThisFieldIsAnInteger(value); + } + + static UnionTypeExample alsoAnInteger(int value) { + return new AlsoAnInteger(value); + } + + static UnionTypeExample if_(int value) { + return new If(value); + } + + static UnionTypeExample new_(int value) { + return new New(value); + } + + static UnionTypeExample interface_(int value) { + return new Interface(value); + } + + static UnionTypeExample completed(int value) { + return new Completed(value); + } + + static UnionTypeExample unknown_(int value) { + return new Unknown_(value); + } + + static UnionTypeExample known_(String value) { + return new Known_(value); + } + + static UnionTypeExample optional(java.util.Optional value) { + return new Optional(value); + } + + static UnionTypeExample list(java.util.List value) { + return new List(value); + } + + static UnionTypeExample set(java.util.Set value) { + return new Set(value); + } + + static UnionTypeExample map(java.util.Map value) { + return new Map(value); + } + + static UnionTypeExample optionalAlias(withvisitors.com.palantir.product.OptionalAlias value) { + return new OptionalAlias(value); + } + + static UnionTypeExample listAlias(withvisitors.com.palantir.product.ListAlias value) { + return new ListAlias(value); + } + + static UnionTypeExample setAlias(withvisitors.com.palantir.product.SetAlias value) { + return new SetAlias(value); + } + + static UnionTypeExample mapAlias(MapAliasExample value) { + return new MapAlias(value); + } + + static UnionTypeExample unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + case "stringExample": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: stringExample"); + case "thisFieldIsAnInteger": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: thisFieldIsAnInteger"); + case "alsoAnInteger": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: alsoAnInteger"); + case "if": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: if"); + case "new": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: new"); + case "interface": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: interface"); + case "completed": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: completed"); + case "unknown": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: unknown"); + case "known": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: known"); + case "optional": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: optional"); + case "list": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: list"); + case "set": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: set"); + case "map": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: map"); + case "optionalAlias": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: optionalAlias"); + case "listAlias": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: listAlias"); + case "setAlias": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: setAlias"); + case "mapAlias": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: mapAlias"); + default: + return new Unknown(type, Collections.singletonMap(type, value)); + } + } + + default Known throwOnUnknown() { + if (this instanceof Unknown) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", SafeArg.of("type", ((Unknown) this).getType())); + } else { + return (Known) this; + } + } + + void accept(Visitor visitor); + + sealed interface Known + permits StringExample, + ThisFieldIsAnInteger, + AlsoAnInteger, + If, + New, + Interface, + Completed, + Unknown_, + Known_, + Optional, + List, + Set, + Map, + OptionalAlias, + ListAlias, + SetAlias, + MapAlias {} + + @JsonTypeName("stringExample") + record StringExample(withvisitors.com.palantir.product.StringExample value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + StringExample(@JsonSetter("stringExample") @Nonnull withvisitors.com.palantir.product.StringExample value) { + Preconditions.checkNotNull(value, "stringExample cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "stringExample"; + } + + @JsonProperty("stringExample") + private withvisitors.com.palantir.product.StringExample getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitStringExample(value); + } + + @Override + public String toString() { + return "StringExample{value: " + value + '}'; + } + } + + @JsonTypeName("thisFieldIsAnInteger") + record ThisFieldIsAnInteger(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ThisFieldIsAnInteger(@JsonSetter("thisFieldIsAnInteger") @Nonnull int value) { + Preconditions.checkNotNull(value, "thisFieldIsAnInteger cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "thisFieldIsAnInteger"; + } + + @JsonProperty("thisFieldIsAnInteger") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitThisFieldIsAnInteger(value); + } + + @Override + public String toString() { + return "ThisFieldIsAnInteger{value: " + value + '}'; + } + } + + @JsonTypeName("alsoAnInteger") + record AlsoAnInteger(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + AlsoAnInteger(@JsonSetter("alsoAnInteger") @Nonnull int value) { + Preconditions.checkNotNull(value, "alsoAnInteger cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "alsoAnInteger"; + } + + @JsonProperty("alsoAnInteger") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitAlsoAnInteger(value); + } + + @Override + public String toString() { + return "AlsoAnInteger{value: " + value + '}'; + } + } + + @JsonTypeName("if") + record If(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + If(@JsonSetter("if") @Nonnull int value) { + Preconditions.checkNotNull(value, "if cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "if"; + } + + @JsonProperty("if") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitIf(value); + } + + @Override + public String toString() { + return "If{value: " + value + '}'; + } + } + + @JsonTypeName("new") + record New(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + New(@JsonSetter("new") @Nonnull int value) { + Preconditions.checkNotNull(value, "new cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "new"; + } + + @JsonProperty("new") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitNew(value); + } + + @Override + public String toString() { + return "New{value: " + value + '}'; + } + } + + @JsonTypeName("interface") + record Interface(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Interface(@JsonSetter("interface") @Nonnull int value) { + Preconditions.checkNotNull(value, "interface cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "interface"; + } + + @JsonProperty("interface") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitInterface(value); + } + + @Override + public String toString() { + return "Interface{value: " + value + '}'; + } + } + + @JsonTypeName("completed") + record Completed(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Completed(@JsonSetter("completed") @Nonnull int value) { + Preconditions.checkNotNull(value, "completed cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "completed"; + } + + @JsonProperty("completed") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitCompleted(value); + } + + @Override + public String toString() { + return "Completed{value: " + value + '}'; + } + } + + @JsonTypeName("unknown") + record Unknown_(int value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Unknown_(@JsonSetter("unknown") @Nonnull int value) { + Preconditions.checkNotNull(value, "unknown_ cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "unknown"; + } + + @JsonProperty("unknown") + private int getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown_(value); + } + + @Override + public String toString() { + return "Unknown_{value: " + value + '}'; + } + } + + @JsonTypeName("known") + record Known_(String value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Known_(@JsonSetter("known") @Nonnull String value) { + Preconditions.checkNotNull(value, "known_ cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "known"; + } + + @JsonProperty("known") + private String getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitKnown_(value); + } + + @Override + public String toString() { + return "Known_{value: " + value + '}'; + } + } + + @JsonTypeName("optional") + record Optional(java.util.Optional value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Optional(@JsonSetter(value = "optional", nulls = Nulls.AS_EMPTY) @Nonnull java.util.Optional value) { + Preconditions.checkNotNull(value, "optional cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "optional"; + } + + @JsonProperty("optional") + private java.util.Optional getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitOptional(value); + } + + @Override + public String toString() { + return "Optional{value: " + value + '}'; + } + } + + @JsonTypeName("list") + record List(java.util.List value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + List(@JsonSetter(value = "list", nulls = Nulls.AS_EMPTY) @Nonnull java.util.List value) { + Preconditions.checkNotNull(value, "list cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "list"; + } + + @JsonProperty("list") + private java.util.List getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitList(value); + } + + @Override + public String toString() { + return "List{value: " + value + '}'; + } + } + + @JsonTypeName("set") + record Set(java.util.Set value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Set(@JsonSetter(value = "set", nulls = Nulls.AS_EMPTY) @Nonnull java.util.Set value) { + Preconditions.checkNotNull(value, "set cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "set"; + } + + @JsonProperty("set") + private java.util.Set getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitSet(value); + } + + @Override + public String toString() { + return "Set{value: " + value + '}'; + } + } + + @JsonTypeName("map") + record Map(java.util.Map value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Map(@JsonSetter(value = "map", nulls = Nulls.AS_EMPTY) @Nonnull java.util.Map value) { + Preconditions.checkNotNull(value, "map cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "map"; + } + + @JsonProperty("map") + private java.util.Map getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitMap(value); + } + + @Override + public String toString() { + return "Map{value: " + value + '}'; + } + } + + @JsonTypeName("optionalAlias") + record OptionalAlias(withvisitors.com.palantir.product.OptionalAlias value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + OptionalAlias( + @JsonSetter(value = "optionalAlias", nulls = Nulls.AS_EMPTY) @Nonnull + withvisitors.com.palantir.product.OptionalAlias value) { + Preconditions.checkNotNull(value, "optionalAlias cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "optionalAlias"; + } + + @JsonProperty("optionalAlias") + private withvisitors.com.palantir.product.OptionalAlias getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitOptionalAlias(value); + } + + @Override + public String toString() { + return "OptionalAlias{value: " + value + '}'; + } + } + + @JsonTypeName("listAlias") + record ListAlias(withvisitors.com.palantir.product.ListAlias value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ListAlias( + @JsonSetter(value = "listAlias", nulls = Nulls.AS_EMPTY) @Nonnull + withvisitors.com.palantir.product.ListAlias value) { + Preconditions.checkNotNull(value, "listAlias cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "listAlias"; + } + + @JsonProperty("listAlias") + private withvisitors.com.palantir.product.ListAlias getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitListAlias(value); + } + + @Override + public String toString() { + return "ListAlias{value: " + value + '}'; + } + } + + @JsonTypeName("setAlias") + record SetAlias(withvisitors.com.palantir.product.SetAlias value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + SetAlias( + @JsonSetter(value = "setAlias", nulls = Nulls.AS_EMPTY) @Nonnull + withvisitors.com.palantir.product.SetAlias value) { + Preconditions.checkNotNull(value, "setAlias cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "setAlias"; + } + + @JsonProperty("setAlias") + private withvisitors.com.palantir.product.SetAlias getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitSetAlias(value); + } + + @Override + public String toString() { + return "SetAlias{value: " + value + '}'; + } + } + + @JsonTypeName("mapAlias") + record MapAlias(MapAliasExample value) implements UnionTypeExample, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + MapAlias(@JsonSetter(value = "mapAlias", nulls = Nulls.AS_EMPTY) @Nonnull MapAliasExample value) { + Preconditions.checkNotNull(value, "mapAlias cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "mapAlias"; + } + + @JsonProperty("mapAlias") + private MapAliasExample getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitMapAlias(value); + } + + @Override + public String toString() { + return "MapAlias{value: " + value + '}'; + } + } + + final class Unknown implements UnionTypeExample { + private final String type; + + private final java.util.Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private Unknown(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private Unknown(@Nonnull String type, @Nonnull java.util.Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private java.util.Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Unknown && equalTo((Unknown) other)); + } + + private boolean equalTo(Unknown other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "Unknown{type: " + type + ", value: " + value + '}'; + } + } + + interface Visitor { + /** + * Docs for when UnionTypeExample is of type StringExample. + */ + T visitStringExample(withvisitors.com.palantir.product.StringExample value); + + T visitThisFieldIsAnInteger(int value); + + T visitAlsoAnInteger(int value); + + T visitIf(int value); + + T visitNew(int value); + + T visitInterface(int value); + + T visitCompleted(int value); + + T visitUnknown_(int value); + + T visitKnown_(String value); + + T visitOptional(java.util.Optional value); + + T visitList(java.util.List value); + + T visitSet(java.util.Set value); + + T visitMap(java.util.Map value); + + T visitOptionalAlias(withvisitors.com.palantir.product.OptionalAlias value); + + T visitListAlias(withvisitors.com.palantir.product.ListAlias value); + + T visitSetAlias(withvisitors.com.palantir.product.SetAlias value); + + T visitMapAlias(MapAliasExample value); + + T visitUnknown(@Safe String unknownType, Object unknownValue); + + /** + * @Deprecated - prefer Java 17 pattern matching switch expressions. + */ + @Deprecated + static AlsoAnIntegerStageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + final class VisitorBuilder + implements AlsoAnIntegerStageVisitorBuilder, + CompletedStageVisitorBuilder, + IfStageVisitorBuilder, + InterfaceStageVisitorBuilder, + Known_StageVisitorBuilder, + ListStageVisitorBuilder, + ListAliasStageVisitorBuilder, + MapStageVisitorBuilder, + MapAliasStageVisitorBuilder, + NewStageVisitorBuilder, + OptionalStageVisitorBuilder, + OptionalAliasStageVisitorBuilder, + SetStageVisitorBuilder, + SetAliasStageVisitorBuilder, + StringExampleStageVisitorBuilder, + ThisFieldIsAnIntegerStageVisitorBuilder, + Unknown_StageVisitorBuilder, + UnknownStageVisitorBuilder, + Completed_StageVisitorBuilder { + private IntFunction alsoAnIntegerVisitor; + + private IntFunction completedVisitor; + + private IntFunction ifVisitor; + + private IntFunction interfaceVisitor; + + private Function known_Visitor; + + private Function, T> listVisitor; + + private Function listAliasVisitor; + + private Function, T> mapVisitor; + + private Function mapAliasVisitor; + + private IntFunction newVisitor; + + private Function, T> optionalVisitor; + + private Function optionalAliasVisitor; + + private Function, T> setVisitor; + + private Function setAliasVisitor; + + private Function stringExampleVisitor; + + private IntFunction thisFieldIsAnIntegerVisitor; + + private IntFunction unknown_Visitor; + + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public CompletedStageVisitorBuilder alsoAnInteger(@Nonnull IntFunction alsoAnIntegerVisitor) { + Preconditions.checkNotNull(alsoAnIntegerVisitor, "alsoAnIntegerVisitor cannot be null"); + this.alsoAnIntegerVisitor = alsoAnIntegerVisitor; + return this; + } + + @Override + public IfStageVisitorBuilder completed(@Nonnull IntFunction completedVisitor) { + Preconditions.checkNotNull(completedVisitor, "completedVisitor cannot be null"); + this.completedVisitor = completedVisitor; + return this; + } + + @Override + public InterfaceStageVisitorBuilder if_(@Nonnull IntFunction ifVisitor) { + Preconditions.checkNotNull(ifVisitor, "ifVisitor cannot be null"); + this.ifVisitor = ifVisitor; + return this; + } + + @Override + public Known_StageVisitorBuilder interface_(@Nonnull IntFunction interfaceVisitor) { + Preconditions.checkNotNull(interfaceVisitor, "interfaceVisitor cannot be null"); + this.interfaceVisitor = interfaceVisitor; + return this; + } + + @Override + public ListStageVisitorBuilder known_(@Nonnull Function known_Visitor) { + Preconditions.checkNotNull(known_Visitor, "known_Visitor cannot be null"); + this.known_Visitor = known_Visitor; + return this; + } + + @Override + public ListAliasStageVisitorBuilder list(@Nonnull Function, T> listVisitor) { + Preconditions.checkNotNull(listVisitor, "listVisitor cannot be null"); + this.listVisitor = listVisitor; + return this; + } + + @Override + public MapStageVisitorBuilder listAlias( + @Nonnull Function listAliasVisitor) { + Preconditions.checkNotNull(listAliasVisitor, "listAliasVisitor cannot be null"); + this.listAliasVisitor = listAliasVisitor; + return this; + } + + @Override + public MapAliasStageVisitorBuilder map(@Nonnull Function, T> mapVisitor) { + Preconditions.checkNotNull(mapVisitor, "mapVisitor cannot be null"); + this.mapVisitor = mapVisitor; + return this; + } + + @Override + public NewStageVisitorBuilder mapAlias(@Nonnull Function mapAliasVisitor) { + Preconditions.checkNotNull(mapAliasVisitor, "mapAliasVisitor cannot be null"); + this.mapAliasVisitor = mapAliasVisitor; + return this; + } + + @Override + public OptionalStageVisitorBuilder new_(@Nonnull IntFunction newVisitor) { + Preconditions.checkNotNull(newVisitor, "newVisitor cannot be null"); + this.newVisitor = newVisitor; + return this; + } + + @Override + public OptionalAliasStageVisitorBuilder optional( + @Nonnull Function, T> optionalVisitor) { + Preconditions.checkNotNull(optionalVisitor, "optionalVisitor cannot be null"); + this.optionalVisitor = optionalVisitor; + return this; + } + + @Override + public SetStageVisitorBuilder optionalAlias( + @Nonnull Function optionalAliasVisitor) { + Preconditions.checkNotNull(optionalAliasVisitor, "optionalAliasVisitor cannot be null"); + this.optionalAliasVisitor = optionalAliasVisitor; + return this; + } + + @Override + public SetAliasStageVisitorBuilder set(@Nonnull Function, T> setVisitor) { + Preconditions.checkNotNull(setVisitor, "setVisitor cannot be null"); + this.setVisitor = setVisitor; + return this; + } + + @Override + public StringExampleStageVisitorBuilder setAlias( + @Nonnull Function setAliasVisitor) { + Preconditions.checkNotNull(setAliasVisitor, "setAliasVisitor cannot be null"); + this.setAliasVisitor = setAliasVisitor; + return this; + } + + @Override + public ThisFieldIsAnIntegerStageVisitorBuilder stringExample( + @Nonnull Function stringExampleVisitor) { + Preconditions.checkNotNull(stringExampleVisitor, "stringExampleVisitor cannot be null"); + this.stringExampleVisitor = stringExampleVisitor; + return this; + } + + @Override + public Unknown_StageVisitorBuilder thisFieldIsAnInteger( + @Nonnull IntFunction thisFieldIsAnIntegerVisitor) { + Preconditions.checkNotNull(thisFieldIsAnIntegerVisitor, "thisFieldIsAnIntegerVisitor cannot be null"); + this.thisFieldIsAnIntegerVisitor = thisFieldIsAnIntegerVisitor; + return this; + } + + @Override + public UnknownStageVisitorBuilder unknown_(@Nonnull IntFunction unknown_Visitor) { + Preconditions.checkNotNull(unknown_Visitor, "unknown_Visitor cannot be null"); + this.unknown_Visitor = unknown_Visitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'UnionTypeExample' union", SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final IntFunction alsoAnIntegerVisitor = this.alsoAnIntegerVisitor; + final IntFunction completedVisitor = this.completedVisitor; + final IntFunction ifVisitor = this.ifVisitor; + final IntFunction interfaceVisitor = this.interfaceVisitor; + final Function known_Visitor = this.known_Visitor; + final Function, T> listVisitor = this.listVisitor; + final Function listAliasVisitor = this.listAliasVisitor; + final Function, T> mapVisitor = this.mapVisitor; + final Function mapAliasVisitor = this.mapAliasVisitor; + final IntFunction newVisitor = this.newVisitor; + final Function, T> optionalVisitor = this.optionalVisitor; + final Function optionalAliasVisitor = + this.optionalAliasVisitor; + final Function, T> setVisitor = this.setVisitor; + final Function setAliasVisitor = this.setAliasVisitor; + final Function stringExampleVisitor = + this.stringExampleVisitor; + final IntFunction thisFieldIsAnIntegerVisitor = this.thisFieldIsAnIntegerVisitor; + final IntFunction unknown_Visitor = this.unknown_Visitor; + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitAlsoAnInteger(int value) { + return alsoAnIntegerVisitor.apply(value); + } + + @Override + public T visitCompleted(int value) { + return completedVisitor.apply(value); + } + + @Override + public T visitIf(int value) { + return ifVisitor.apply(value); + } + + @Override + public T visitInterface(int value) { + return interfaceVisitor.apply(value); + } + + @Override + public T visitKnown_(String value) { + return known_Visitor.apply(value); + } + + @Override + public T visitList(java.util.List value) { + return listVisitor.apply(value); + } + + @Override + public T visitListAlias(withvisitors.com.palantir.product.ListAlias value) { + return listAliasVisitor.apply(value); + } + + @Override + public T visitMap(java.util.Map value) { + return mapVisitor.apply(value); + } + + @Override + public T visitMapAlias(MapAliasExample value) { + return mapAliasVisitor.apply(value); + } + + @Override + public T visitNew(int value) { + return newVisitor.apply(value); + } + + @Override + public T visitOptional(java.util.Optional value) { + return optionalVisitor.apply(value); + } + + @Override + public T visitOptionalAlias(withvisitors.com.palantir.product.OptionalAlias value) { + return optionalAliasVisitor.apply(value); + } + + @Override + public T visitSet(java.util.Set value) { + return setVisitor.apply(value); + } + + @Override + public T visitSetAlias(withvisitors.com.palantir.product.SetAlias value) { + return setAliasVisitor.apply(value); + } + + @Override + public T visitStringExample(withvisitors.com.palantir.product.StringExample value) { + return stringExampleVisitor.apply(value); + } + + @Override + public T visitThisFieldIsAnInteger(int value) { + return thisFieldIsAnIntegerVisitor.apply(value); + } + + @Override + public T visitUnknown_(int value) { + return unknown_Visitor.apply(value); + } + + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + interface AlsoAnIntegerStageVisitorBuilder { + CompletedStageVisitorBuilder alsoAnInteger(@Nonnull IntFunction alsoAnIntegerVisitor); + } + + interface CompletedStageVisitorBuilder { + IfStageVisitorBuilder completed(@Nonnull IntFunction completedVisitor); + } + + interface IfStageVisitorBuilder { + InterfaceStageVisitorBuilder if_(@Nonnull IntFunction ifVisitor); + } + + interface InterfaceStageVisitorBuilder { + Known_StageVisitorBuilder interface_(@Nonnull IntFunction interfaceVisitor); + } + + interface Known_StageVisitorBuilder { + ListStageVisitorBuilder known_(@Nonnull Function known_Visitor); + } + + interface ListStageVisitorBuilder { + ListAliasStageVisitorBuilder list(@Nonnull Function, T> listVisitor); + } + + interface ListAliasStageVisitorBuilder { + MapStageVisitorBuilder listAlias( + @Nonnull Function listAliasVisitor); + } + + interface MapStageVisitorBuilder { + MapAliasStageVisitorBuilder map(@Nonnull Function, T> mapVisitor); + } + + interface MapAliasStageVisitorBuilder { + NewStageVisitorBuilder mapAlias(@Nonnull Function mapAliasVisitor); + } + + interface NewStageVisitorBuilder { + OptionalStageVisitorBuilder new_(@Nonnull IntFunction newVisitor); + } + + interface OptionalStageVisitorBuilder { + OptionalAliasStageVisitorBuilder optional(@Nonnull Function, T> optionalVisitor); + } + + interface OptionalAliasStageVisitorBuilder { + SetStageVisitorBuilder optionalAlias( + @Nonnull Function optionalAliasVisitor); + } + + interface SetStageVisitorBuilder { + SetAliasStageVisitorBuilder set(@Nonnull Function, T> setVisitor); + } + + interface SetAliasStageVisitorBuilder { + StringExampleStageVisitorBuilder setAlias( + @Nonnull Function setAliasVisitor); + } + + interface StringExampleStageVisitorBuilder { + ThisFieldIsAnIntegerStageVisitorBuilder stringExample( + @Nonnull Function stringExampleVisitor); + } + + interface ThisFieldIsAnIntegerStageVisitorBuilder { + Unknown_StageVisitorBuilder thisFieldIsAnInteger(@Nonnull IntFunction thisFieldIsAnIntegerVisitor); + } + + interface Unknown_StageVisitorBuilder { + UnknownStageVisitorBuilder unknown_(@Nonnull IntFunction unknown_Visitor); + } + + interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + interface Completed_StageVisitorBuilder { + Visitor build(); + } +} diff --git a/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionWithUnknownString.java b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionWithUnknownString.java new file mode 100644 index 000000000..0d840b2a6 --- /dev/null +++ b/conjure-java-core/src/test/resources/sealedunions/withvisitors/com/palantir/product/UnionWithUnknownString.java @@ -0,0 +1,234 @@ +package withvisitors.com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Generated; +import javax.annotation.Nonnull; + +@Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnionWithUnknownString.Unknown.class) +@JsonSubTypes(@JsonSubTypes.Type(Unknown_.class)) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface UnionWithUnknownString { + static UnionWithUnknownString unknown_(String value) { + return new Unknown_(value); + } + + static UnionWithUnknownString unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + case "unknown": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: unknown"); + default: + return new Unknown(type, Collections.singletonMap(type, value)); + } + } + + default Known throwOnUnknown() { + if (this instanceof Unknown) { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'Union' union", SafeArg.of("type", ((Unknown) this).getType())); + } else { + return (Known) this; + } + } + + void accept(Visitor visitor); + + sealed interface Known permits Unknown_ {} + + @JsonTypeName("unknown") + record Unknown_(String value) implements UnionWithUnknownString, Known { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + Unknown_(@JsonSetter("unknown") @Nonnull String value) { + Preconditions.checkNotNull(value, "unknown_ cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "unknown"; + } + + @JsonProperty("unknown") + private String getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown_(value); + } + + @Override + public String toString() { + return "Unknown_{value: " + value + '}'; + } + } + + final class Unknown implements UnionWithUnknownString { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private Unknown(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private Unknown(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Unknown && equalTo((Unknown) other)); + } + + private boolean equalTo(Unknown other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "Unknown{type: " + type + ", value: " + value + '}'; + } + } + + interface Visitor { + T visitUnknown_(String value); + + T visitUnknown(@Safe String unknownType, Object unknownValue); + + /** + * @Deprecated - prefer Java 17 pattern matching switch expressions. + */ + @Deprecated + static Unknown_StageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + final class VisitorBuilder + implements Unknown_StageVisitorBuilder, UnknownStageVisitorBuilder, Completed_StageVisitorBuilder { + private Function unknown_Visitor; + + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public UnknownStageVisitorBuilder unknown_(@Nonnull Function unknown_Visitor) { + Preconditions.checkNotNull(unknown_Visitor, "unknown_Visitor cannot be null"); + this.unknown_Visitor = unknown_Visitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'UnionWithUnknownString' union", + SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final Function unknown_Visitor = this.unknown_Visitor; + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitUnknown_(String value) { + return unknown_Visitor.apply(value); + } + + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + interface Unknown_StageVisitorBuilder { + UnknownStageVisitorBuilder unknown_(@Nonnull Function unknown_Visitor); + } + + interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + interface Completed_StageVisitorBuilder { + Visitor build(); + } +} diff --git a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java index 826ab95de..25cad8426 100644 --- a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java +++ b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java @@ -209,6 +209,12 @@ public static final class GenerateCommand implements Runnable { description = "Union visitors expose the values of unknowns in addition to their types.") private boolean unionsWithUnknownValues; + @CommandLine.Option( + names = "--sealedUnions", + defaultValue = "false", + description = "Generates sealed interfaces for union types.") + private boolean sealedUnions; + @SuppressWarnings("unused") @CommandLine.Unmatched private List unmatchedOptions; @@ -277,6 +283,7 @@ CliConfiguration getConfiguration() { .excludeEmptyOptionals(excludeEmptyOptionals) .excludeEmptyCollections(excludeEmptyCollections) .unionsWithUnknownValues(unionsWithUnknownValues) + .sealedUnions(sealedUnions) .build()) .build(); } diff --git a/gradle.properties b/gradle.properties index 7a0ad515d..fa6081706 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,10 @@ systemProp.org.gradle.internal.http.socketTimeout=600000 systemProp.org.gradle.internal.http.connectionTimeout=600000 org.gradle.parallel=true + +# Trying to make Java17 features work (https://github.com/palantir/palantir-java-format/issues/548) +org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ No newline at end of file diff --git a/readme.md b/readme.md index 70b7f473c..cea07279d 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,8 @@ The recommended way to use conjure-java is via a build tool like [gradle-conjure Generate POJOs that by default will fail to deserialize collections with null values --useStagedBuilders Generates compile-time safe builders to ensure all required attributes are set. + --sealedUnions + Generates sealed interfaces for union types. ### Known Tag Values diff --git a/versions.lock b/versions.lock index c36d6c023..8e887f294 100644 --- a/versions.lock +++ b/versions.lock @@ -11,6 +11,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3 (4 constraints: 253f com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.3 (2 constraints: 2b2b94af) com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3 (2 constraints: 2b2b94af) com.fasterxml.jackson.module:jackson-module-afterburner:2.13.3 (2 constraints: 472b27b6) +com.github.FabricMC:javapoet:d9b7dba158 (1 constraints: f50b65f7) com.github.ben-manes.caffeine:caffeine:3.1.1 (10 constraints: 39b0aad1) com.google.auto:auto-common:1.2.1 (2 constraints: ec16041a) com.google.code.findbugs:jsr305:3.0.2 (20 constraints: a13e998f) @@ -36,7 +37,7 @@ com.palantir.dialogue:dialogue-core:3.62.0 (3 constraints: 763d30e0) com.palantir.dialogue:dialogue-futures:3.62.0 (3 constraints: c833d9bc) com.palantir.dialogue:dialogue-serde:3.62.0 (3 constraints: b42e52a1) com.palantir.dialogue:dialogue-target:3.62.0 (7 constraints: 7976e34f) -com.palantir.goethe:goethe:0.7.0 (1 constraints: 09050036) +com.palantir.goethe:goethe:0.8.0 (1 constraints: 0a050336) com.palantir.human-readable-types:human-readable-types:1.5.0 (1 constraints: 0805ff35) com.palantir.refreshable:refreshable:2.2.0 (2 constraints: 642473b7) com.palantir.ri:resource-identifier:2.4.0 (3 constraints: be246df2) @@ -55,7 +56,6 @@ com.palantir.tritium:tritium-api:0.48.0 (2 constraints: 391fa2bd) com.palantir.tritium:tritium-core:0.48.0 (1 constraints: 441050a2) com.palantir.tritium:tritium-metrics:0.48.0 (5 constraints: 7b5cea30) com.palantir.tritium:tritium-registry:0.48.0 (10 constraints: 8cc37d6e) -com.squareup:javapoet:1.13.0 (2 constraints: 2b113eee) commons-codec:commons-codec:1.15 (1 constraints: 0d13c328) info.picocli:picocli:4.6.3 (1 constraints: 0f051436) io.dropwizard.metrics:metrics-core:4.1.2 (19 constraints: d52d542d) diff --git a/versions.props b/versions.props index 9286ce2eb..273624816 100644 --- a/versions.props +++ b/versions.props @@ -16,7 +16,6 @@ com.palantir.tokens:* = 3.14.0 com.palantir.tracing:* = 6.11.0 com.palantir.websecurity:dropwizard-web-security = 1.1.0 com.squareup.okhttp3:* = 3.14.1 -com.squareup:javapoet = 1.13.0 info.picocli:picocli = 4.6.3 io.dropwizard.metrics:metrics-core = 4.1.0 io.dropwizard:dropwizard-* = 2.0.2 @@ -35,7 +34,7 @@ org.junit.jupiter:* = 5.8.2 org.junit.vintage:* = 5.8.2 org.mockito:* = 4.6.1 org.slf4j:* = 1.7.36 -com.palantir.goethe:* = 0.7.0 +com.palantir.goethe:* = 0.8.0 com.github.stefanbirkner:system-lambda = 1.2.0 com.palantir.human-readable-types:* = 1.5.0 org.derive4j:* = 1.1.1