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 support for @catch on fieldDefinitions, interfaces and objects #5623

Merged
merged 4 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<p>
Before being referenced, directives and types supported by Apollo Kotlin must be imported by your schema using the <code>@link</code> directive<br>.
For instance, to use the <code>@semanticNonNull</code> directive, import it from the
<a href="https://specs.apollo.dev/nullability/v0.2"><code>nullability</code></a> definitions:
<a href="https://specs.apollo.dev/nullability/v0.3"><code>nullability</code></a> definitions:
<pre>
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@semanticNonNull"]
)
</pre>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@catch"]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@catch", "CatchTo"]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@catch", "CatchTo"]
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@catch", "CatchTo"]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extend schema

extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.2",
url: "https://specs.apollo.dev/nullability/v0.3",
import: ["@catch", "CatchTo"]
)

Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-ast/api/apollo-ast.api
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ public final class com/apollographql/apollo3/ast/ReservedEnumValueName : com/apo

public final class com/apollographql/apollo3/ast/Schema {
public static final field CATCH Ljava/lang/String;
public static final field CATCH_FIELD Ljava/lang/String;
public static final field Companion Lcom/apollographql/apollo3/ast/Schema$Companion;
public static final field FIELD_POLICY Ljava/lang/String;
public static final field FIELD_POLICY_FOR_FIELD Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ class Schema internal constructor(
@ApolloExperimental
const val CATCH = "catch"
@ApolloExperimental
const val CATCH_FIELD = "catchField"
@ApolloExperimental
const val SEMANTIC_NON_NULL = "semanticNonNull"
@ApolloExperimental
const val SEMANTIC_NON_NULL_FIELD = "semanticNonNullField"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ enum class CatchTo {
}

@ApolloInternal
data class Catch(val to: CatchTo, val levels: List<Int>)
data class Catch(val to: CatchTo, val levels: List<Int>?)

private fun GQLDirectiveDefinition.getArgumentDefaultValue(argName: String): GQLValue? {
return arguments.firstOrNull { it.name == argName }?.defaultValue
}

@ApolloInternal
fun GQLDirective.getArgumentValueOrDefault(argName: String, schema: Schema): GQLValue? {
val directiveDefinition: GQLDirectiveDefinition = schema.directiveDefinitions.get(name)!!
val argument = arguments.firstOrNull { it.name == argName }
if (argument == null) {
val directiveDefinition: GQLDirectiveDefinition = schema.directiveDefinitions.get(name)!!
return directiveDefinition.getArgumentDefaultValue(argName)
}
return argument.value
Expand Down Expand Up @@ -148,7 +148,7 @@ private fun GQLValue?.toCatchTo(): CatchTo {
}

@ApolloInternal
fun List<GQLDirective>.findCatch(schema: Schema): Catch? {
private fun List<GQLDirective>.findCatch(schema: Schema): Catch? {
return filter {
schema.originalDirectiveName(it.name) == Schema.CATCH
}.map {
Expand All @@ -160,27 +160,29 @@ fun List<GQLDirective>.findCatch(schema: Schema): Catch? {
}

@ApolloInternal
fun GQLFieldDefinition.findSemanticNonNulls(schema: Schema): List<Int> {
val semanticNonNulls = directives.filter {
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL
fun GQLField.findCatch(fieldDefinition: GQLFieldDefinition, schema: Schema): Catch? {
var catch = directives.findCatch(schema)
if (catch != null) {
return catch
}

val semanticNonNull = semanticNonNulls.singleOrNull()
if (semanticNonNull == null) {
return emptyList()
catch = fieldDefinition.directives.findCatch(schema)
if (catch != null) {
return catch
}
return semanticNonNull.getArgumentValueOrDefault("levels", schema)!!.toListOfInt()

return schema.schemaDefinition?.directives?.findCatch(schema)?.copy(levels = null)
}

@ApolloInternal
fun GQLTypeDefinition.findSemanticNonNulls(fieldName: String, schema: Schema): List<Int> {
fun GQLFieldDefinition.findSemanticNonNulls(schema: Schema): List<Int> {
val semanticNonNulls = directives.filter {
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL_FIELD
&& it.getArgumentValueOrDefault("name", schema)?.toStringOrNull() == fieldName
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL
}

val semanticNonNull = semanticNonNulls.singleOrNull()
if (semanticNonNull == null) {
return emptyList()
}
return semanticNonNull.getArgumentValueOrDefault("levels", schema)!!.toListOfInt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fun kotlinLabsDefinitions(version: String): List<GQLDefinition> {
})
}

@ApolloInternal const val NULLABILITY_VERSION = "v0.2"
@ApolloInternal const val NULLABILITY_VERSION = "v0.3"

/**
* Extra nullability definitions from https://specs.apollo.dev/nullability/<[version]>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,10 @@ internal class ExecutableValidationScope(
val fieldA = fieldWithParentA.field
val fieldB = fieldWithParentB.field

val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type
val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type
val fieldDefinitionA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)
val fieldDefinitionB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)
val typeA = fieldDefinitionA?.type
val typeB = fieldDefinitionB?.type
if (typeA == null || typeB == null) {
// will be caught by other validation rules
return
Expand All @@ -512,7 +514,7 @@ internal class ExecutableValidationScope(
addFieldMergingIssue(fieldWithParentA.field, fieldWithParentB.field, "they have different types")
return
}
if (hasCatch && !areCatchesEqual(fieldA.directives.findCatch(schema), fieldB.directives.findCatch(schema))) {
if (hasCatch && !areCatchesEqual(fieldA.findCatch(fieldDefinitionA, schema), fieldB.findCatch(fieldDefinitionB, schema))) {
addFieldMergingIssue(fieldWithParentA.field, fieldWithParentB.field, "they have different `@catch` directives")
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
import com.apollographql.apollo3.ast.GQLScalarTypeExtension
import com.apollographql.apollo3.ast.GQLSchemaDefinition
import com.apollographql.apollo3.ast.GQLSchemaExtension
import com.apollographql.apollo3.ast.GQLStringValue
import com.apollographql.apollo3.ast.GQLType
import com.apollographql.apollo3.ast.GQLTypeSystemExtension
import com.apollographql.apollo3.ast.GQLUnionTypeDefinition
import com.apollographql.apollo3.ast.GQLUnionTypeExtension
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.MergeOptions
import com.apollographql.apollo3.ast.OtherValidationIssue
import com.apollographql.apollo3.ast.Schema
import com.apollographql.apollo3.ast.SourceLocation
import com.apollographql.apollo3.ast.toUtf8
import kotlin.reflect.KClass
Expand Down Expand Up @@ -131,9 +133,10 @@ private fun ExtensionsMerger.mergeObject(
objectTypeDefinition: GQLObjectTypeDefinition,
extension: GQLObjectTypeExtension,
): GQLObjectTypeDefinition = with(objectTypeDefinition) {
val mergedDirectives = mergeObjectOrInterfaceDirectives(directives, extension.directives)
return copy(
directives = mergeDirectives(directives, extension.directives),
fields = mergeFields(fields, extension.fields),
fields = mergeFields(fields, extension.fields, mergedDirectives.fieldDirectives),
directives = mergedDirectives.directives,
implementsInterfaces = mergeUniqueInterfacesOrThrow(implementsInterfaces, extension.implementsInterfaces, extension.sourceLocation)
)
}
Expand All @@ -142,9 +145,10 @@ private fun ExtensionsMerger.mergeInterface(
interfaceTypeDefinition: GQLInterfaceTypeDefinition,
extension: GQLInterfaceTypeExtension,
): GQLInterfaceTypeDefinition = with(interfaceTypeDefinition) {
val mergedDirectives = mergeObjectOrInterfaceDirectives(directives, extension.directives)
return copy(
fields = mergeFields(fields, extension.fields),
directives = mergeDirectives(directives, extension.directives),
fields = mergeFields(fields, extension.fields, mergedDirectives.fieldDirectives),
directives = mergedDirectives.directives,
implementsInterfaces = mergeUniqueInterfacesOrThrow(implementsInterfaces, extension.implementsInterfaces, extension.sourceLocation)
)
}
Expand Down Expand Up @@ -210,17 +214,60 @@ private fun ExtensionsMerger.mergeSchema(
)
}

private class MergedDirectives(
val directives: List<GQLDirective>,
val fieldDirectives: Map<String, List<GQLDirective>>,
)

private fun ExtensionsMerger.mergeObjectOrInterfaceDirectives(
list: List<GQLDirective>,
other: List<GQLDirective>,
): MergedDirectives {
val fieldDirectives = mutableListOf<Pair<String, GQLDirective>>()

val directives = mergeDirectives(list, other) {
if (it.name == Schema.CATCH_FIELD) {
fieldDirectives.add((it.arguments.first { it.name == "name" }.value as GQLStringValue).value to GQLDirective(
name = Schema.CATCH,
arguments = it.arguments.filter { it.name != "name" }
))
false
} else if (it.name == Schema.SEMANTIC_NON_NULL_FIELD) {
fieldDirectives.add((it.arguments.first { it.name == "name" }.value as GQLStringValue).value to GQLDirective(
name = Schema.SEMANTIC_NON_NULL,
arguments = it.arguments.filter { it.name != "name" }
))
false
} else {
true
}
}

return MergedDirectives(
directives,
fieldDirectives.groupBy(
keySelector = { it.first },
valueTransform = { it.second }
)
)
}

/**
* Merge both list of directive
* Merge both lists of directives
*/
private fun ExtensionsMerger.mergeDirectives(
list: List<GQLDirective>,
other: List<GQLDirective>,
filter: (GQLDirective) -> Boolean = { true },
): List<GQLDirective> {
val result = mutableListOf<GQLDirective>()

result.addAll(list)
for (directiveToAdd in other) {
if (!filter(directiveToAdd)) {
continue
}

if (result.any { it.name == directiveToAdd.name }) {
// duplicated directive, get the definition to see if we can
val definition = directiveDefinitions[directiveToAdd.name]
Expand All @@ -239,6 +286,7 @@ private fun ExtensionsMerger.mergeDirectives(
return result
}


private inline fun <reified T> ExtensionsMerger.mergeUniquesOrThrow(
list: List<T>,
others: List<T>,
Expand All @@ -265,6 +313,7 @@ private fun ExtensionsMerger.mergeUniqueInterfacesOrThrow(
private fun ExtensionsMerger.mergeFields(
list: List<GQLFieldDefinition>,
others: List<GQLFieldDefinition>,
extraDirectives: Map<String, List<GQLDirective>> = emptyMap(),
): List<GQLFieldDefinition> {

val result = list.toMutableList()
Expand Down Expand Up @@ -300,7 +349,9 @@ private fun ExtensionsMerger.mergeFields(
}
}

return result
return result.map {
it.copy(directives = mergeDirectives(it.directives, extraDirectives.get(it.name).orEmpty()))
}
}

private fun GQLType.isCompatibleWith(other: GQLType): Boolean {
Expand All @@ -322,13 +373,15 @@ private fun GQLType.isCompatibleWith(other: GQLType): Boolean {
false
}
}

is GQLNamedType -> {
if (b !is GQLNamedType) {
false
} else {
b.name == a.name
}
}

is GQLNonNullType -> {
error("")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,22 @@ Passing a negative level or a level greater than the list dimension is an error.

See `CatchTo` for more details.
""${'"'}
directive @catch(to: CatchTo! = RESULT, levels: [Int] = [0]) on FIELD | SCHEMA
directive @catch(to: CatchTo! = RESULT, levels: [Int] = [0]) on FIELD | FIELD_DEFINITION | SCHEMA

""${'"'}
Indicates how clients should handle errors on a given position.

`@catchField` is the same as `@catch` but can be used on type system extensions for services
that do not own the schema like client services:

```graphql
# extend the schema to catch User.email to `RESULT`.
extend type User @catchField(name: "email", to: RESULT)
```

See `@catch`.
""${'"'}
directive @catchField(to: CatchTo! = RESULT, levels: [Int] = [0]) repeatable on INTERFACE | OBJECT

enum CatchTo {
""${'"'}
Expand Down