Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compiler] Add Enum.KNOWN__ as an intermediary interface #6248

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions libraries/apollo-annotations/api/apollo-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}

Comment on lines +11 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces the Opt-in marker that we removed in d160e98.

The underlying reason is that Kotlin doesn't support static factory method (companion obejcts cannot access private constructors of nested classes).

The previous trick of extracting a public interface doesn't work with sealed classes because the compiler insists on having UNKNOWN__Impl be part of the exhaustive when statements and it's private in the file. Maybe a future version of the compiler could fix that but as of today I haven't found a way.

public abstract interface annotation class com/apollographql/apollo/annotations/ApolloRequiresOptIn : java/lang/annotation/Annotation {
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ open annotation class com.apollographql.apollo.annotations/ApolloInternal : kotl
constructor <init>() // com.apollographql.apollo.annotations/ApolloInternal.<init>|<init>(){}[0]
}

open annotation class com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor : kotlin/Annotation { // com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor|null[0]
constructor <init>() // com.apollographql.apollo.annotations/ApolloPrivateEnumConstructor.<init>|<init>(){}[0]
}

open annotation class com.apollographql.apollo.annotations/ApolloRequiresOptIn : kotlin/Annotation { // com.apollographql.apollo.annotations/ApolloRequiresOptIn|null[0]
constructor <init>() // com.apollographql.apollo.annotations/ApolloRequiresOptIn.<init>|<init>(){}[0]
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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`
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
*/
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")

Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,18 +18,20 @@ 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
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 {
Expand All @@ -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()
}
Expand All @@ -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)
Expand All @@ -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",
"""
$safeValueOf returns an instance of [%T] representing [$rawValue].
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved

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))
Expand All @@ -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()
}
Loading
Loading