Skip to content

Commit

Permalink
馃И @experimental support (#4091)
Browse files Browse the repository at this point in the history
* add support for @experimental

See graphql/graphql-spec#943

* fix tests, remove debug

* wip

* add deprecated on input fields and arguments

* add a validation error for deprecated arguments usage

* added validation tests

* ApolloExperimental -> Experimental

* update apiDump
  • Loading branch information
martinbonnin committed May 10, 2022
1 parent 9dab1f3 commit a20964c
Show file tree
Hide file tree
Showing 40 changed files with 586 additions and 265 deletions.
3 changes: 3 additions & 0 deletions apollo-annotations/api/apollo-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ public abstract interface annotation class com/apollographql/apollo3/annotations
public abstract interface annotation class com/apollographql/apollo3/annotations/ApolloInternal : java/lang/annotation/Annotation {
}

public abstract interface annotation class com/apollographql/apollo3/annotations/Experimental : java/lang/annotation/Annotation {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.apollographql.apollo3.annotations

@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "This field, input field or enum value is declared experimental and can be changed in a backwards-incompatible manner in future schema updates."
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@MustBeDocumented
annotation class Experimental
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ fun List<GQLDirective>.findDeprecationReason() = firstOrNull { it.name == "depre
?: "No longer supported"
}

fun List<GQLDirective>.findExperimentalReason() = firstOrNull { it.name == "experimental" }
?.let {
it.arguments
?.arguments
?.firstOrNull { it.name == "reason" }
?.value
?.let { value ->
if (value !is GQLStringValue) {
throw ConversionException("reason must be a string", it.sourceLocation)
}
value.value
}
?: "Experimental"
}

/**
* @return `true` or `false` based on the `if` argument if the `optional` directive is present, `null` otherwise
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.apollographql.apollo3.ast.SourceLocation
import com.apollographql.apollo3.ast.VariableUsage
import com.apollographql.apollo3.ast.definitionFromScope
import com.apollographql.apollo3.ast.findDeprecationReason
import com.apollographql.apollo3.ast.findExperimentalReason
import com.apollographql.apollo3.ast.leafType
import com.apollographql.apollo3.ast.pretty
import com.apollographql.apollo3.ast.responseName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.apollographql.apollo3.ast.GQLValue
import com.apollographql.apollo3.ast.GQLVariableValue
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.VariableUsage
import com.apollographql.apollo3.ast.findDeprecationReason
import com.apollographql.apollo3.ast.isDeprecated
import com.apollographql.apollo3.ast.pretty
import com.apollographql.apollo3.ast.toUtf8
Expand Down Expand Up @@ -110,14 +111,23 @@ private fun ValidationScope.validateAndCoerceInputObject(value: GQLValue, expect
return value
}

// 3.10 All required input fields must have a value
expectedTypeDefinition.inputFields.forEach { inputValueDefinition ->
// 3.10 All required input fields must have a value
if (inputValueDefinition.type is GQLNonNullType
&& inputValueDefinition.defaultValue == null
&& value.fields.firstOrNull { it.name == inputValueDefinition.name } == null
) {
registerIssue(message = "No value passed for required inputField ${inputValueDefinition.name}", sourceLocation = value.sourceLocation)
registerIssue(message = "No value passed for required inputField `${inputValueDefinition.name}`", sourceLocation = value.sourceLocation)
}
if (inputValueDefinition.directives.findDeprecationReason() != null) {
issues.add(
Issue.DeprecatedUsage(
message = "Use of deprecated input field `${inputValueDefinition.name}`",
sourceLocation = value.sourceLocation
)
)
}

}

return GQLObjectValue(fields = value.fields.mapNotNull { field ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.apollographql.apollo3.ast.Schema.Companion.TYPE_POLICY
import com.apollographql.apollo3.ast.SourceLocation
import com.apollographql.apollo3.ast.ValidationDetails
import com.apollographql.apollo3.ast.VariableUsage
import com.apollographql.apollo3.ast.findDeprecationReason
import com.apollographql.apollo3.ast.isVariableUsageAllowed
import com.apollographql.apollo3.ast.parseAsGQLSelections
import com.apollographql.apollo3.ast.pretty
Expand Down Expand Up @@ -210,7 +211,7 @@ internal fun ValidationScope.extraValidateTypePolicyDirective(directive: GQLDire
(directive.arguments!!.arguments.first().value as GQLStringValue).value.buffer().parseAsGQLSelections().valueAssertNoErrors().forEach {
if (it !is GQLField) {
registerIssue("Fragments are not supported in @$TYPE_POLICY directives", it.sourceLocation)
} else if (it.selectionSet != null){
} else if (it.selectionSet != null) {
registerIssue("Composite fields are not supported in @$TYPE_POLICY directives", it.sourceLocation)
}
}
Expand All @@ -228,6 +229,9 @@ private fun ValidationScope.validateArgument(
registerIssue(message = "Unknown argument `$name` on $debug", sourceLocation = sourceLocation)
return@with
}
if (schemaArgument.directives.findDeprecationReason() != null) {
issues.add(Issue.DeprecatedUsage(message = "Use of deprecated argument `$name`", sourceLocation = sourceLocation))
}

// 5.6.2 Input Object Field Names
// Note that this does not modify the document, it calls coerce because it's easier
Expand Down Expand Up @@ -270,7 +274,7 @@ internal fun ValidationScope.validateArguments(

internal fun ValidationScope.validateVariable(
operation: GQLOperationDefinition?,
variableUsage: VariableUsage
variableUsage: VariableUsage,
) {
if (operation == null) {
// if operation is null, it means we're currently validating a fragment outside the context of an operation
Expand Down
6 changes: 6 additions & 0 deletions apollo-ast/src/main/resources/apollo.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ directive @typePolicy(keyFields: String!) on OBJECT
# Indicates how to compute a key from a field arguments.
# `keyArgs` should contain a selection set. Composite args are not supported yet.
directive @fieldPolicy(forField: String!, keyArgs: String!) repeatable on OBJECT

# Indicates that the given field or enum value is still experimental and might be changed
# in a backward incompatible manner
directive @experimental(
reason: String! = "Experimental"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
2 changes: 1 addition & 1 deletion apollo-ast/src/main/resources/builtins.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE

directive @experimental_defer(
label: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ object ApolloCompiler {
scalarMapping = options.scalarMapping,
nameToClassName = nameToClassName,
addJvmOverloads = options.addJvmOverloads,
experimentalAnnotation = options.experimentalAnnotation,
).write(outputDir = outputDir, testDir = testDir)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class Options(
*/
val addJvmOverloads: Boolean = false,
val addTypename: String = defaultAddTypename,
val experimentalAnnotation: String? = defaultExperimentalAnnotation
) {

/**
Expand Down Expand Up @@ -239,6 +240,7 @@ class Options(
generateOptionalOperationVariables: Boolean = this.generateOptionalOperationVariables,
addJvmOverloads: Boolean = this.addJvmOverloads,
addTypename: String = this.addTypename,
experimentalAnnotation: String? = this.experimentalAnnotation
) = Options(
executableFiles = executableFiles,
schema = schema,
Expand Down Expand Up @@ -273,6 +275,7 @@ class Options(
generateOptionalOperationVariables = generateOptionalOperationVariables,
addJvmOverloads = addJvmOverloads,
addTypename = addTypename,
experimentalAnnotation = experimentalAnnotation,
)

companion object {
Expand All @@ -292,6 +295,7 @@ class Options(
const val defaultModuleName = "apollographql"
const val defaultCodegenModels = MODELS_OPERATION_BASED
const val defaultAddTypename = ADD_TYPENAME_IF_FRAGMENTS
const val defaultExperimentalAnnotation = "com.apollographql.apollo3.annotations.Experimental"
const val defaultFlattenModels = true
val defaultTargetLanguage = TargetLanguage.KOTLIN_1_5
const val defaultGenerateSchema = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ class KotlinCodeGen(
private val scalarMapping: Map<String, ScalarInfo>,
private val nameToClassName: Map<String, String>,
private val addJvmOverloads: Boolean,
private val experimentalAnnotation: String?
) {
/**
* @param outputDir: the directory where to write the Kotlin files
* @return a ResolverInfo to be used by downstream modules
*/
fun write(outputDir: File, testDir: File): ResolverInfo {
val upstreamResolver = resolverInfos.fold(null as KotlinResolver?) { acc, resolverInfo ->
KotlinResolver(resolverInfo.entries, acc, scalarMapping)
KotlinResolver(resolverInfo.entries, acc, scalarMapping, experimentalAnnotation)
}

val layout = KotlinCodegenLayout(
Expand All @@ -85,7 +86,7 @@ class KotlinCodeGen(

val context = KotlinContext(
layout = layout,
resolver = KotlinResolver(emptyList(), upstreamResolver, scalarMapping),
resolver = KotlinResolver(emptyList(), upstreamResolver, scalarMapping, experimentalAnnotation),
targetLanguageVersion = targetLanguageVersion,
)
val builders = mutableListOf<CgFileBuilder>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName


class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, private val scalarMapping: Map<String, ScalarInfo>) {
class KotlinResolver(
entries: List<ResolverEntry>,
val next: KotlinResolver?,
private val scalarMapping: Map<String, ScalarInfo>,
private val experimentalAnnotation: String?
) {
fun resolve(key: ResolverKey): ClassName? = classNames[key] ?: next?.resolve(key)

private var classNames = entries.associateBy(
Expand Down Expand Up @@ -72,7 +77,7 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr

private fun resolveIrScalarType(type: IrScalarType): ClassName {
// Try mapping first, then built-ins, then fallback to Any
return resolveScalarTaget(type.name) ?: when (type.name) {
return resolveScalarTarget(type.name) ?: when (type.name) {
"String" -> KotlinSymbols.String
"Float" -> KotlinSymbols.Double
"Int" -> KotlinSymbols.Int
Expand All @@ -82,8 +87,8 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr
}
}

fun resolveScalarTaget(name: String): ClassName? {
return scalarMapping.get(name)?.targetName?.let {
private fun resolveScalarTarget(name: String): ClassName? {
return scalarMapping[name]?.targetName?.let {
ClassName.bestGuess(it)
}
}
Expand All @@ -98,7 +103,7 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr
type is IrScalarType && type.name == "String" && scalarWithoutCustomMapping -> CodeBlock.of("%M", KotlinSymbols.NullableStringAdapter)
type is IrScalarType && type.name == "Int" && scalarWithoutCustomMapping -> CodeBlock.of("%M", KotlinSymbols.NullableIntAdapter)
type is IrScalarType && type.name == "Float" && scalarWithoutCustomMapping -> CodeBlock.of("%M", KotlinSymbols.NullableDoubleAdapter)
type is IrScalarType && resolveScalarTaget(type.name) == null -> {
type is IrScalarType && resolveScalarTarget(type.name) == null -> {
CodeBlock.of("%M", KotlinSymbols.NullableAnyAdapter)
}
else -> {
Expand Down Expand Up @@ -161,7 +166,7 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr
CodeBlock.of(adapterInitializer.expression)
}
is RuntimeAdapterInitializer -> {
val target = resolveScalarTaget(type.name)
val target = resolveScalarTarget(type.name)
CodeBlock.of(
"$customScalarAdapters.responseAdapterFor<%T>(%L)",
target,
Expand All @@ -176,7 +181,7 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr
"Int" -> CodeBlock.of("%M", KotlinSymbols.IntAdapter)
"Float" -> CodeBlock.of("%M", KotlinSymbols.DoubleAdapter)
else -> {
val target = resolveScalarTaget(type.name)
val target = resolveScalarTarget(type.name)
if (target == null) {
CodeBlock.of("%M", KotlinSymbols.AnyAdapter)
} else {
Expand Down Expand Up @@ -237,4 +242,10 @@ class KotlinResolver(entries: List<ResolverEntry>, val next: KotlinResolver?, pr

fun registerTestBuilder(path: String, className: ClassName) = register(ResolverKeyKind.TestBuilder, path, className)
fun resolveTestBuilder(path: String) = resolveAndAssert(ResolverKeyKind.TestBuilder, path)
fun resolveExperimentalAnnotation(): ClassName? {
if (experimentalAnnotation == "none") {
return null
}
return experimentalAnnotation?.let { ClassName.bestGuess(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinContext
import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinSymbols
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDeprecation
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDescription
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddExperimental
import com.apollographql.apollo3.compiler.ir.IrEnum
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
Expand Down Expand Up @@ -103,6 +104,7 @@ class EnumAsEnumBuilder(
private fun IrEnum.Value.enumConstTypeSpec(): TypeSpec {
return TypeSpec.anonymousClassBuilder()
.maybeAddDeprecation(deprecationReason)
.maybeAddExperimental(context.resolver, experimentalReason)
.maybeAddDescription(description)
.addSuperclassConstructorParameter("%S", name)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.apollographql.apollo3.compiler.codegen.kotlin.CgFileBuilder
import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinContext
import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinSymbols
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.deprecatedAnnotation
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDeprecation
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDescription
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddExperimental
import com.apollographql.apollo3.compiler.ir.IrEnum
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
Expand Down Expand Up @@ -75,8 +77,9 @@ class EnumAsSealedBuilder(

private fun IrEnum.Value.toObjectTypeSpec(superClass: TypeName): TypeSpec {
return TypeSpec.objectBuilder(layout.enumAsSealedClassValueName(name))
.applyIf(description?.isNotBlank() == true) { addKdoc("%L\n", description!!) }
.applyIf(deprecationReason != null) { addAnnotation(deprecatedAnnotation(deprecationReason!!)) }
.maybeAddDeprecation(deprecationReason)
.maybeAddDescription(description)
.maybeAddExperimental(context.resolver, experimentalReason)
.superclass(superClass)
.addSuperclassConstructorParameter("rawValue路=路%S", name)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import com.apollographql.apollo3.compiler.codegen.kotlin.CgFile
import com.apollographql.apollo3.compiler.codegen.kotlin.CgFileBuilder
import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinContext
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.makeDataClass
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDeprecation
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddDescription
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.maybeAddExperimental
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.toNamedType
import com.apollographql.apollo3.compiler.codegen.kotlin.helpers.toParameterSpec
import com.apollographql.apollo3.compiler.ir.IrInputObject
Expand Down Expand Up @@ -35,7 +38,7 @@ class InputObjectBuilder(
private fun IrInputObject.typeSpec() =
TypeSpec
.classBuilder(simpleName)
.applyIf(description?.isNotBlank()== true) { addKdoc("%L\n", description!!) }
.maybeAddDescription(description)
.makeDataClass(fields.map {
it.toNamedType().toParameterSpec(context)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.apollographql.apollo3.compiler.codegen.kotlin.helpers

import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinResolver
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec

Expand All @@ -19,6 +22,15 @@ internal fun PropertySpec.Builder.maybeAddDescription(description: String?): Pro
return addKdoc("%L", description)
}

internal fun ParameterSpec.Builder.maybeAddDescription(description: String?): ParameterSpec.Builder {
if (description.isNullOrBlank()) {
return this
}

return addKdoc("%L", description)
}


internal fun TypeSpec.Builder.maybeAddDeprecation(deprecationReason: String?): TypeSpec.Builder {
if (deprecationReason.isNullOrBlank()) {
return this
Expand All @@ -34,3 +46,38 @@ internal fun PropertySpec.Builder.maybeAddDeprecation(deprecationReason: String?

return addAnnotation(deprecatedAnnotation(deprecationReason))
}

internal fun ParameterSpec.Builder.maybeAddDeprecation(deprecationReason: String?): ParameterSpec.Builder {
if (deprecationReason.isNullOrBlank()) {
return this
}

return addAnnotation(deprecatedAnnotation(deprecationReason))
}

internal fun TypeSpec.Builder.maybeAddExperimental(resolver: KotlinResolver, experimentalReason: String?): TypeSpec.Builder {
if (experimentalReason.isNullOrBlank()) {
return this
}

val annotation = resolver.resolveExperimentalAnnotation() ?: return this
return addAnnotation(AnnotationSpec.builder(annotation).build())
}

internal fun PropertySpec.Builder.maybeAddExperimental(resolver: KotlinResolver, experimentalReason: String?): PropertySpec.Builder {
if (experimentalReason.isNullOrBlank()) {
return this
}

val annotation = resolver.resolveExperimentalAnnotation() ?: return this
return addAnnotation(AnnotationSpec.builder(annotation).build())
}

internal fun ParameterSpec.Builder.maybeAddExperimental(resolver: KotlinResolver, experimentalReason: String?): ParameterSpec.Builder {
if (experimentalReason.isNullOrBlank()) {
return this
}

val annotation = resolver.resolveExperimentalAnnotation() ?: return this
return addAnnotation(AnnotationSpec.builder(annotation).build())
}

0 comments on commit a20964c

Please sign in to comment.