diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e549..27a5dfa420f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-rc-3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/apollo-annotations/api/apollo-annotations.api b/libraries/apollo-annotations/api/apollo-annotations.api index 66563d5f7da..721b436cbc3 100644 --- a/libraries/apollo-annotations/api/apollo-annotations.api +++ b/libraries/apollo-annotations/api/apollo-annotations.api @@ -8,6 +8,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 da18bae67b6..e9db1af0056 100644 --- a/libraries/apollo-annotations/api/apollo-annotations.klib.api +++ b/libraries/apollo-annotations/api/apollo-annotations.klib.api @@ -21,6 +21,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 927faea3f99..dd3fffad196 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 @@ -40,9 +40,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" @@ -60,11 +58,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 a1809823c71..5cb38970b1f 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 @@ -23,7 +23,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.InputObjectAdapterBuilder import com.apollographql.apollo.compiler.codegen.kotlin.schema.InputObjectBuilder @@ -49,7 +49,6 @@ import com.apollographql.apollo.compiler.generateMethodsKotlin import com.apollographql.apollo.compiler.ir.DefaultIrSchema import com.apollographql.apollo.compiler.ir.IrOperations import com.apollographql.apollo.compiler.ir.IrSchema -import com.apollographql.apollo.compiler.ir.IrTargetObject import com.apollographql.apollo.compiler.maybeTransform import com.apollographql.apollo.compiler.operationoutput.OperationOutput import com.apollographql.apollo.compiler.operationoutput.findOperationId @@ -179,7 +178,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 9caef705743..a7c4b6c2bb0 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 @@ -15,10 +15,6 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy * Symbols can be [ClassName] or [MemberName] */ internal object KotlinSymbols { - val ExecutableSchemaBuilder = ClassName(ClassNames.apolloExecutionPackageName, "ExecutableSchema", "Builder") - val Resolver = ClassName(ClassNames.apolloExecutionPackageName, "Resolver") - val ResolveInfo = ClassName(ClassNames.apolloExecutionPackageName, "ResolveInfo") - val Roots = ClassName(ClassNames.apolloExecutionPackageName, "Roots") val Schema = ClassName(ClassNames.apolloAstPackageName, "Schema") val ObjectType = ClassNames.ObjectType.toKotlinPoetClassName() val ObjectTypeBuilder = ClassNames.ObjectTypeBuilder.toKotlinPoetClassName() @@ -105,6 +101,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") @@ -114,7 +111,6 @@ internal object KotlinSymbols { val errorAware = MemberName(apolloApiPackageName, "errorAware") val readTypename = MemberName(apolloApiJsonPackageName, "readTypename") val buildData = MemberName(apolloApiPackageName, "buildData") - val GlobalBuilder = MemberName(apolloApiPackageName, "GlobalBuilder") val assertOneOf = MemberName(apolloApiPackageName, "assertOneOf") val missingField = MemberName(apolloApiPackageName, "missingField") val FieldResult = ClassNames.FieldResult.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..6c92d873bd4 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. + * safeValueOf 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..6a8b4d2c88f 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. + * safeValueOf 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 d35d3415d7f..9a6fbc11b3e 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 202583 -aggregate-kotlin-responseBased 65280 +aggregate-all 202606 +aggregate-kotlin-responseBased 65303 aggregate-kotlin-operationBased 41281 aggregate-kotlin-compat 0 aggregate-java-operationBased 96022 @@ -193,12 +193,12 @@ kotlin-responseBased-root_query_fragment kotlin-responseBased-typename_always_first 534 java-operationBased-arguments_hardcoded 531 kotlin-operationBased-fragment_with_multiple_fieldsets 528 +kotlin-responseBased-enum_field 527 kotlin-responseBased-input_object_oneof 527 java-operationBased-antlr_tokens 521 kotlin-operationBased-operationbased2_ex7 521 java-operationBased-subscriptions 520 kotlin-operationBased-typename_always_first 517 -kotlin-responseBased-enum_field 516 kotlin-operationBased-path_vs_flat_accessors 513 kotlin-responseBased-hero_name 507 kotlin-operationBased-interface_on_interface 506 @@ -235,8 +235,8 @@ kotlin-responseBased-starships kotlin-operationBased-inline_fragment_simple 399 kotlin-responseBased-java8annotation 397 kotlin-responseBased-antlr_tokens 391 +kotlin-responseBased-enums_as_sealed 385 kotlin-responseBased-subscriptions 385 -kotlin-responseBased-enums_as_sealed 373 kotlin-responseBased-case_sensitive_enum 342 kotlin-responseBased-operation_id_generator 342 kotlin-responseBased-merged_include 340 diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt index bc98cc8f34f..f6658fea8c3 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt @@ -623,14 +623,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 @@ -642,7 +644,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 @@ -655,8 +657,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 d81a9a5bfaf..6f85856b288 100644 --- a/tests/enums/build.gradle.kts +++ b/tests/enums/build.gradle.kts @@ -23,12 +23,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 + } }