Skip to content

Commit

Permalink
[compiler] Add support for @catch on fieldDefinitions, interfaces and…
Browse files Browse the repository at this point in the history
… objects (#5623)

* add support for @catch on fieldDefinitions, interfaces&objects

* fix indentation

* fix indentation

* fix indentation
  • Loading branch information
martinbonnin committed Feb 20, 2024
1 parent 902729e commit f301e9b
Show file tree
Hide file tree
Showing 22 changed files with 188 additions and 53 deletions.
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

0 comments on commit f301e9b

Please sign in to comment.