From 780120b56744262008cf16f10e04e74107bca640 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 8 Nov 2024 14:51:21 +0100 Subject: [PATCH 1/5] [compiler] Add Enum.KNOWN__ as an intermediary interface (#6248) * Add Enum.KNOWN__ as an intermediary interface * use present * update test fixtures * Update libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt Co-authored-by: Benoit 'BoD' Lubek * Update libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt Co-authored-by: Benoit 'BoD' Lubek --------- Co-authored-by: Benoit 'BoD' Lubek --- .../api/apollo-annotations.api | 3 + .../api/apollo-annotations.klib.api | 4 + .../ApolloPrivateEnumConstructor.kt | 13 ++ .../apollo/compiler/codegen/Identifiers.kt | 11 +- .../compiler/codegen/kotlin/KotlinCodegen.kt | 4 +- .../compiler/codegen/kotlin/KotlinSymbols.kt | 1 + ...der.kt => EnumAsSealedInterfaceBuilder.kt} | 124 ++++++++++-------- .../enum_field/type/Gravity.kt.expected | 61 +++++---- .../enums_as_sealed/type/Enum.kt.expected | 54 +++++--- .../src/test/graphql/com/example/measurements | 8 +- .../apollo/gradle/api/Service.kt | 18 +-- tests/enums/build.gradle.kts | 4 +- .../enums/src/main/graphql/operation.graphql | 4 + tests/enums/src/main/graphql/schema.graphqls | 8 ++ tests/enums/src/test/kotlin/test/EnumsTest.kt | 23 ++++ 15 files changed, 217 insertions(+), 123 deletions(-) create mode 100644 libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt rename libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/{EnumAsSealedBuilder.kt => EnumAsSealedInterfaceBuilder.kt} (62%) diff --git a/libraries/apollo-annotations/api/apollo-annotations.api b/libraries/apollo-annotations/api/apollo-annotations.api index 3885423aa6f..0c8f44e5e2a 100644 --- a/libraries/apollo-annotations/api/apollo-annotations.api +++ b/libraries/apollo-annotations/api/apollo-annotations.api @@ -38,6 +38,9 @@ public abstract interface annotation class com/apollographql/apollo/annotations/ public abstract interface annotation class com/apollographql/apollo/annotations/ApolloInternal : java/lang/annotation/Annotation { } +public abstract interface annotation class com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor : java/lang/annotation/Annotation { +} + public abstract interface annotation class com/apollographql/apollo/annotations/ApolloRequiresOptIn : java/lang/annotation/Annotation { } diff --git a/libraries/apollo-annotations/api/apollo-annotations.klib.api b/libraries/apollo-annotations/api/apollo-annotations.klib.api index df4e0bb814e..dabb4822633 100644 --- a/libraries/apollo-annotations/api/apollo-annotations.klib.api +++ b/libraries/apollo-annotations/api/apollo-annotations.klib.api @@ -57,6 +57,10 @@ open annotation class com.apollographql.apollo.annotations/ApolloInternal : kotl constructor () // com.apollographql.apollo.annotations/ApolloInternal.|(){}[0] } +open annotation class com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor : kotlin/Annotation { // com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor|null[0] + constructor () // com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor.|(){}[0] +} + open annotation class com.apollographql.apollo.annotations/ApolloRequiresOptIn : kotlin/Annotation { // com.apollographql.apollo.annotations/ApolloRequiresOptIn|null[0] constructor () // com.apollographql.apollo.annotations/ApolloRequiresOptIn.|(){}[0] } diff --git a/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt b/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt new file mode 100644 index 00000000000..cfcf551feb1 --- /dev/null +++ b/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt @@ -0,0 +1,13 @@ +package com.apollographql.apollo.annotations + +/** + * Kotlin has no static factory functions like Java so we rely on an OptIn marker to prevent public usage. + * See https://youtrack.jetbrains.com/issue/KT-19400/Allow-access-to-private-members-between-nested-classes-of-the-same-class + */ +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "The `__UNKNOWN` constructor is public for technical reasons only. Use `${'$'}YourEnum.safeValueOf(String)` instead." +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CONSTRUCTOR) +annotation class ApolloPrivateEnumConstructor \ No newline at end of file diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt index 5ecc1246724..440a2a8c79e 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt @@ -41,9 +41,7 @@ internal object Identifier { const val copy = "copy" const val Data = "Data" - const val cacheKeyForObject = "cacheKeyForObject" const val field = "field" - const val __map = "__map" const val __path = "__path" const val __fields = "__fields" @@ -63,11 +61,16 @@ internal object Identifier { const val knownValues = "knownValues" const val knownEntries = "knownEntries" - // extra underscores at the end to prevent potential name clashes + /** + * UNKNOWN__ and KNOWN__ should probably have been __UNKNOWN because GraphQL reserves the leading __ but it's too late now. + * + * All in all it's not too bad because typing 'U', 'N', ... is usually more intuitive and in the very unlikely event that + * there is a name clash, it can always be resolved with `@targetName` + */ const val UNKNOWN__ = "UNKNOWN__" + const val KNOWN__ = "KNOWN__" const val rawValue = "rawValue" const val types = "types" - const val testResolver = "testResolver" const val block = "block" const val resolver = "resolver" const val newBuilder = "newBuilder" diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt index 1fc654fe945..eb52cd9a844 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt @@ -28,7 +28,7 @@ import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationSele import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationVariablesAdapterBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.CustomScalarAdaptersBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsEnumBuilder -import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsSealedBuilder +import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsSealedInterfaceBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumResponseAdapterBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.InlineClassBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.InputObjectAdapterBuilder @@ -190,7 +190,7 @@ internal object KotlinCodegen { } irSchema.irEnums.forEach { irEnum -> if (sealedClassesForEnumsMatching.any { Regex(it).matches(irEnum.name) }) { - builders.add(EnumAsSealedBuilder(context, irEnum)) + builders.add(EnumAsSealedInterfaceBuilder(context, irEnum)) } else { builders.add(EnumAsEnumBuilder(context, irEnum, addUnknownForEnums)) } diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinSymbols.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinSymbols.kt index ba5e13cfcda..0ab0bc4d8dc 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinSymbols.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinSymbols.kt @@ -112,6 +112,7 @@ internal object KotlinSymbols { val ApolloAdaptableWith = ClassName(ClassNames.apolloAnnotationsPackageName, "ApolloAdaptableWith") val ApolloExperimental = ClassName(ClassNames.apolloAnnotationsPackageName, "ApolloExperimental") + val ApolloPrivateEnumConstructor = ClassName(ClassNames.apolloAnnotationsPackageName, "ApolloPrivateEnumConstructor") val JsExport = ClassName("kotlin.js", "JsExport") val ExecutableDefinition = ClassNames.ExecutableDefinition.toKotlinPoetClassName() diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedBuilder.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt similarity index 62% rename from libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedBuilder.kt rename to libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt index ea469b65898..e6ec3080086 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedBuilder.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt @@ -1,6 +1,10 @@ package com.apollographql.apollo.compiler.codegen.kotlin.schema import com.apollographql.apollo.compiler.codegen.Identifier +import com.apollographql.apollo.compiler.codegen.Identifier.KNOWN__ +import com.apollographql.apollo.compiler.codegen.Identifier.UNKNOWN__ +import com.apollographql.apollo.compiler.codegen.Identifier.rawValue +import com.apollographql.apollo.compiler.codegen.Identifier.safeValueOf import com.apollographql.apollo.compiler.codegen.kotlin.CgFile import com.apollographql.apollo.compiler.codegen.kotlin.CgFileBuilder import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSchemaContext @@ -14,6 +18,7 @@ import com.apollographql.apollo.compiler.codegen.kotlin.schema.util.typeProperty import com.apollographql.apollo.compiler.codegen.typePackageName import com.apollographql.apollo.compiler.internal.escapeKotlinReservedWordInSealedClass import com.apollographql.apollo.compiler.ir.IrEnum +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FunSpec @@ -21,11 +26,12 @@ import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.buildCodeBlock import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.withIndent -internal class EnumAsSealedBuilder( +internal class EnumAsSealedInterfaceBuilder( private val context: KotlinSchemaContext, private val enum: IrEnum, ) : CgFileBuilder { @@ -50,20 +56,23 @@ internal class EnumAsSealedBuilder( return CgFile( packageName = packageName, fileName = simpleName, - typeSpecs = listOf(enum.toSealedClassTypeSpec(), enum.unknownClassTypeSpec()) + typeSpecs = listOf(enum.toSealedInterfaceTypeSpec()) ) } - private fun IrEnum.toSealedClassTypeSpec(): TypeSpec { + private fun IrEnum.toSealedInterfaceTypeSpec(): TypeSpec { return TypeSpec.interfaceBuilder(simpleName) .maybeAddDescription(description) - // XXX: can an enum be made deprecated (and not only its values) ? .addModifiers(KModifier.SEALED) - .addProperty(rawValuePropertySpec) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .build() + ) .addType(companionTypeSpec()) .addTypes(values.map { value -> - value.toObjectTypeSpec(selfClassName) + value.toObjectTypeSpec() }) + .addType(knownValueTypeSpec()) .addType(unknownValueTypeSpec()) .build() } @@ -76,12 +85,12 @@ internal class EnumAsSealedBuilder( .build() } - private fun IrEnum.Value.toObjectTypeSpec(superClass: TypeName): TypeSpec { + private fun IrEnum.Value.toObjectTypeSpec(): TypeSpec { return TypeSpec.objectBuilder(targetName.escapeKotlinReservedWordInSealedClass()) .maybeAddDeprecation(deprecationReason) .maybeAddDescription(description) .maybeAddRequiresOptIn(context.resolver, optInFeature) - .addSuperinterface(superClass) + .addSuperinterface(selfClassName.nestedClass(KNOWN__)) .addProperty( PropertySpec.builder("rawValue", KotlinSymbols.String) .addModifiers(KModifier.OVERRIDE) @@ -91,71 +100,99 @@ internal class EnumAsSealedBuilder( .build() } - private fun IrEnum.unknownValueTypeSpec(): TypeSpec { - return TypeSpec.interfaceBuilder("UNKNOWN__") - .addKdoc("An enum value that wasn't known at compile time.") + private fun IrEnum.knownValueTypeSpec(): TypeSpec { + return TypeSpec.interfaceBuilder(KNOWN__) + .addKdoc("An enum value that is known at build time.") .addSuperinterface(selfClassName) - .addProperty(unknownValueRawValuePropertySpec) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .build() + ) + .addModifiers(KModifier.SEALED) + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) .build() } - private fun IrEnum.unknownClassTypeSpec(): TypeSpec { - return TypeSpec.classBuilder("UNKNOWN__${simpleName}") - .addSuperinterface(unknownValueInterfaceName()) - .primaryConstructor(unknownValuePrimaryConstructorSpec) - .addProperty(unknownValueRawValuePropertySpecWithInitializer) - .addModifiers(KModifier.PRIVATE) + private fun IrEnum.unknownValueTypeSpec(): TypeSpec { + return TypeSpec.classBuilder(UNKNOWN__) + .addKdoc("An enum value that isn't known at build time.") + .addSuperinterface(selfClassName) + .primaryConstructor( + FunSpec.constructorBuilder() + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.ApolloPrivateEnumConstructor).build()) + .addParameter(rawValue, KotlinSymbols.String) + .build() + ) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .initializer(rawValue) + .build() + ) + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) .addFunction( FunSpec.builder("equals") .addModifiers(KModifier.OVERRIDE) .addParameter(ParameterSpec("other", KotlinSymbols.Any.copy(nullable = true))) .returns(KotlinSymbols.Boolean) - .addCode("if (other !is %T) return false\n", unknownValueClassName()) - .addCode("return this.rawValue == other.rawValue") + .addCode("if (other !is $UNKNOWN__) return false\n",) + .addCode("return this.$rawValue == other.rawValue") .build() ) .addFunction( FunSpec.builder("hashCode") .addModifiers(KModifier.OVERRIDE) .returns(KotlinSymbols.Int) - .addCode("return this.rawValue.hashCode()") + .addCode("return this.$rawValue.hashCode()") .build() ) .addFunction( FunSpec.builder("toString") .addModifiers(KModifier.OVERRIDE) .returns(KotlinSymbols.String) - .addCode("return \"UNKNOWN__(${'$'}rawValue)\"") + .addCode("return \"$UNKNOWN__(${'$'}$rawValue)\"") .build() ) .build() } private fun IrEnum.safeValueOfFunSpec(): FunSpec { - return FunSpec.builder(Identifier.safeValueOf) + return FunSpec.builder(safeValueOf) .addKdoc( - "Returns the [%T] that represents the specified [rawValue].\n" + - "Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly.\n", + """ + Returns an instance of [%T] representing [$rawValue]. + + The returned value may be an instance of [$UNKNOWN__] if the enum value is not known at build time. + You may want to update your schema instead of calling this function directly. + """.trimIndent(), selfClassName ) .addSuppressions(enum.values.any { it.deprecationReason != null }) .maybeAddOptIn(context.resolver, enum.values) - .addParameter("rawValue", KotlinSymbols.String) + .addParameter(rawValue, KotlinSymbols.String) .returns(selfClassName) - .beginControlFlow("return when(rawValue)") + .beginControlFlow("return when($rawValue)") .addCode( values .map { CodeBlock.of("%S -> %T", it.name, it.valueClassName()) } .joinToCode(separator = "\n", suffix = "\n") ) - .addCode("else -> %T(rawValue)\n", unknownValueClassName()) + .addCode(buildCodeBlock { + add("else -> {\n") + withIndent { + add("@%T(%T::class)\n", KotlinSymbols.OptIn, KotlinSymbols.ApolloPrivateEnumConstructor) + add("$UNKNOWN__($rawValue)\n") + } + add("}\n") + }) .endControlFlow() .build() } private fun IrEnum.knownValuesFunSpec(): FunSpec { return FunSpec.builder(Identifier.knownValues) - .addKdoc("Returns all [%T] known at compile time", selfClassName) + .addKdoc("Returns all [%T] known at build time", selfClassName) .addSuppressions(enum.values.any { it.deprecationReason != null }) .maybeAddOptIn(context.resolver, enum.values) .returns(KotlinSymbols.Array.parameterizedBy(selfClassName)) @@ -179,31 +216,4 @@ internal class EnumAsSealedBuilder( return ClassName(selfClassName.packageName, selfClassName.simpleName, targetName.escapeKotlinReservedWordInSealedClass()) } - private fun unknownValueInterfaceName(): ClassName { - return ClassName(selfClassName.packageName, selfClassName.simpleName, "UNKNOWN__") - } - - private fun unknownValueClassName(): ClassName { - return ClassName(selfClassName.packageName, "UNKNOWN__${selfClassName.simpleName}") - } - - private val unknownValuePrimaryConstructorSpec = - FunSpec.constructorBuilder() - .addParameter("rawValue", KotlinSymbols.String) - .build() - - private val unknownValueRawValuePropertySpec = - PropertySpec.builder("rawValue", KotlinSymbols.String) - .addModifiers(KModifier.OVERRIDE) - .build() - - private val unknownValueRawValuePropertySpecWithInitializer = - PropertySpec.builder("rawValue", KotlinSymbols.String) - .addModifiers(KModifier.OVERRIDE) - .initializer("rawValue") - .build() - - private val rawValuePropertySpec = - PropertySpec.builder("rawValue", KotlinSymbols.String) - .build() } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected b/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected index 3c12a4bca16..ad0f787b0c5 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected +++ b/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected @@ -5,13 +5,14 @@ // package com.example.enum_field.type +import com.apollographql.apollo.annotations.ApolloPrivateEnumConstructor import com.apollographql.apollo.api.EnumType import kotlin.Any import kotlin.Array import kotlin.Boolean import kotlin.Deprecated import kotlin.Int -import kotlin.String +import kotlin.OptIn import kotlin.Suppress internal sealed interface Gravity { @@ -22,8 +23,10 @@ internal sealed interface Gravity { EnumType("Gravity", listOf("TOP", "CENTER", "BOTTOM", "bottom", "is", "type", "String", "field")) /** - * Returns the [Gravity] that represents the specified [rawValue]. - * Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly. + * Returns an instance of [Gravity] representing [rawValue]. + * + * The returned value may be an instance of [UNKNOWN__] if the enum value is not known at build time. + * You may want to update your schema instead of calling this function directly. */ @Suppress("DEPRECATION") public fun safeValueOf(rawValue: kotlin.String): Gravity = when(rawValue) { @@ -35,11 +38,14 @@ internal sealed interface Gravity { "type" -> type_ "String" -> String "field" -> `field` - else -> UNKNOWN__Gravity(rawValue) + else -> { + @OptIn(ApolloPrivateEnumConstructor::class) + UNKNOWN__(rawValue) + } } /** - * Returns all [Gravity] known at compile time + * Returns all [Gravity] known at build time */ @Suppress("DEPRECATION") public fun knownValues(): Array = arrayOf( @@ -53,56 +59,61 @@ internal sealed interface Gravity { `field`) } - public object TOP : Gravity { + public object TOP : KNOWN__ { override val rawValue: kotlin.String = "TOP" } - public object CENTER : Gravity { + public object CENTER : KNOWN__ { override val rawValue: kotlin.String = "CENTER" } - public object BOTTOM : Gravity { + public object BOTTOM : KNOWN__ { override val rawValue: kotlin.String = "BOTTOM" } @Deprecated(message = "use BOTTOM instead") - public object bottom : Gravity { + public object bottom : KNOWN__ { override val rawValue: kotlin.String = "bottom" } - public object `is` : Gravity { + public object `is` : KNOWN__ { override val rawValue: kotlin.String = "is" } - public object type_ : Gravity { + public object type_ : KNOWN__ { override val rawValue: kotlin.String = "type" } - public object String : Gravity { + public object String : KNOWN__ { override val rawValue: kotlin.String = "String" } - public object `field` : Gravity { + public object `field` : KNOWN__ { override val rawValue: kotlin.String = "field" } /** - * An enum value that wasn't known at compile time. + * An enum value that is known at build time. */ - public interface UNKNOWN__ : Gravity { + @Suppress("ClassName") + public sealed interface KNOWN__ : Gravity { override val rawValue: kotlin.String } -} -private class UNKNOWN__Gravity( - override val rawValue: String, -) : Gravity.UNKNOWN__ { - override fun equals(other: Any?): Boolean { - if (other !is UNKNOWN__Gravity) return false - return this.rawValue == other.rawValue - } + /** + * An enum value that isn't known at build time. + */ + @Suppress("ClassName") + public class UNKNOWN__ @ApolloPrivateEnumConstructor constructor( + override val rawValue: kotlin.String, + ) : Gravity { + override fun equals(other: Any?): Boolean { + if (other !is UNKNOWN__) return false + return this.rawValue == other.rawValue + } - override fun hashCode(): Int = this.rawValue.hashCode() + override fun hashCode(): Int = this.rawValue.hashCode() - override fun toString(): String = "UNKNOWN__($rawValue)" + override fun toString(): kotlin.String = "UNKNOWN__($rawValue)" + } } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected b/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected index 49c471e1b9a..fc7ff7dad28 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected +++ b/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected @@ -5,12 +5,14 @@ // package com.example.enums_as_sealed.type +import com.apollographql.apollo.annotations.ApolloPrivateEnumConstructor import com.apollographql.apollo.api.EnumType import kotlin.Any import kotlin.Array import kotlin.Boolean import kotlin.Deprecated import kotlin.Int +import kotlin.OptIn import kotlin.String import kotlin.Suppress @@ -21,8 +23,10 @@ public sealed interface Enum { public val type: EnumType = EnumType("Enum", listOf("north", "North", "NORTH", "SOUTH", "type")) /** - * Returns the [Enum] that represents the specified [rawValue]. - * Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly. + * Returns an instance of [Enum] representing [rawValue]. + * + * The returned value may be an instance of [UNKNOWN__] if the enum value is not known at build time. + * You may want to update your schema instead of calling this function directly. */ @Suppress("DEPRECATION") public fun safeValueOf(rawValue: String): Enum = when(rawValue) { @@ -31,11 +35,14 @@ public sealed interface Enum { "NORTH" -> NORTH "SOUTH" -> SOUTH "type" -> type_ - else -> UNKNOWN__Enum(rawValue) + else -> { + @OptIn(ApolloPrivateEnumConstructor::class) + UNKNOWN__(rawValue) + } } /** - * Returns all [Enum] known at compile time + * Returns all [Enum] known at build time */ @Suppress("DEPRECATION") public fun knownValues(): Array = arrayOf( @@ -47,44 +54,49 @@ public sealed interface Enum { } @Deprecated(message = "No longer supported") - public object north : Enum { + public object north : KNOWN__ { override val rawValue: String = "north" } @Deprecated(message = "No longer supported") - public object North : Enum { + public object North : KNOWN__ { override val rawValue: String = "North" } - public object NORTH : Enum { + public object NORTH : KNOWN__ { override val rawValue: String = "NORTH" } - public object SOUTH : Enum { + public object SOUTH : KNOWN__ { override val rawValue: String = "SOUTH" } - public object type_ : Enum { + public object type_ : KNOWN__ { override val rawValue: String = "type" } /** - * An enum value that wasn't known at compile time. + * An enum value that is known at build time. */ - public interface UNKNOWN__ : Enum { + @Suppress("ClassName") + public sealed interface KNOWN__ : Enum { override val rawValue: String } -} -private class UNKNOWN__Enum( - override val rawValue: String, -) : Enum.UNKNOWN__ { - override fun equals(other: Any?): Boolean { - if (other !is UNKNOWN__Enum) return false - return this.rawValue == other.rawValue - } + /** + * An enum value that isn't known at build time. + */ + @Suppress("ClassName") + public class UNKNOWN__ @ApolloPrivateEnumConstructor constructor( + override val rawValue: String, + ) : Enum { + override fun equals(other: Any?): Boolean { + if (other !is UNKNOWN__) return false + return this.rawValue == other.rawValue + } - override fun hashCode(): Int = this.rawValue.hashCode() + override fun hashCode(): Int = this.rawValue.hashCode() - override fun toString(): String = "UNKNOWN__($rawValue)" + override fun toString(): String = "UNKNOWN__($rawValue)" + } } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/measurements b/libraries/apollo-compiler/src/test/graphql/com/example/measurements index 6f3fb517538..ca0c3ebd631 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/measurements +++ b/libraries/apollo-compiler/src/test/graphql/com/example/measurements @@ -2,8 +2,8 @@ // If you updated the codegen and test fixtures, you should commit this file too. Test: Total LOC: -aggregate-all 201251 -aggregate-kotlin-responseBased 63247 +aggregate-all 201274 +aggregate-kotlin-responseBased 63270 aggregate-kotlin-operationBased 41359 aggregate-kotlin-compat 0 aggregate-java-operationBased 96645 @@ -206,9 +206,9 @@ java-operationBased-merged_include java-operationBased-operation_id_generator 455 kotlin-operationBased-path_vs_flat_accessors 451 kotlin-responseBased-hero_name 445 +kotlin-responseBased-enum_field 444 kotlin-responseBased-interface_always_nested 435 kotlin-responseBased-deprecation 434 -kotlin-responseBased-enum_field 433 kotlin-operationBased-root_query_fragment 427 kotlin-responseBased-inline_fragment_for_non_optional_field 426 kotlin-responseBased-hero_name_query_long_name 425 @@ -239,9 +239,9 @@ kotlin-responseBased-subscriptions kotlin-responseBased-java8annotation 335 kotlin-responseBased-antlr_tokens 329 java-operationBased-java_hashcode 304 +kotlin-responseBased-enums_as_sealed 302 kotlin-responseBased-operation_id_generator 300 kotlin-responseBased-merged_include 298 -kotlin-responseBased-enums_as_sealed 290 kotlin-responseBased-big_query 275 kotlin-responseBased-case_sensitive_enum 259 kotlin-operationBased-companion 255 diff --git a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt index e383cc08368..ce21b8548e3 100644 --- a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt +++ b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt @@ -510,14 +510,16 @@ interface Service { val debugDir: DirectoryProperty /** - * A list of [Regex] patterns for GraphQL enums that should be generated as Kotlin sealed classes instead of the default Kotlin enums. + * A list of [Regex] patterns for GraphQL enums that should be generated as a Kotlin sealed interface. * - * Use this if you want your client to have access to the rawValue of the enum. This can be useful if new GraphQL enums are added but - * the client was compiled against an older schema that doesn't have knowledge of the new enums. + * This provides several benefits over the default of mapping GraphQL enums to Kotlin enums: + * - the client can access the string value of unknown values (enum values added on the server after the client has been compiled). + * - it introduces an intermediate `KNOWN__` type that does not contain the unknown value for the cases where you want to map all unknown values to a known one. + * - it's harder to create instances of `UNKNOWN__` values, making it more explicit that those values are dangerous to be used as input. * - * Only valid when [generateKotlinModels] is `true` + * Only valid when [generateKotlinModels] is `true`. * - * Default: emptyList() + * Default: `emptyList()` */ val sealedClassesForEnumsMatching: ListProperty @@ -529,7 +531,7 @@ interface Service { * Use this if you want your client to have access to the rawValue of the enum. This can be useful if new GraphQL enums are added but * the client was compiled against an older schema that doesn't have knowledge of the new enums. * - * Default: listOf(".*") + * Default: `listOf(".*")` */ val classesForEnumsMatching: ListProperty @@ -542,8 +544,8 @@ interface Service { * * You can pass the special value "none" to disable adding an annotation. * If you're using a custom annotation, it must be able to target: - * - AnnotationTarget.PROPERTY - * - AnnotationTarget.CLASS + * - [AnnotationTarget.PROPERTY] + * - [AnnotationTarget.CLASS] * * Default: "none" */ diff --git a/tests/enums/build.gradle.kts b/tests/enums/build.gradle.kts index b9a72d7523a..bec30bc024b 100644 --- a/tests/enums/build.gradle.kts +++ b/tests/enums/build.gradle.kts @@ -22,12 +22,12 @@ apollo { service("kotlin19") { packageName.set("enums.kotlin19") - sealedClassesForEnumsMatching.set(listOf(".*avity", "FooSealed")) + sealedClassesForEnumsMatching.set(listOf(".*avity", "FooSealed", "Color")) } service("java") { packageName.set("enums.java") - classesForEnumsMatching.set(listOf(".*avity", "FooClass")) + classesForEnumsMatching.set(listOf(".*avity", "FooClass", "Color")) generateKotlinModels.set(false) outputDirConnection { connectToJavaSourceSet("main") diff --git a/tests/enums/src/main/graphql/operation.graphql b/tests/enums/src/main/graphql/operation.graphql index 8b05a607372..ae4122b8193 100644 --- a/tests/enums/src/main/graphql/operation.graphql +++ b/tests/enums/src/main/graphql/operation.graphql @@ -6,3 +6,7 @@ query GetEnums { fooClass fooEnum } + +query GetColor { + color +} \ No newline at end of file diff --git a/tests/enums/src/main/graphql/schema.graphqls b/tests/enums/src/main/graphql/schema.graphqls index 730f4534e2c..c42aa043089 100644 --- a/tests/enums/src/main/graphql/schema.graphqls +++ b/tests/enums/src/main/graphql/schema.graphqls @@ -5,6 +5,7 @@ type Query { fooSealed: FooSealed fooClass: FooClass fooEnum: FooEnum + color: Color! } enum Direction { @@ -64,3 +65,10 @@ enum FooClass { # not renamed in extra.graphqls, will be renamed automatically type, } + +#See https://github.com/apollographql/apollo-kotlin/issues/6243 +enum Color { + BLUEBERRY, + CHERRY + CANDY +} \ No newline at end of file diff --git a/tests/enums/src/test/kotlin/test/EnumsTest.kt b/tests/enums/src/test/kotlin/test/EnumsTest.kt index 7c1077da4f2..e97de1829bf 100644 --- a/tests/enums/src/test/kotlin/test/EnumsTest.kt +++ b/tests/enums/src/test/kotlin/test/EnumsTest.kt @@ -5,6 +5,7 @@ import enums.kotlin15.type.Foo import enums.kotlin15.type.FooEnum import enums.kotlin15.type.FooSealed import enums.kotlin15.type.Gravity +import enums.kotlin19.type.Color import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -114,4 +115,26 @@ class EnumsTest { Gravity.knownValues().toList() ) } + + /** + * This is only used to check it compiles properly + */ + @Suppress("unused") + fun foo(color: Color) { + when (color.unwrap()) { + Color.BLUEBERRY -> TODO() + Color.CANDY -> TODO() + Color.CHERRY -> TODO() + } + } + + /** + * Turns a maybe unknown color value into a known one + */ + private fun Color.unwrap(): Color.KNOWN__ = when (this) { + is Color.UNKNOWN__ -> Color.CANDY + // Sadly cannot use `else ->` here so we use explicit branches + // See https://youtrack.jetbrains.com/issue/KT-18950/Smart-Cast-should-work-within-else-branch-for-sealed-subclasses + is Color.KNOWN__ -> this + } } From 70d5e8d5f01cd624489a3fc4d8fe6330bc3c2498 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 4 Jul 2025 16:13:45 +0200 Subject: [PATCH 2/5] Introduce generateApolloEnums --- .../apollo-compiler/api/apollo-compiler.api | 8 +- .../apollographql/apollo/compiler/Options.kt | 12 + .../apollo/compiler/codegen/Identifiers.kt | 2 + .../compiler/codegen/kotlin/KotlinCodegen.kt | 11 +- .../kotlin/schema/EnumAsApolloEnumBuilder.kt | 228 ++++++++++++++++++ .../schema/EnumAsApolloEnumSupportBuilder.kt | 72 ++++++ .../schema/EnumAsSealedInterfaceBuilder.kt | 122 +++++----- .../enum_field/type/Gravity.kt.expected | 61 ++--- .../enums_as_sealed/type/Enum.kt.expected | 54 ++--- .../src/test/graphql/com/example/measurements | 8 +- .../api/apollo-gradle-plugin-tasks.api | 4 +- .../gradle/task/apolloGenerateOptions.kt | 2 + .../api/apollo-gradle-plugin.api | 3 +- .../apollo/gradle/api/Service.kt | 8 + .../gradle/internal/DefaultApolloExtension.kt | 1 + tests/enums/build.gradle.kts | 6 +- .../src/test/kotlin/test/ApolloEnumTest.kt | 48 ++++ tests/enums/src/test/kotlin/test/EnumsTest.kt | 23 -- 18 files changed, 503 insertions(+), 170 deletions(-) create mode 100644 libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumBuilder.kt create mode 100644 libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumSupportBuilder.kt create mode 100644 tests/enums/src/test/kotlin/test/ApolloEnumTest.kt diff --git a/libraries/apollo-compiler/api/apollo-compiler.api b/libraries/apollo-compiler/api/apollo-compiler.api index 2e5cabba11f..8ae2a5f561d 100644 --- a/libraries/apollo-compiler/api/apollo-compiler.api +++ b/libraries/apollo-compiler/api/apollo-compiler.api @@ -103,12 +103,13 @@ public final class com/apollographql/apollo/compiler/CodegenMetadata$Companion { public final class com/apollographql/apollo/compiler/CodegenOptions : com/apollographql/apollo/compiler/OperationsCodegenOptions, com/apollographql/apollo/compiler/SchemaCodegenOptions { public static final field Companion Lcom/apollographql/apollo/compiler/CodegenOptions$Companion; - public fun (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;)V + public fun (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;)V public fun getAddDefaultArgumentForInputObjects ()Ljava/lang/Boolean; public fun getAddJvmOverloads ()Ljava/lang/Boolean; public fun getAddUnknownForEnums ()Ljava/lang/Boolean; public fun getClassesForEnumsMatching ()Ljava/util/List; public fun getDecapitalizeFields ()Ljava/lang/Boolean; + public fun getGenerateApolloEnums ()Ljava/lang/Boolean; public fun getGenerateAsInternal ()Ljava/lang/Boolean; public fun getGenerateFilterNotNull ()Ljava/lang/Boolean; public fun getGenerateFragmentImplementations ()Ljava/lang/Boolean; @@ -352,6 +353,7 @@ public abstract interface class com/apollographql/apollo/compiler/KotlinCodegenO public abstract fun getAddDefaultArgumentForInputObjects ()Ljava/lang/Boolean; public abstract fun getAddJvmOverloads ()Ljava/lang/Boolean; public abstract fun getAddUnknownForEnums ()Ljava/lang/Boolean; + public abstract fun getGenerateApolloEnums ()Ljava/lang/Boolean; public abstract fun getGenerateAsInternal ()Ljava/lang/Boolean; public abstract fun getGenerateFilterNotNull ()Ljava/lang/Boolean; public abstract fun getGenerateInputBuilders ()Ljava/lang/Boolean; @@ -393,8 +395,8 @@ public final class com/apollographql/apollo/compiler/OptionsKt { public static final field MODELS_OPERATION_BASED Ljava/lang/String; public static final field MODELS_OPERATION_BASED_WITH_INTERFACES Ljava/lang/String; public static final field MODELS_RESPONSE_BASED Ljava/lang/String; - public static final fun buildCodegenOptions (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo/compiler/CodegenOptions; - public static synthetic fun buildCodegenOptions$default (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/apollographql/apollo/compiler/CodegenOptions; + public static final fun buildCodegenOptions (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo/compiler/CodegenOptions; + public static synthetic fun buildCodegenOptions$default (Lcom/apollographql/apollo/compiler/TargetLanguage;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Lcom/apollographql/apollo/compiler/JavaNullable;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/apollographql/apollo/compiler/CodegenOptions; public static final fun buildIrOptions (Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;)Lcom/apollographql/apollo/compiler/IrOptions; public static synthetic fun buildIrOptions$default (Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;ILjava/lang/Object;)Lcom/apollographql/apollo/compiler/IrOptions; public static final fun validate (Lcom/apollographql/apollo/compiler/CodegenOptions;)V diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/Options.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/Options.kt index 04eb2e3aca5..ad1fde7e9b9 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/Options.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/Options.kt @@ -448,6 +448,14 @@ interface KotlinCodegenOpt { */ val addUnknownForEnums: Boolean? + /** + * Whether to generate enums as ApolloEnum. + * + * Experimental, see https://github.com/apollographql/apollo-kotlin/issues/6243. + */ + @ApolloExperimental + val generateApolloEnums: Boolean? + /** * Whether to add default arguments for input objects. */ @@ -521,6 +529,7 @@ class CodegenOptions( override val generateSchema: Boolean?, override val generatedSchemaName: String?, override val sealedClassesForEnumsMatching: List?, + override val generateApolloEnums: Boolean?, override val generateAsInternal: Boolean?, override val addUnknownForEnums: Boolean?, override val addDefaultArgumentForInputObjects: Boolean?, @@ -548,6 +557,7 @@ fun buildCodegenOptions( sealedClassesForEnumsMatching: List? = null, generateAsInternal: Boolean? = null, addUnknownForEnums: Boolean? = null, + generateApolloEnums: Boolean? = null, addDefaultArgumentForInputObjects: Boolean? = null, generateFilterNotNull: Boolean? = null, generateInputBuilders: Boolean? = null, @@ -573,6 +583,7 @@ fun buildCodegenOptions( sealedClassesForEnumsMatching = sealedClassesForEnumsMatching, generateAsInternal = generateAsInternal, addUnknownForEnums = addUnknownForEnums, + generateApolloEnums = generateApolloEnums, addDefaultArgumentForInputObjects = addDefaultArgumentForInputObjects, generateFilterNotNull = generateFilterNotNull, generateInputBuilders = generateInputBuilders, @@ -703,6 +714,7 @@ internal val defaultAddUnkownForEnums = true internal val defaultAddDefaultArgumentForInputObjects = true internal val defaultCodegenModels = "operationBased" internal val defaultTargetLanguage = TargetLanguage.KOTLIN_1_9 +internal val defaultGenerateApolloEnums = false internal fun defaultTargetLanguage(targetLanguage: TargetLanguage?, upstreamCodegenMetadata: List): TargetLanguage { val upstreamTargetLanguage = upstreamCodegenMetadata.map { it.targetLanguage }.distinct().run { diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt index 440a2a8c79e..d950b27e7fe 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt @@ -69,6 +69,8 @@ internal object Identifier { */ const val UNKNOWN__ = "UNKNOWN__" const val KNOWN__ = "KNOWN__" + const val __Unknown = "__Unknown" + const val __Known = "__Known" const val rawValue = "rawValue" const val types = "types" const val block = "block" diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt index eb52cd9a844..675c7f7b65b 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/KotlinCodegen.kt @@ -27,6 +27,8 @@ import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationResp import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationSelectionsBuilder import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationVariablesAdapterBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.CustomScalarAdaptersBuilder +import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsApolloEnumBuilder +import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsApolloEnumSupportBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsEnumBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsSealedInterfaceBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumResponseAdapterBuilder @@ -43,6 +45,7 @@ import com.apollographql.apollo.compiler.codegen.kotlin.schema.asTargetClassName import com.apollographql.apollo.compiler.defaultAddDefaultArgumentForInputObjects import com.apollographql.apollo.compiler.defaultAddJvmOverloads import com.apollographql.apollo.compiler.defaultAddUnkownForEnums +import com.apollographql.apollo.compiler.defaultGenerateApolloEnums import com.apollographql.apollo.compiler.defaultGenerateAsInternal import com.apollographql.apollo.compiler.defaultGenerateFilterNotNull import com.apollographql.apollo.compiler.defaultGenerateFragmentImplementations @@ -161,6 +164,7 @@ internal object KotlinCodegen { val jsExport = codegenOptions.jsExport ?: defaultJsExport val requiresOptInAnnotation = codegenOptions.requiresOptInAnnotation ?: defaultRequiresOptInAnnotation val sealedClassesForEnumsMatching = codegenOptions.sealedClassesForEnumsMatching ?: defaultSealedClassesForEnumsMatching + val generateApolloEnums = codegenOptions.generateApolloEnums ?: defaultGenerateApolloEnums val addUnknownForEnums = codegenOptions.addUnknownForEnums ?: defaultAddUnkownForEnums val addDefaultArgumentForInputObjects = codegenOptions.addDefaultArgumentForInputObjects ?: defaultAddDefaultArgumentForInputObjects @@ -188,8 +192,13 @@ internal object KotlinCodegen { } builders.add(ScalarBuilder(context, irScalar, inlineClassBuilder?.className)) } + if (generateApolloEnums) { + builders.add(EnumAsApolloEnumSupportBuilder(context)) + } irSchema.irEnums.forEach { irEnum -> - if (sealedClassesForEnumsMatching.any { Regex(it).matches(irEnum.name) }) { + if(generateApolloEnums) { + builders.add(EnumAsApolloEnumBuilder(context, irEnum)) + } else if (sealedClassesForEnumsMatching.any { Regex(it).matches(irEnum.name) }) { builders.add(EnumAsSealedInterfaceBuilder(context, irEnum)) } else { builders.add(EnumAsEnumBuilder(context, irEnum, addUnknownForEnums)) diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumBuilder.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumBuilder.kt new file mode 100644 index 00000000000..d48a18783a8 --- /dev/null +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumBuilder.kt @@ -0,0 +1,228 @@ +package com.apollographql.apollo.compiler.codegen.kotlin.schema + +import com.apollographql.apollo.compiler.codegen.Identifier +import com.apollographql.apollo.compiler.codegen.Identifier.__Known +import com.apollographql.apollo.compiler.codegen.Identifier.__Unknown +import com.apollographql.apollo.compiler.codegen.Identifier.rawValue +import com.apollographql.apollo.compiler.codegen.Identifier.safeValueOf +import com.apollographql.apollo.compiler.codegen.kotlin.CgFile +import com.apollographql.apollo.compiler.codegen.kotlin.CgFileBuilder +import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSchemaContext +import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSymbols +import com.apollographql.apollo.compiler.codegen.kotlin.helpers.addSuppressions +import com.apollographql.apollo.compiler.codegen.kotlin.helpers.maybeAddDeprecation +import com.apollographql.apollo.compiler.codegen.kotlin.helpers.maybeAddDescription +import com.apollographql.apollo.compiler.codegen.kotlin.helpers.maybeAddOptIn +import com.apollographql.apollo.compiler.codegen.kotlin.helpers.maybeAddRequiresOptIn +import com.apollographql.apollo.compiler.codegen.kotlin.schema.util.typePropertySpec +import com.apollographql.apollo.compiler.codegen.typePackageName +import com.apollographql.apollo.compiler.internal.escapeKotlinReservedWordInSealedClass +import com.apollographql.apollo.compiler.ir.IrEnum +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.buildCodeBlock +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.withIndent + +internal class EnumAsApolloEnumBuilder( + private val context: KotlinSchemaContext, + private val enum: IrEnum, +) : CgFileBuilder { + private val layout = context.layout + private val packageName = layout.typePackageName() + private val simpleName = layout.schemaTypeName(enum.name) + + private val selfClassName: ClassName + get() = context.resolver.resolveSchemaType(enum.name) + + override fun prepare() { + context.resolver.registerSchemaType( + enum.name, + ClassName( + packageName, + simpleName + ) + ) + } + + override fun build(): CgFile { + return CgFile( + packageName = packageName, + fileName = simpleName, + typeSpecs = listOf(enum.toSealedInterfaceTypeSpec()) + ) + } + + private fun IrEnum.toSealedInterfaceTypeSpec(): TypeSpec { + return TypeSpec.interfaceBuilder(simpleName) + .maybeAddDescription(description) + .addSuperinterface( + ClassName(packageName.experimental(), "ApolloEnum") + .parameterizedBy( + selfClassName, + ClassName(packageName, simpleName, __Known) + ) + ) + .addModifiers(KModifier.SEALED) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .build() + ) + .addType(companionTypeSpec()) + .addTypes(values.map { value -> + value.toObjectTypeSpec() + }) + .addType(knownValueTypeSpec()) + .addType(unknownValueTypeSpec()) + .build() + } + + private fun IrEnum.companionTypeSpec(): TypeSpec { + return TypeSpec.companionObjectBuilder() + .addProperty(typePropertySpec()) + .addFunction(safeValueOfFunSpec()) + .addFunction(knownValuesFunSpec()) + .build() + } + + private fun IrEnum.Value.toObjectTypeSpec(): TypeSpec { + return TypeSpec.objectBuilder(targetName.escapeKotlinReservedWordInSealedClass()) + .maybeAddDeprecation(deprecationReason) + .maybeAddDescription(description) + .maybeAddRequiresOptIn(context.resolver, optInFeature) + .addSuperinterface(selfClassName.nestedClass(__Known)) + .addProperty( + PropertySpec.builder("rawValue", KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .initializer("%S", name) + .build() + ) + .build() + } + + private fun knownValueTypeSpec(): TypeSpec { + return TypeSpec.interfaceBuilder(__Known) + .addKdoc("An enum value that is known at build time.") + .addSuperinterface(selfClassName) + .addSuperinterface(ClassName(packageName.experimental(), "KnownEnum").parameterizedBy(selfClassName)) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .build() + ) + .addModifiers(KModifier.SEALED) + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) + .build() + } + + private fun unknownValueTypeSpec(): TypeSpec { + return TypeSpec.classBuilder(__Unknown) + .addKdoc("An enum value that isn't known at build time.") + .addSuperinterface(selfClassName) + .primaryConstructor( + FunSpec.constructorBuilder() + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.ApolloPrivateEnumConstructor).build()) + .addParameter(rawValue, KotlinSymbols.String) + .build() + ) + .addProperty( + PropertySpec.builder(rawValue, KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .initializer(rawValue) + .build() + ) + .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) + .addFunction( + FunSpec.builder("equals") + .addModifiers(KModifier.OVERRIDE) + .addParameter(ParameterSpec("other", KotlinSymbols.Any.copy(nullable = true))) + .returns(KotlinSymbols.Boolean) + .addCode("if (other !is $__Unknown) return false\n") + .addCode("return this.$rawValue == other.rawValue") + .build() + ) + .addFunction( + FunSpec.builder("hashCode") + .addModifiers(KModifier.OVERRIDE) + .returns(KotlinSymbols.Int) + .addCode("return this.$rawValue.hashCode()") + .build() + ) + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.OVERRIDE) + .returns(KotlinSymbols.String) + .addCode("return \"$__Unknown(${'$'}$rawValue)\"") + .build() + ) + .build() + } + + private fun IrEnum.safeValueOfFunSpec(): FunSpec { + return FunSpec.builder(safeValueOf) + .addKdoc( + """ + Returns an instance of [%T] representing [$rawValue]. + + The returned value may be an instance of [$__Unknown] if the enum value is not known at build time. + You may want to update your schema instead of calling this function directly. + """.trimIndent(), + selfClassName + ) + .addSuppressions(enum.values.any { it.deprecationReason != null }) + .maybeAddOptIn(context.resolver, enum.values) + .addParameter(rawValue, KotlinSymbols.String) + .returns(selfClassName) + .beginControlFlow("return when($rawValue)") + .addCode( + values + .map { CodeBlock.of("%S -> %T", it.name, it.valueClassName()) } + .joinToCode(separator = "\n", suffix = "\n") + ) + .addCode(buildCodeBlock { + add("else -> {\n") + withIndent { + add("@%T(%T::class)\n", KotlinSymbols.OptIn, KotlinSymbols.ApolloPrivateEnumConstructor) + add("$__Unknown($rawValue)\n") + } + add("}\n") + }) + .endControlFlow() + .build() + } + + private fun IrEnum.knownValuesFunSpec(): FunSpec { + return FunSpec.builder(Identifier.knownValues) + .addKdoc("Returns all [%T] known at build time", selfClassName) + .addSuppressions(enum.values.any { it.deprecationReason != null }) + .maybeAddOptIn(context.resolver, enum.values) + .returns(KotlinSymbols.Array.parameterizedBy(selfClassName)) + .addCode( + CodeBlock.builder() + .add("return arrayOf(\n") + .indent() + .add( + values.map { + CodeBlock.of("%T", it.valueClassName()) + }.joinToCode(",\n") + ) + .unindent() + .add(")\n") + .build() + ) + .build() + } + + private fun IrEnum.Value.valueClassName(): ClassName { + return ClassName(selfClassName.packageName, selfClassName.simpleName, targetName.escapeKotlinReservedWordInSealedClass()) + } + +} diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumSupportBuilder.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumSupportBuilder.kt new file mode 100644 index 00000000000..e2a282da2de --- /dev/null +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsApolloEnumSupportBuilder.kt @@ -0,0 +1,72 @@ +package com.apollographql.apollo.compiler.codegen.kotlin.schema + +import com.apollographql.apollo.compiler.codegen.Identifier.rawValue +import com.apollographql.apollo.compiler.codegen.kotlin.CgFile +import com.apollographql.apollo.compiler.codegen.kotlin.CgFileBuilder +import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSchemaContext +import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSymbols +import com.apollographql.apollo.compiler.codegen.typePackageName +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeVariableName + +/** + * Generates supporting symbols for ApolloEnum. + * This could have been `apollo-api` symbols but since everything is experimental, generating them + * only exposes the symbols for the actual users. Also because it will be generated in a dedicated + * package name, it allows removing/tweaking them more easily if needed. + */ +internal class EnumAsApolloEnumSupportBuilder( + context: KotlinSchemaContext, +) : CgFileBuilder { + private val layout = context.layout + private val packageName = layout.typePackageName().experimental() + private val simpleName = "apollo-enum" + + override fun prepare() { + } + + override fun build(): CgFile { + return CgFile( + packageName = packageName, + fileName = simpleName, + typeSpecs = listOf( + TypeSpec.interfaceBuilder("ApolloEnum") + .addTypeVariable(TypeVariableName("E")) + .addTypeVariable(TypeVariableName("K", ClassName(packageName, "KnownEnum").parameterizedBy(TypeVariableName("E")))) + .addProperty(PropertySpec.builder(rawValue, KotlinSymbols.String).build()) + .build(), + TypeSpec.interfaceBuilder("KnownEnum") + .addTypeVariable(TypeVariableName("E")) + .build(), + ), + funSpecs = listOf( + FunSpec.builder("knownOrDefault") + .addModifiers(KModifier.INLINE) + .addTypeVariable(TypeVariableName("E", ClassName(packageName, "ApolloEnum").parameterizedBy(TypeVariableName("E"), TypeVariableName("K")))) + .addTypeVariable(TypeVariableName("K", ClassName(packageName, "KnownEnum").parameterizedBy(TypeVariableName("E"))).copy(reified = true)) + .returns(TypeVariableName("K")) + .receiver(TypeVariableName("E")) + .addParameter(ParameterSpec.builder("default", LambdaTypeName.get(receiver = null, parameters = listOf(ParameterSpec.unnamed(TypeVariableName("E"))), returnType = TypeVariableName("K"))).build()) + .addCode("return if (this is K) this else default(this)\n") + .build(), + FunSpec.builder("knownOrNull") + .addModifiers(KModifier.INLINE) + .addTypeVariable(TypeVariableName("E", ClassName(packageName, "ApolloEnum").parameterizedBy(TypeVariableName("E"), TypeVariableName("K")))) + .addTypeVariable(TypeVariableName("K", ClassName(packageName, "KnownEnum").parameterizedBy(TypeVariableName("E"))).copy(reified = true)) + .returns(TypeVariableName("K").copy(nullable = true)) + .receiver(TypeVariableName("E")) + .addCode("return if (this is K) this else null\n") + .build(), + ) + ) + } +} + +internal fun String.experimental(): String = "$this.experimental" diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt index e6ec3080086..9dfe2cf503e 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/schema/EnumAsSealedInterfaceBuilder.kt @@ -1,10 +1,6 @@ package com.apollographql.apollo.compiler.codegen.kotlin.schema import com.apollographql.apollo.compiler.codegen.Identifier -import com.apollographql.apollo.compiler.codegen.Identifier.KNOWN__ -import com.apollographql.apollo.compiler.codegen.Identifier.UNKNOWN__ -import com.apollographql.apollo.compiler.codegen.Identifier.rawValue -import com.apollographql.apollo.compiler.codegen.Identifier.safeValueOf import com.apollographql.apollo.compiler.codegen.kotlin.CgFile import com.apollographql.apollo.compiler.codegen.kotlin.CgFileBuilder import com.apollographql.apollo.compiler.codegen.kotlin.KotlinSchemaContext @@ -18,7 +14,6 @@ import com.apollographql.apollo.compiler.codegen.kotlin.schema.util.typeProperty import com.apollographql.apollo.compiler.codegen.typePackageName import com.apollographql.apollo.compiler.internal.escapeKotlinReservedWordInSealedClass import com.apollographql.apollo.compiler.ir.IrEnum -import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FunSpec @@ -26,10 +21,9 @@ import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.buildCodeBlock import com.squareup.kotlinpoet.joinToCode -import com.squareup.kotlinpoet.withIndent internal class EnumAsSealedInterfaceBuilder( private val context: KotlinSchemaContext, @@ -56,23 +50,20 @@ internal class EnumAsSealedInterfaceBuilder( return CgFile( packageName = packageName, fileName = simpleName, - typeSpecs = listOf(enum.toSealedInterfaceTypeSpec()) + typeSpecs = listOf(enum.toSealedClassTypeSpec(), enum.unknownClassTypeSpec()) ) } - private fun IrEnum.toSealedInterfaceTypeSpec(): TypeSpec { + private fun IrEnum.toSealedClassTypeSpec(): TypeSpec { return TypeSpec.interfaceBuilder(simpleName) .maybeAddDescription(description) + // XXX: can an enum be made deprecated (and not only its values) ? .addModifiers(KModifier.SEALED) - .addProperty( - PropertySpec.builder(rawValue, KotlinSymbols.String) - .build() - ) + .addProperty(rawValuePropertySpec) .addType(companionTypeSpec()) .addTypes(values.map { value -> - value.toObjectTypeSpec() + value.toObjectTypeSpec(selfClassName) }) - .addType(knownValueTypeSpec()) .addType(unknownValueTypeSpec()) .build() } @@ -85,12 +76,12 @@ internal class EnumAsSealedInterfaceBuilder( .build() } - private fun IrEnum.Value.toObjectTypeSpec(): TypeSpec { + private fun IrEnum.Value.toObjectTypeSpec(superClass: TypeName): TypeSpec { return TypeSpec.objectBuilder(targetName.escapeKotlinReservedWordInSealedClass()) .maybeAddDeprecation(deprecationReason) .maybeAddDescription(description) .maybeAddRequiresOptIn(context.resolver, optInFeature) - .addSuperinterface(selfClassName.nestedClass(KNOWN__)) + .addSuperinterface(superClass) .addProperty( PropertySpec.builder("rawValue", KotlinSymbols.String) .addModifiers(KModifier.OVERRIDE) @@ -100,99 +91,71 @@ internal class EnumAsSealedInterfaceBuilder( .build() } - private fun IrEnum.knownValueTypeSpec(): TypeSpec { - return TypeSpec.interfaceBuilder(KNOWN__) - .addKdoc("An enum value that is known at build time.") + private fun IrEnum.unknownValueTypeSpec(): TypeSpec { + return TypeSpec.interfaceBuilder("UNKNOWN__") + .addKdoc("An enum value that wasn't known at compile time.") .addSuperinterface(selfClassName) - .addProperty( - PropertySpec.builder(rawValue, KotlinSymbols.String) - .addModifiers(KModifier.OVERRIDE) - .build() - ) - .addModifiers(KModifier.SEALED) - .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) + .addProperty(unknownValueRawValuePropertySpec) .build() } - private fun IrEnum.unknownValueTypeSpec(): TypeSpec { - return TypeSpec.classBuilder(UNKNOWN__) - .addKdoc("An enum value that isn't known at build time.") - .addSuperinterface(selfClassName) - .primaryConstructor( - FunSpec.constructorBuilder() - .addAnnotation(AnnotationSpec.builder(KotlinSymbols.ApolloPrivateEnumConstructor).build()) - .addParameter(rawValue, KotlinSymbols.String) - .build() - ) - .addProperty( - PropertySpec.builder(rawValue, KotlinSymbols.String) - .addModifiers(KModifier.OVERRIDE) - .initializer(rawValue) - .build() - ) - .addAnnotation(AnnotationSpec.builder(KotlinSymbols.Suppress).addMember("%S", "ClassName").build()) + private fun IrEnum.unknownClassTypeSpec(): TypeSpec { + return TypeSpec.classBuilder("UNKNOWN__${simpleName}") + .addSuperinterface(unknownValueInterfaceName()) + .primaryConstructor(unknownValuePrimaryConstructorSpec) + .addProperty(unknownValueRawValuePropertySpecWithInitializer) + .addModifiers(KModifier.PRIVATE) .addFunction( FunSpec.builder("equals") .addModifiers(KModifier.OVERRIDE) .addParameter(ParameterSpec("other", KotlinSymbols.Any.copy(nullable = true))) .returns(KotlinSymbols.Boolean) - .addCode("if (other !is $UNKNOWN__) return false\n",) - .addCode("return this.$rawValue == other.rawValue") + .addCode("if (other !is %T) return false\n", unknownValueClassName()) + .addCode("return this.rawValue == other.rawValue") .build() ) .addFunction( FunSpec.builder("hashCode") .addModifiers(KModifier.OVERRIDE) .returns(KotlinSymbols.Int) - .addCode("return this.$rawValue.hashCode()") + .addCode("return this.rawValue.hashCode()") .build() ) .addFunction( FunSpec.builder("toString") .addModifiers(KModifier.OVERRIDE) .returns(KotlinSymbols.String) - .addCode("return \"$UNKNOWN__(${'$'}$rawValue)\"") + .addCode("return \"UNKNOWN__(${'$'}rawValue)\"") .build() ) .build() } private fun IrEnum.safeValueOfFunSpec(): FunSpec { - return FunSpec.builder(safeValueOf) + return FunSpec.builder(Identifier.safeValueOf) .addKdoc( - """ - Returns an instance of [%T] representing [$rawValue]. - - The returned value may be an instance of [$UNKNOWN__] if the enum value is not known at build time. - You may want to update your schema instead of calling this function directly. - """.trimIndent(), + "Returns the [%T] that represents the specified [rawValue].\n" + + "Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly.\n", selfClassName ) .addSuppressions(enum.values.any { it.deprecationReason != null }) .maybeAddOptIn(context.resolver, enum.values) - .addParameter(rawValue, KotlinSymbols.String) + .addParameter("rawValue", KotlinSymbols.String) .returns(selfClassName) - .beginControlFlow("return when($rawValue)") + .beginControlFlow("return when(rawValue)") .addCode( values .map { CodeBlock.of("%S -> %T", it.name, it.valueClassName()) } .joinToCode(separator = "\n", suffix = "\n") ) - .addCode(buildCodeBlock { - add("else -> {\n") - withIndent { - add("@%T(%T::class)\n", KotlinSymbols.OptIn, KotlinSymbols.ApolloPrivateEnumConstructor) - add("$UNKNOWN__($rawValue)\n") - } - add("}\n") - }) + .addCode("else -> %T(rawValue)\n", unknownValueClassName()) .endControlFlow() .build() } private fun IrEnum.knownValuesFunSpec(): FunSpec { return FunSpec.builder(Identifier.knownValues) - .addKdoc("Returns all [%T] known at build time", selfClassName) + .addKdoc("Returns all [%T] known at compile time", selfClassName) .addSuppressions(enum.values.any { it.deprecationReason != null }) .maybeAddOptIn(context.resolver, enum.values) .returns(KotlinSymbols.Array.parameterizedBy(selfClassName)) @@ -216,4 +179,31 @@ internal class EnumAsSealedInterfaceBuilder( return ClassName(selfClassName.packageName, selfClassName.simpleName, targetName.escapeKotlinReservedWordInSealedClass()) } + private fun unknownValueInterfaceName(): ClassName { + return ClassName(selfClassName.packageName, selfClassName.simpleName, "UNKNOWN__") + } + + private fun unknownValueClassName(): ClassName { + return ClassName(selfClassName.packageName, "UNKNOWN__${selfClassName.simpleName}") + } + + private val unknownValuePrimaryConstructorSpec = + FunSpec.constructorBuilder() + .addParameter("rawValue", KotlinSymbols.String) + .build() + + private val unknownValueRawValuePropertySpec = + PropertySpec.builder("rawValue", KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .build() + + private val unknownValueRawValuePropertySpecWithInitializer = + PropertySpec.builder("rawValue", KotlinSymbols.String) + .addModifiers(KModifier.OVERRIDE) + .initializer("rawValue") + .build() + + private val rawValuePropertySpec = + PropertySpec.builder("rawValue", KotlinSymbols.String) + .build() } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected b/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected index ad0f787b0c5..3c12a4bca16 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected +++ b/libraries/apollo-compiler/src/test/graphql/com/example/enum_field/kotlin/responseBased/enum_field/type/Gravity.kt.expected @@ -5,14 +5,13 @@ // package com.example.enum_field.type -import com.apollographql.apollo.annotations.ApolloPrivateEnumConstructor import com.apollographql.apollo.api.EnumType import kotlin.Any import kotlin.Array import kotlin.Boolean import kotlin.Deprecated import kotlin.Int -import kotlin.OptIn +import kotlin.String import kotlin.Suppress internal sealed interface Gravity { @@ -23,10 +22,8 @@ internal sealed interface Gravity { EnumType("Gravity", listOf("TOP", "CENTER", "BOTTOM", "bottom", "is", "type", "String", "field")) /** - * Returns an instance of [Gravity] representing [rawValue]. - * - * The returned value may be an instance of [UNKNOWN__] if the enum value is not known at build time. - * You may want to update your schema instead of calling this function directly. + * Returns the [Gravity] that represents the specified [rawValue]. + * Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly. */ @Suppress("DEPRECATION") public fun safeValueOf(rawValue: kotlin.String): Gravity = when(rawValue) { @@ -38,14 +35,11 @@ internal sealed interface Gravity { "type" -> type_ "String" -> String "field" -> `field` - else -> { - @OptIn(ApolloPrivateEnumConstructor::class) - UNKNOWN__(rawValue) - } + else -> UNKNOWN__Gravity(rawValue) } /** - * Returns all [Gravity] known at build time + * Returns all [Gravity] known at compile time */ @Suppress("DEPRECATION") public fun knownValues(): Array = arrayOf( @@ -59,61 +53,56 @@ internal sealed interface Gravity { `field`) } - public object TOP : KNOWN__ { + public object TOP : Gravity { override val rawValue: kotlin.String = "TOP" } - public object CENTER : KNOWN__ { + public object CENTER : Gravity { override val rawValue: kotlin.String = "CENTER" } - public object BOTTOM : KNOWN__ { + public object BOTTOM : Gravity { override val rawValue: kotlin.String = "BOTTOM" } @Deprecated(message = "use BOTTOM instead") - public object bottom : KNOWN__ { + public object bottom : Gravity { override val rawValue: kotlin.String = "bottom" } - public object `is` : KNOWN__ { + public object `is` : Gravity { override val rawValue: kotlin.String = "is" } - public object type_ : KNOWN__ { + public object type_ : Gravity { override val rawValue: kotlin.String = "type" } - public object String : KNOWN__ { + public object String : Gravity { override val rawValue: kotlin.String = "String" } - public object `field` : KNOWN__ { + public object `field` : Gravity { override val rawValue: kotlin.String = "field" } /** - * An enum value that is known at build time. + * An enum value that wasn't known at compile time. */ - @Suppress("ClassName") - public sealed interface KNOWN__ : Gravity { + public interface UNKNOWN__ : Gravity { override val rawValue: kotlin.String } +} - /** - * An enum value that isn't known at build time. - */ - @Suppress("ClassName") - public class UNKNOWN__ @ApolloPrivateEnumConstructor constructor( - override val rawValue: kotlin.String, - ) : Gravity { - override fun equals(other: Any?): Boolean { - if (other !is UNKNOWN__) return false - return this.rawValue == other.rawValue - } +private class UNKNOWN__Gravity( + override val rawValue: String, +) : Gravity.UNKNOWN__ { + override fun equals(other: Any?): Boolean { + if (other !is UNKNOWN__Gravity) return false + return this.rawValue == other.rawValue + } - override fun hashCode(): Int = this.rawValue.hashCode() + override fun hashCode(): Int = this.rawValue.hashCode() - override fun toString(): kotlin.String = "UNKNOWN__($rawValue)" - } + override fun toString(): String = "UNKNOWN__($rawValue)" } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected b/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected index fc7ff7dad28..49c471e1b9a 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected +++ b/libraries/apollo-compiler/src/test/graphql/com/example/enums_as_sealed/kotlin/responseBased/enums_as_sealed/type/Enum.kt.expected @@ -5,14 +5,12 @@ // package com.example.enums_as_sealed.type -import com.apollographql.apollo.annotations.ApolloPrivateEnumConstructor import com.apollographql.apollo.api.EnumType import kotlin.Any import kotlin.Array import kotlin.Boolean import kotlin.Deprecated import kotlin.Int -import kotlin.OptIn import kotlin.String import kotlin.Suppress @@ -23,10 +21,8 @@ public sealed interface Enum { public val type: EnumType = EnumType("Enum", listOf("north", "North", "NORTH", "SOUTH", "type")) /** - * Returns an instance of [Enum] representing [rawValue]. - * - * The returned value may be an instance of [UNKNOWN__] if the enum value is not known at build time. - * You may want to update your schema instead of calling this function directly. + * Returns the [Enum] that represents the specified [rawValue]. + * Note: unknown values of [rawValue] will return [UNKNOWN__]. You may want to update your schema instead of calling this function directly. */ @Suppress("DEPRECATION") public fun safeValueOf(rawValue: String): Enum = when(rawValue) { @@ -35,14 +31,11 @@ public sealed interface Enum { "NORTH" -> NORTH "SOUTH" -> SOUTH "type" -> type_ - else -> { - @OptIn(ApolloPrivateEnumConstructor::class) - UNKNOWN__(rawValue) - } + else -> UNKNOWN__Enum(rawValue) } /** - * Returns all [Enum] known at build time + * Returns all [Enum] known at compile time */ @Suppress("DEPRECATION") public fun knownValues(): Array = arrayOf( @@ -54,49 +47,44 @@ public sealed interface Enum { } @Deprecated(message = "No longer supported") - public object north : KNOWN__ { + public object north : Enum { override val rawValue: String = "north" } @Deprecated(message = "No longer supported") - public object North : KNOWN__ { + public object North : Enum { override val rawValue: String = "North" } - public object NORTH : KNOWN__ { + public object NORTH : Enum { override val rawValue: String = "NORTH" } - public object SOUTH : KNOWN__ { + public object SOUTH : Enum { override val rawValue: String = "SOUTH" } - public object type_ : KNOWN__ { + public object type_ : Enum { override val rawValue: String = "type" } /** - * An enum value that is known at build time. + * An enum value that wasn't known at compile time. */ - @Suppress("ClassName") - public sealed interface KNOWN__ : Enum { + public interface UNKNOWN__ : Enum { override val rawValue: String } +} - /** - * An enum value that isn't known at build time. - */ - @Suppress("ClassName") - public class UNKNOWN__ @ApolloPrivateEnumConstructor constructor( - override val rawValue: String, - ) : Enum { - override fun equals(other: Any?): Boolean { - if (other !is UNKNOWN__) return false - return this.rawValue == other.rawValue - } +private class UNKNOWN__Enum( + override val rawValue: String, +) : Enum.UNKNOWN__ { + override fun equals(other: Any?): Boolean { + if (other !is UNKNOWN__Enum) return false + return this.rawValue == other.rawValue + } - override fun hashCode(): Int = this.rawValue.hashCode() + override fun hashCode(): Int = this.rawValue.hashCode() - override fun toString(): String = "UNKNOWN__($rawValue)" - } + override fun toString(): String = "UNKNOWN__($rawValue)" } diff --git a/libraries/apollo-compiler/src/test/graphql/com/example/measurements b/libraries/apollo-compiler/src/test/graphql/com/example/measurements index ca0c3ebd631..6f3fb517538 100644 --- a/libraries/apollo-compiler/src/test/graphql/com/example/measurements +++ b/libraries/apollo-compiler/src/test/graphql/com/example/measurements @@ -2,8 +2,8 @@ // If you updated the codegen and test fixtures, you should commit this file too. Test: Total LOC: -aggregate-all 201274 -aggregate-kotlin-responseBased 63270 +aggregate-all 201251 +aggregate-kotlin-responseBased 63247 aggregate-kotlin-operationBased 41359 aggregate-kotlin-compat 0 aggregate-java-operationBased 96645 @@ -206,9 +206,9 @@ java-operationBased-merged_include java-operationBased-operation_id_generator 455 kotlin-operationBased-path_vs_flat_accessors 451 kotlin-responseBased-hero_name 445 -kotlin-responseBased-enum_field 444 kotlin-responseBased-interface_always_nested 435 kotlin-responseBased-deprecation 434 +kotlin-responseBased-enum_field 433 kotlin-operationBased-root_query_fragment 427 kotlin-responseBased-inline_fragment_for_non_optional_field 426 kotlin-responseBased-hero_name_query_long_name 425 @@ -239,9 +239,9 @@ kotlin-responseBased-subscriptions kotlin-responseBased-java8annotation 335 kotlin-responseBased-antlr_tokens 329 java-operationBased-java_hashcode 304 -kotlin-responseBased-enums_as_sealed 302 kotlin-responseBased-operation_id_generator 300 kotlin-responseBased-merged_include 298 +kotlin-responseBased-enums_as_sealed 290 kotlin-responseBased-big_query 275 kotlin-responseBased-case_sensitive_enum 259 kotlin-operationBased-companion 255 diff --git a/libraries/apollo-gradle-plugin-tasks/api/apollo-gradle-plugin-tasks.api b/libraries/apollo-gradle-plugin-tasks/api/apollo-gradle-plugin-tasks.api index 33a21cbd0e9..e644a71cc23 100644 --- a/libraries/apollo-gradle-plugin-tasks/api/apollo-gradle-plugin-tasks.api +++ b/libraries/apollo-gradle-plugin-tasks/api/apollo-gradle-plugin-tasks.api @@ -51,11 +51,11 @@ public final class com/apollographql/apollo/gradle/task/ApolloGenerateIrOperatio public final class com/apollographql/apollo/gradle/task/ApolloGenerateOptionsEntryPoint { public static final field Companion Lcom/apollographql/apollo/gradle/task/ApolloGenerateOptionsEntryPoint$Companion; public fun ()V - public static final fun run (Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;ZLjava/lang/String;ZZLjava/io/File;Ljava/io/File;Ljava/io/File;Ljava/io/File;)V + public static final fun run (Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;ZLjava/lang/String;ZZLjava/io/File;Ljava/io/File;Ljava/io/File;Ljava/io/File;)V } public final class com/apollographql/apollo/gradle/task/ApolloGenerateOptionsEntryPoint$Companion { - public final fun run (Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;ZLjava/lang/String;ZZLjava/io/File;Ljava/io/File;Ljava/io/File;Ljava/io/File;)V + public final fun run (Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/List;ZLjava/lang/String;ZZLjava/io/File;Ljava/io/File;Ljava/io/File;Ljava/io/File;)V } public final class com/apollographql/apollo/gradle/task/ApolloGenerateOptionsKt { diff --git a/libraries/apollo-gradle-plugin-tasks/src/main/kotlin/com/apollographql/apollo/gradle/task/apolloGenerateOptions.kt b/libraries/apollo-gradle-plugin-tasks/src/main/kotlin/com/apollographql/apollo/gradle/task/apolloGenerateOptions.kt index 5de26b83c6d..778df3cd088 100644 --- a/libraries/apollo-gradle-plugin-tasks/src/main/kotlin/com/apollographql/apollo/gradle/task/apolloGenerateOptions.kt +++ b/libraries/apollo-gradle-plugin-tasks/src/main/kotlin/com/apollographql/apollo/gradle/task/apolloGenerateOptions.kt @@ -53,6 +53,7 @@ internal fun apolloGenerateOptions( classesForEnumsMatching: List?, // KotlinCodegenOptions sealedClassesForEnumsMatching: List?, + generateApolloEnums: Boolean?, generateAsInternal: Boolean?, generateInputBuilders: Boolean?, addJvmOverloads: Boolean?, @@ -136,6 +137,7 @@ internal fun apolloGenerateOptions( decapitalizeFields = decapitalizeFields, addDefaultArgumentForInputObjects = true, addUnknownForEnums = true, + generateApolloEnums = generateApolloEnums, packageName = packageName, rootPackageName = rootPackageName ).writeTo(codegenOptions) diff --git a/libraries/apollo-gradle-plugin/api/apollo-gradle-plugin.api b/libraries/apollo-gradle-plugin/api/apollo-gradle-plugin.api index 39a247432a3..e99dfc64638 100644 --- a/libraries/apollo-gradle-plugin/api/apollo-gradle-plugin.api +++ b/libraries/apollo-gradle-plugin/api/apollo-gradle-plugin.api @@ -148,6 +148,7 @@ public abstract interface class com/apollographql/apollo/gradle/api/Service { public abstract fun getFailOnWarnings ()Lorg/gradle/api/provider/Property; public abstract fun getFieldsOnDisjointTypesMustMerge ()Lorg/gradle/api/provider/Property; public abstract fun getFlattenModels ()Lorg/gradle/api/provider/Property; + public abstract fun getGenerateApolloEnums ()Lorg/gradle/api/provider/Property; public abstract fun getGenerateApolloMetadata ()Lorg/gradle/api/provider/Property; public abstract fun getGenerateAsInternal ()Lorg/gradle/api/provider/Property; public abstract fun getGenerateDataBuilders ()Lorg/gradle/api/provider/Property; @@ -238,6 +239,6 @@ public abstract interface class com/apollographql/apollo/gradle/api/Service$Outg } public final class com/apollographql/apollo/gradle/task/ApolloGenerateOptionsTaskKt { - public static synthetic fun registerApolloGenerateOptionsTask$default (Lorg/gradle/api/Project;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/gradle/api/file/FileCollection;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/file/FileCollection;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;IILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; + public static synthetic fun registerApolloGenerateOptionsTask$default (Lorg/gradle/api/Project;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/gradle/api/file/FileCollection;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/file/FileCollection;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;IILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; } diff --git a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt index ce21b8548e3..74cfcb36f9f 100644 --- a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt +++ b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt @@ -523,6 +523,14 @@ interface Service { */ val sealedClassesForEnumsMatching: ListProperty + /** + * Whether to generate enums as ApolloEnum. + * + * Experimental, see https://github.com/apollographql/apollo-kotlin/issues/6243. + */ + @ApolloExperimental + val generateApolloEnums: Property + /** * A list of [Regex] patterns for GraphQL enums that should be generated as Java classes. * diff --git a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/internal/DefaultApolloExtension.kt b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/internal/DefaultApolloExtension.kt index b52c72c622e..e72845693b7 100644 --- a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/internal/DefaultApolloExtension.kt +++ b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/internal/DefaultApolloExtension.kt @@ -405,6 +405,7 @@ abstract class DefaultApolloExtension( * KotlinCodegenOptions */ sealedClassesForEnumsMatching = service.sealedClassesForEnumsMatching, + generateApolloEnums = service.generateApolloEnums, generateAsInternal = service.generateAsInternal, generateInputBuilders = service.generateInputBuilders, addJvmOverloads = service.addJvmOverloads, diff --git a/tests/enums/build.gradle.kts b/tests/enums/build.gradle.kts index bec30bc024b..a9cb4365dad 100644 --- a/tests/enums/build.gradle.kts +++ b/tests/enums/build.gradle.kts @@ -22,7 +22,7 @@ apollo { service("kotlin19") { packageName.set("enums.kotlin19") - sealedClassesForEnumsMatching.set(listOf(".*avity", "FooSealed", "Color")) + sealedClassesForEnumsMatching.set(listOf(".*avity", "FooSealed")) } service("java") { @@ -33,6 +33,10 @@ apollo { connectToJavaSourceSet("main") } } + service("apollo") { + packageName.set("enums.apollo") + generateApolloEnums.set(true) + } } //kotlin { diff --git a/tests/enums/src/test/kotlin/test/ApolloEnumTest.kt b/tests/enums/src/test/kotlin/test/ApolloEnumTest.kt new file mode 100644 index 00000000000..342ac4cd46d --- /dev/null +++ b/tests/enums/src/test/kotlin/test/ApolloEnumTest.kt @@ -0,0 +1,48 @@ +package test + +import enums.apollo.type.Color +import enums.apollo.type.experimental.knownOrDefault +import enums.apollo.type.experimental.knownOrNull +import kotlin.test.Test +import kotlin.test.assertEquals + + +class ApolloEnumTest { + @Test + fun knownOrDefault() { + assertEquals(Color.BLUEBERRY, Color.safeValueOf("UNKNOWN").knownOrDefault { Color.BLUEBERRY }) + assertEquals(Color.CHERRY, Color.safeValueOf("CHERRY").knownOrDefault { Color.CHERRY }) + } + + @Test + fun knownOrNull() { + assertEquals(null, Color.safeValueOf("UNKNOWN").knownOrNull()) + assertEquals(Color.CHERRY, Color.safeValueOf("CHERRY").knownOrNull()) + } + + @Test + fun knownOrCandy() { + assertEquals(Color.CANDY, Color.safeValueOf("UNKNOWN").knownOrCandy()) + } + + /** + * This is only used to check it compiles properly + */ + fun doStuff(color: Color) { + when (color.knownOrCandy()) { + Color.BLUEBERRY -> TODO() + Color.CANDY -> TODO() + Color.CHERRY -> TODO() + } + } + + /** + * Turns a maybe unknown color value into a known one + */ + private fun Color.knownOrCandy(): Color.__Known = when (this) { + is Color.__Unknown -> Color.CANDY + // Sadly cannot use `else ->` here so we use explicit branches + // See https://youtrack.jetbrains.com/issue/KT-18950/Smart-Cast-should-work-within-else-branch-for-sealed-subclasses + is Color.__Known -> this + } +} \ No newline at end of file diff --git a/tests/enums/src/test/kotlin/test/EnumsTest.kt b/tests/enums/src/test/kotlin/test/EnumsTest.kt index e97de1829bf..7c1077da4f2 100644 --- a/tests/enums/src/test/kotlin/test/EnumsTest.kt +++ b/tests/enums/src/test/kotlin/test/EnumsTest.kt @@ -5,7 +5,6 @@ import enums.kotlin15.type.Foo import enums.kotlin15.type.FooEnum import enums.kotlin15.type.FooSealed import enums.kotlin15.type.Gravity -import enums.kotlin19.type.Color import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -115,26 +114,4 @@ class EnumsTest { Gravity.knownValues().toList() ) } - - /** - * This is only used to check it compiles properly - */ - @Suppress("unused") - fun foo(color: Color) { - when (color.unwrap()) { - Color.BLUEBERRY -> TODO() - Color.CANDY -> TODO() - Color.CHERRY -> TODO() - } - } - - /** - * Turns a maybe unknown color value into a known one - */ - private fun Color.unwrap(): Color.KNOWN__ = when (this) { - is Color.UNKNOWN__ -> Color.CANDY - // Sadly cannot use `else ->` here so we use explicit branches - // See https://youtrack.jetbrains.com/issue/KT-18950/Smart-Cast-should-work-within-else-branch-for-sealed-subclasses - is Color.KNOWN__ -> this - } } From 7881a3dd5bd8e09557442534e78b59b87a632c1d Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 4 Jul 2025 16:53:10 +0200 Subject: [PATCH 3/5] revert Kdoc --- .../com/apollographql/apollo/gradle/api/Service.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt index 74cfcb36f9f..d37a1997d90 100644 --- a/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt +++ b/libraries/apollo-gradle-plugin/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt @@ -510,16 +510,14 @@ interface Service { val debugDir: DirectoryProperty /** - * A list of [Regex] patterns for GraphQL enums that should be generated as a Kotlin sealed interface. + * A list of [Regex] patterns for GraphQL enums that should be generated as Kotlin sealed classes instead of the default Kotlin enums. * - * This provides several benefits over the default of mapping GraphQL enums to Kotlin enums: - * - the client can access the string value of unknown values (enum values added on the server after the client has been compiled). - * - it introduces an intermediate `KNOWN__` type that does not contain the unknown value for the cases where you want to map all unknown values to a known one. - * - it's harder to create instances of `UNKNOWN__` values, making it more explicit that those values are dangerous to be used as input. + * Use this if you want your client to have access to the rawValue of the enum. This can be useful if new GraphQL enums are added but + * the client was compiled against an older schema that doesn't have knowledge of the new enums. * - * Only valid when [generateKotlinModels] is `true`. + * Only valid when [generateKotlinModels] is `true` * - * Default: `emptyList()` + * Default: emptyList() */ val sealedClassesForEnumsMatching: ListProperty From d8df24ad77f52d7eadf0a73845f9cb64391e920b Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 4 Jul 2025 16:54:58 +0200 Subject: [PATCH 4/5] remove unused things --- .../apollo/annotations/ApolloPrivateEnumConstructor.kt | 2 +- .../com/apollographql/apollo/compiler/codegen/Identifiers.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt b/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt index cfcf551feb1..ac8aa0acc07 100644 --- a/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt +++ b/libraries/apollo-annotations/src/commonMain/kotlin/com/apollographql/apollo/annotations/ApolloPrivateEnumConstructor.kt @@ -6,7 +6,7 @@ package com.apollographql.apollo.annotations */ @RequiresOptIn( level = RequiresOptIn.Level.ERROR, - message = "The `__UNKNOWN` constructor is public for technical reasons only. Use `${'$'}YourEnum.safeValueOf(String)` instead." + message = "The `__Unknown` constructor is public for technical reasons only. Use `${'$'}YourEnum.safeValueOf(String)` instead." ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CONSTRUCTOR) diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt index d950b27e7fe..5971ee10a7f 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt @@ -62,13 +62,12 @@ internal object Identifier { const val knownEntries = "knownEntries" /** - * UNKNOWN__ and KNOWN__ should probably have been __UNKNOWN because GraphQL reserves the leading __ but it's too late now. + * UNKNOWN__should probably have been __UNKNOWN because GraphQL reserves the leading __ but it's too late now. * * All in all it's not too bad because typing 'U', 'N', ... is usually more intuitive and in the very unlikely event that * there is a name clash, it can always be resolved with `@targetName` */ const val UNKNOWN__ = "UNKNOWN__" - const val KNOWN__ = "KNOWN__" const val __Unknown = "__Unknown" const val __Known = "__Known" const val rawValue = "rawValue" From 6a72fa8a04f2da81a9be5b211fa3b5367cf7ee4d Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 4 Jul 2025 17:31:19 +0200 Subject: [PATCH 5/5] Update libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt Co-authored-by: Benoit 'BoD' Lubek --- .../com/apollographql/apollo/compiler/codegen/Identifiers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt index 5971ee10a7f..1c5e2dffec0 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/Identifiers.kt @@ -62,7 +62,7 @@ internal object Identifier { const val knownEntries = "knownEntries" /** - * UNKNOWN__should probably have been __UNKNOWN because GraphQL reserves the leading __ but it's too late now. + * UNKNOWN__ should probably have been __UNKNOWN because GraphQL reserves the leading __ but it's too late now. * * All in all it's not too bad because typing 'U', 'N', ... is usually more intuitive and in the very unlikely event that * there is a name clash, it can always be resolved with `@targetName`