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

Add inline classes support for scalars #6352

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ class Schema internal constructor(
@ApolloExperimental
const val FIELD_POLICY_PAGINATION_ARGS = "paginationArgs"

@ApolloExperimental
const val INLINE_CLASS = "inlineClass"

/**
* Parses the given [map] and creates a new [Schema].
* The [map] must come from a previous call to [toMap] to make sure the schema is valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,18 @@ fun GQLFieldDefinition.findSemanticNonNulls(schema: Schema): List<Int> {
}
return semanticNonNull.getArgumentValueOrDefault("levels", schema)!!.toListOfInt()
}

@ApolloInternal
fun GQLScalarTypeDefinition.findInlineClassCoercion(schema: Schema): String? =
directives.filter { schema.originalDirectiveName(it.name) == Schema.INLINE_CLASS }
.map {
it.arguments
.firstOrNull { it.name == "coercion" }
?.value
?.let { value ->
if (value !is GQLEnumValue) {
throw ConversionException("coercion must be an enum", it.sourceLocation)
}
value.value
}
}.firstOrNull()
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.apollographql.apollo.ast.internal.builtinsDefinitionsStr
import com.apollographql.apollo.ast.internal.ensureSchemaDefinition
import com.apollographql.apollo.ast.internal.kotlinLabsDefinitions_0_3
import com.apollographql.apollo.ast.internal.kotlinLabsDefinitions_0_4
import com.apollographql.apollo.ast.internal.kotlinLabsDefinitions_0_5
import com.apollographql.apollo.ast.internal.linkDefinitionsStr
import com.apollographql.apollo.ast.internal.nullabilityDefinitionsStr
import okio.Buffer
Expand Down Expand Up @@ -93,6 +94,8 @@ fun kotlinLabsDefinitions(version: String): List<GQLDefinition> {
"v0.2", "v0.3" -> kotlinLabsDefinitions_0_3
// v0.4 doesn't have `@nonnull`
"v0.4" -> kotlinLabsDefinitions_0_4
// v0.5 adds `@inlineClass`
"v0.5" -> kotlinLabsDefinitions_0_5
else -> error("kotlin_labs/$version definitions are not supported, please use $KOTLIN_LABS_VERSION")
})
}
Expand All @@ -107,6 +110,7 @@ fun builtinForeignSchemas(): List<ForeignSchema> {
ForeignSchema("kotlin_labs", "v0.2", kotlinLabsDefinitions("v0.2"), listOf("optional", "nonnull")),
ForeignSchema("kotlin_labs", "v0.3", kotlinLabsDefinitions("v0.3"), listOf("optional", "nonnull")),
ForeignSchema("kotlin_labs", "v0.4", kotlinLabsDefinitions("v0.4"), listOf("optional")),
ForeignSchema("kotlin_labs", "v0.5", kotlinLabsDefinitions("v0.5"), listOf("optional")),
ForeignSchema("nullability", "v0.4", nullabilityDefinitions("v0.4"), listOf("catch")),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, options: SchemaVal
mergedScope.validateUnions()
mergedScope.validateInputObjects()
mergedScope.validateCatch(mergedSchemaDefinition)
mergedScope.validateScalars()

val keyFields = mergedScope.validateAndComputeKeyFields()
val connectionTypes = mergedScope.computeConnectionTypes()
Expand Down Expand Up @@ -654,6 +655,14 @@ private fun ValidationScope.validateInputObjects() {
}
}

private fun ValidationScope.validateScalars() {
typeDefinitions.values.filterIsInstance<GQLScalarTypeDefinition>().forEach { i ->
validateDirectives(i.directives, i) {
issues.add(it.constContextError())
}
}
}

private fun ValidationScope.validateNoIntrospectionNames() {
// 3.3 All types and directives defined within a schema must not have a name which begins with "__"
(typeDefinitions.values + directiveDefinitions.values).forEach { definition ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,22 @@ on OBJECT
| UNION
| SCALAR
| INPUT_OBJECT

""".trimIndent()

internal val kotlinLabsDefinitions_0_5 = kotlinLabsDefinitions_0_4 + """
""${'"'}
Possible values for the `coercion` argument of the `@inlineClass` directive.
Since: 4.1.1
""${'"'}
enum InlineClassCoercion { String, Boolean, Int, Long, Float, Double, Any }

""${'"'}
Generate an inline class for the given scalar type. The wrapped type is determined by the `coercion` argument.
Since: 4.1.1
""${'"'}
directive @inlineClass(coercion: InlineClassCoercion!) on SCALAR
"""
Comment on lines +179 to +191
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

Copy link
Contributor

Choose a reason for hiding this comment

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

Also reminder to add the matching definition on https://github.com/apollographql/specs if we agree on this

Copy link
Contributor

Choose a reason for hiding this comment

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

Also reminder to add support in the IDE plugin

Screenshot 2025-01-15 at 19 52 03


// Built in scalar and introspection types from the Draft:
// - https://spec.graphql.org/draft/#sec-Scalars
// - https://spec.graphql.org/draft/#sec-Schema-Introspection.Schema-Introspection-Schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ enum class TargetLanguage {
/**
* Base language version.
*/
@Deprecated("Use KOTLIN_1_9 instead" , ReplaceWith("KOTLIN_1_9"))
@Deprecated("Use KOTLIN_1_9 instead", ReplaceWith("KOTLIN_1_9"))
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_2)
KOTLIN_1_5,

Expand Down Expand Up @@ -244,13 +244,15 @@ interface CommonCodegenOpt {
* Default: false
*/
val decapitalizeFields: Boolean?

/**
* When true, the operation class names are suffixed with their operation type like ('Query', 'Mutation' ot 'Subscription').
* For an example, `query getDroid { ... }` GraphQL query generates the 'GetDroidQuery' class.
*
* Default value: true
*/
val useSemanticNaming: Boolean?

/**
* Specifies which methods will be auto generated on operations, models, fragments and input objects.
*
Expand Down Expand Up @@ -332,6 +334,7 @@ interface SchemaCodegenOpt {
* Default: false
*/
val generateSchema: Boolean?

/**
* Class name to use when generating the Schema class.
*
Expand Down Expand Up @@ -435,10 +438,12 @@ interface KotlinCodegenOpt {
* on servers
*/
val addUnknownForEnums: Boolean?

/**
* Whether to add default arguments for input objects.
*/
val addDefaultArgumentForInputObjects: Boolean?

/**
* Kotlin native generates [Any?] for optional types
* Setting generateFilterNotNull generates extra `filterNotNull` functions that help keep the type information.
Expand Down Expand Up @@ -487,10 +492,10 @@ interface KotlinCodegenOpt {
val jsExport: Boolean?
}

interface JavaOperationsCodegenOptions: CommonCodegenOpt, OperationsCodegenOpt, JavaCodegenOpt
interface KotlinOperationsCodegenOptions: CommonCodegenOpt, OperationsCodegenOpt, KotlinCodegenOpt
interface JavaSchemaCodegenOptions: CommonCodegenOpt, SchemaCodegenOpt, JavaCodegenOpt
interface KotlinSchemaCodegenOptions: CommonCodegenOpt, SchemaCodegenOpt, KotlinCodegenOpt
interface JavaOperationsCodegenOptions : CommonCodegenOpt, OperationsCodegenOpt, JavaCodegenOpt
interface KotlinOperationsCodegenOptions : CommonCodegenOpt, OperationsCodegenOpt, KotlinCodegenOpt
interface JavaSchemaCodegenOptions : CommonCodegenOpt, SchemaCodegenOpt, JavaCodegenOpt
interface KotlinSchemaCodegenOptions : CommonCodegenOpt, SchemaCodegenOpt, KotlinCodegenOpt
Comment on lines +495 to +498
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, this is expected:

Screenshot 2025-01-15 at 11 59 54


interface SchemaCodegenOptions : JavaSchemaCodegenOptions, KotlinSchemaCodegenOptions
interface OperationsCodegenOptions : JavaOperationsCodegenOptions, KotlinOperationsCodegenOptions
Expand Down Expand Up @@ -521,7 +526,7 @@ class CodegenOptions(
override val nullableFieldStyle: JavaNullable?,
override val generateFragmentImplementations: Boolean?,
override val generateQueryDocument: Boolean?,
): SchemaCodegenOptions, OperationsCodegenOptions
) : SchemaCodegenOptions, OperationsCodegenOptions

fun buildCodegenOptions(
targetLanguage: TargetLanguage? = null,
Expand Down Expand Up @@ -637,18 +642,33 @@ class ExpressionAdapterInitializer(val expression: String) : AdapterInitializer
object RuntimeAdapterInitializer : AdapterInitializer

@Serializable
class ScalarInfo(
class ScalarInfo
@ApolloExperimental
constructor(
val targetName: String,
val adapterInitializer: AdapterInitializer = RuntimeAdapterInitializer,
val userDefined: Boolean = true,
)

/**
* If the target is an inline class, the property to access the underlying value, `null` otherwise.
*/
@ApolloExperimental
val inlineClassProperty: String? = null,
) {
constructor(
targetName: String,
adapterInitializer: AdapterInitializer = RuntimeAdapterInitializer,
userDefined: Boolean = true,
) : this(targetName, adapterInitializer, userDefined, null)
}

private val NoOpLogger = object : ApolloCompiler.Logger {
override fun warning(message: String) {
}
}

internal val defaultAlwaysGenerateTypesMatching = emptySet<String>()

@Suppress("DEPRECATION")
internal val defaultOperationOutputGenerator = OperationOutputGenerator.Default(OperationIdGenerator.Sha256)
internal val defaultLogger = NoOpLogger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ internal fun SchemaLayout.typePackageName() = "${schemaPackageName()}.type"
internal fun SchemaLayout.typeBuilderPackageName() = "${schemaPackageName()}.type.builder"
internal fun SchemaLayout.typeAdapterPackageName() = "${schemaPackageName()}.type.adapter"
internal fun SchemaLayout.typeUtilPackageName() = "${schemaPackageName()}.type.util"
internal fun SchemaLayout.typeScalarPackageName() = "${schemaPackageName()}.type.scalar"

internal fun SchemaLayout.paginationPackageName() = "${schemaPackageName()}.pagination"
internal fun SchemaLayout.schemaSubPackageName() = "${schemaPackageName()}.schema"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.apollographql.apollo.compiler.codegen.kotlin

import com.apollographql.apollo.ast.GQLScalarTypeDefinition
import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.findInlineClassCoercion
import com.apollographql.apollo.compiler.APOLLO_VERSION
import com.apollographql.apollo.compiler.CodegenMetadata
import com.apollographql.apollo.compiler.CodegenSchema
import com.apollographql.apollo.compiler.ExpressionAdapterInitializer
import com.apollographql.apollo.compiler.KotlinOperationsCodegenOptions
import com.apollographql.apollo.compiler.KotlinSchemaCodegenOptions
import com.apollographql.apollo.compiler.ScalarInfo
import com.apollographql.apollo.compiler.TargetLanguage
import com.apollographql.apollo.compiler.Transform
import com.apollographql.apollo.compiler.codegen.Identifier
import com.apollographql.apollo.compiler.codegen.OperationsLayout
import com.apollographql.apollo.compiler.codegen.ResolverKey
import com.apollographql.apollo.compiler.codegen.ResolverKeyKind
Expand All @@ -31,8 +37,10 @@ import com.apollographql.apollo.compiler.codegen.kotlin.schema.InterfaceBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.ObjectBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.PaginationBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.ScalarBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.ScalarInlineClassBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.SchemaBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.UnionBuilder
import com.apollographql.apollo.compiler.codegen.typeScalarPackageName
import com.apollographql.apollo.compiler.defaultAddDefaultArgumentForInputObjects
import com.apollographql.apollo.compiler.defaultAddJvmOverloads
import com.apollographql.apollo.compiler.defaultAddUnkownForEnums
Expand All @@ -48,8 +56,15 @@ import com.apollographql.apollo.compiler.defaultSealedClassesForEnumsMatching
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.IrScalarInlineClassCoercion
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.ANY
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.BOOLEAN
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.DOUBLE
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.FLOAT
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.INT
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.LONG
import com.apollographql.apollo.compiler.ir.IrScalarInlineClassCoercion.STRING
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 @@ -176,6 +191,9 @@ internal object KotlinCodegen {

irSchema.irScalars.forEach { irScalar ->
builders.add(ScalarBuilder(context, irScalar, scalarMapping.get(irScalar.name)?.targetName))
if (irScalar.inlineClassCoercion != null) {
builders.add(ScalarInlineClassBuilder(context, irScalar))
}
}
irSchema.irEnums.forEach { irEnum ->
if (sealedClassesForEnumsMatching.any { Regex(it).matches(irEnum.name) }) {
Expand Down Expand Up @@ -219,6 +237,14 @@ internal object KotlinCodegen {
layout: OperationsLayout,
kotlinOutputTransform: Transform<KotlinOutput>?,
): KotlinOutput {
// Add scalar mapping for inline classes generated when using @inlineClass
@Suppress("NAME_SHADOWING")
val codegenSchema = CodegenSchema(
schema = codegenSchema.schema,
normalizedPath = codegenSchema.normalizedPath,
scalarMapping = codegenSchema.scalarMapping + scalarMappingForInlineClasses(codegenSchema.schema, layout as SchemaLayout),
generateDataBuilders = codegenSchema.generateDataBuilders
)
val generateDataBuilders = codegenSchema.generateDataBuilders
val flatten = irOperations.flattenModels

Expand Down Expand Up @@ -310,5 +336,34 @@ internal object KotlinCodegen {
}
}
}
}

private fun scalarMappingForInlineClasses(schema: Schema, layout: SchemaLayout): Map<String, ScalarInfo> {
return schema.typeDefinitions.values
.filterIsInstance<GQLScalarTypeDefinition>()
.mapNotNull { scalarTypeDefinition ->
val name = scalarTypeDefinition.name
val inlineClassCoercion = scalarTypeDefinition.findInlineClassCoercion(schema)
if (inlineClassCoercion == null) {
null
} else {
val packageName = layout.typeScalarPackageName()
val simpleName = layout.schemaTypeName(name)
name to ScalarInfo(
targetName = "$packageName.$simpleName",
adapterInitializer = ExpressionAdapterInitializer(
when (IrScalarInlineClassCoercion.fromString(inlineClassCoercion)) {
STRING -> KotlinSymbols.StringAdapter.canonicalName
BOOLEAN -> KotlinSymbols.BooleanAdapter.canonicalName
INT -> KotlinSymbols.IntAdapter.canonicalName
LONG -> KotlinSymbols.LongAdapter.canonicalName
FLOAT -> KotlinSymbols.FloatAdapter.canonicalName
DOUBLE -> KotlinSymbols.DoubleAdapter.canonicalName
ANY -> KotlinSymbols.AnyAdapter.canonicalName
}
),
inlineClassProperty = Identifier.value
)
}
}.associate { it }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,64 @@ internal class KotlinResolver(
}
}

internal fun unwrapInlineClass(type: IrType): String {
return when (type) {
is IrListType -> {
if (type.rawType() is IrScalarType && scalarMapping[type.rawType().name]?.inlineClassProperty != null) {
if (type.nullable) {
"?.map { it${unwrapInlineClass(type.ofType)} }"
} else {
".map { it${unwrapInlineClass(type.ofType)} }"
}
} else {
""
}
}

is IrScalarType -> {
val inlineClassProperty = scalarMapping[type.name]?.inlineClassProperty
when {
inlineClassProperty == null -> ""
type.nullable -> "?.$inlineClassProperty"
else -> ".$inlineClassProperty"
}
}

else -> ""
}
}

internal fun wrapInlineClass(expression: String, type: IrType): String {
return when (type) {
is IrListType -> {
if (type.rawType() is IrScalarType && scalarMapping[type.rawType().name]?.inlineClassProperty != null) {
if (type.nullable) {
"$expression?.map { ${wrapInlineClass("it", type.ofType)} }"
} else {
"$expression.map { ${wrapInlineClass("it", type.ofType)} }"
}
} else {
expression
}
}

is IrScalarType -> {
val inlineClassProperty = scalarMapping[type.name]?.inlineClassProperty
if (inlineClassProperty == null) {
expression
} else {
val targetName = scalarMapping[type.name]!!.targetName
when {
type.nullable -> "$expression?.let { $targetName(it) }"
else -> "$targetName($expression)"
}
}
}

else -> expression
}
}

fun resolveCompiledType(name: String): CodeBlock {
return CodeBlock.of("%T.$type", resolveAndAssert(ResolverKeyKind.SchemaType, name))
}
Expand Down
Loading
Loading