Skip to content

Allow explicit CCN syntax #5148

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

Merged
merged 5 commits into from
Aug 3, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ fun String.parseAsGQLType(options: ParserOptions = ParserOptions.Default): GQLRe
}
}

internal fun String.parseAsGQLNullability(options: ParserOptions = ParserOptions.Default): GQLResult<GQLNullability> {
@Suppress("DEPRECATION")
check (!options.useAntlr)
return parseInternal(null, options) { parseNullability() ?: error("No nullability") }
}

fun String.parseAsGQLSelections(options: ParserOptions = ParserOptions.Default): GQLResult<List<GQLSelection>> {
@Suppress("DEPRECATION")
return if (options.useAntlr) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1529,15 +1529,17 @@ class GQLNullDesignator(override val sourceLocation: SourceLocation? = null) : G
@ApolloExperimental
class GQLListNullability(
override val sourceLocation: SourceLocation? = null,
val itemNullability: GQLNullability,
val itemNullability: GQLNullability?,
val selfNullability: GQLNullability?,
) : GQLNullability {
override val children: List<GQLNode>
get() = listOf(itemNullability)
get() = listOfNotNull(itemNullability)

override fun writeInternal(writer: SDLWriter) {
writer.write("[")
writer.write(itemNullability)
if (itemNullability != null) {
writer.write(itemNullability)
}
writer.write("]")
if (selfNullability != null) {
writer.write(selfNullability)
Expand All @@ -1552,7 +1554,7 @@ class GQLListNullability(

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
ofNullability: GQLNullability = this.itemNullability,
ofNullability: GQLNullability? = this.itemNullability,
selfNullability: GQLNullability? = this.selfNullability,
): GQLListNullability {
return GQLListNullability(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ internal fun isVariableUsageAllowed(variableDefinition: GQLVariableDefinition, u
}

internal fun areTypesCompatible(variableType: GQLType, locationType: GQLType): Boolean {
return if(locationType is GQLNonNullType) {
return if (locationType is GQLNonNullType) {
if (variableType !is GQLNonNullType) {
false
} else {
areTypesCompatible(variableType.type, locationType.type)
}
} else if (variableType is GQLNonNullType) {
areTypesCompatible(variableType.type, locationType)
} else if (locationType is GQLListType){
} else if (locationType is GQLListType) {
if (variableType !is GQLListType) {
false
} else {
Expand Down Expand Up @@ -84,43 +84,77 @@ internal fun GQLType.isOutputType(typeDefinitions: Map<String, GQLTypeDefinition
}
}

private fun GQLNullability?.selfNullability(): GQLNullability? {
return when (this) {
is GQLListNullability -> this.selfNullability
else -> this
private fun GQLType.withItemNullability(itemNullability: GQLNullability?, validation: NullabilityValidation): GQLType {
if (itemNullability == null) {
return this
}
}

private fun GQLType.withListNullability(nullability: GQLNullability?): GQLType {
if (this is GQLListType && nullability is GQLListNullability) {
return copy(type = type.withNullability(nullability.itemNullability))
} else if (this is GQLListType && nullability !is GQLListNullability) {
return this
} else if (this !is GQLListType && nullability is GQLListNullability) {
return this
} else if (this !is GQLListType && nullability !is GQLListNullability) {
return this
} else {
error("")
if (this !is GQLListType) {
when (validation) {
is NullabilityValidationThrow -> {
check(this is GQLListType) {
"Cannot apply nullability, the nullability list dimension exceeds the one of the field type."
}
}

is NullabilityValidationIgnore -> {
return this

}

is NullabilityValidationRegister -> {
validation.issues.add(
Issue.ValidationError(
"Cannot apply nullability on '${validation.fieldName}', the nullability list dimension exceeds the one of the field type.",
itemNullability.sourceLocation,
)
)
return this
}
}
}

return this.copy(type = type.withNullability(itemNullability, validation))
}

@ApolloExperimental
fun GQLType.withNullability(nullability: GQLNullability?): GQLType {
val selfNullability = nullability.selfNullability()
return withNullability(nullability, NullabilityValidationThrow)
}

internal sealed interface NullabilityValidation

internal object NullabilityValidationIgnore: NullabilityValidation
internal object NullabilityValidationThrow: NullabilityValidation
internal class NullabilityValidationRegister(val issues: MutableList<Issue>, val fieldName: String): NullabilityValidation

if (this is GQLNonNullType && selfNullability == null) {
return this.copy(type = type.withListNullability(nullability))
internal fun GQLType.withNullability(nullability: GQLNullability?, validation: NullabilityValidation): GQLType {
val selfNullability: GQLNullability?
val itemNullability: GQLNullability?

when (nullability) {
is GQLListNullability -> {
selfNullability = nullability.selfNullability
itemNullability = nullability.itemNullability
}

else -> {
selfNullability = nullability
itemNullability = null
}
}
return if (this is GQLNonNullType && selfNullability == null) {
this.copy(type = type.withItemNullability(itemNullability, validation))
} else if (this is GQLNonNullType && selfNullability is GQLNonNullDesignator) {
return this.copy(type = type.withListNullability(nullability))
this.copy(type = type.withItemNullability(itemNullability, validation))
} else if (this is GQLNonNullType && selfNullability is GQLNullDesignator) {
return this.type.withListNullability(nullability)
this.type.withItemNullability(itemNullability, validation)
} else if (this !is GQLNonNullType && selfNullability == null) {
return this.withListNullability(nullability)
this.withItemNullability(itemNullability, validation)
} else if (this !is GQLNonNullType && selfNullability is GQLNonNullDesignator) {
return GQLNonNullType(type = this.withListNullability(nullability))
GQLNonNullType(type = this.withItemNullability(itemNullability, validation))
} else if (this !is GQLNonNullType && selfNullability is GQLNullDesignator) {
return this.withListNullability(nullability)
this.withItemNullability(itemNullability, validation)
} else {
error("")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import com.apollographql.apollo3.ast.GQLFragmentDefinition
import com.apollographql.apollo3.ast.GQLFragmentSpread
import com.apollographql.apollo3.ast.GQLInlineFragment
import com.apollographql.apollo3.ast.GQLIntValue
import com.apollographql.apollo3.ast.GQLListNullability
import com.apollographql.apollo3.ast.GQLListType
import com.apollographql.apollo3.ast.GQLListValue
import com.apollographql.apollo3.ast.GQLNamedType
import com.apollographql.apollo3.ast.GQLNode
import com.apollographql.apollo3.ast.GQLNonNullType
import com.apollographql.apollo3.ast.GQLNullValue
import com.apollographql.apollo3.ast.GQLNullability
import com.apollographql.apollo3.ast.GQLObjectTypeDefinition
import com.apollographql.apollo3.ast.GQLObjectValue
import com.apollographql.apollo3.ast.GQLOperationDefinition
import com.apollographql.apollo3.ast.GQLNullDesignator
import com.apollographql.apollo3.ast.GQLNonNullDesignator
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
import com.apollographql.apollo3.ast.GQLSelection
import com.apollographql.apollo3.ast.GQLStringValue
Expand All @@ -34,6 +30,8 @@ import com.apollographql.apollo3.ast.GQLValue
import com.apollographql.apollo3.ast.GQLVariableValue
import com.apollographql.apollo3.ast.InferredVariable
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.NullabilityValidationIgnore
import com.apollographql.apollo3.ast.NullabilityValidationRegister
import com.apollographql.apollo3.ast.Schema
import com.apollographql.apollo3.ast.SourceLocation
import com.apollographql.apollo3.ast.VariableUsage
Expand Down Expand Up @@ -218,15 +216,7 @@ internal class ExecutableValidationScope(
}

if (nullability != null) {
val typeListDimension = fieldDefinition.type.listDimension()
val nullabilityListDimension = nullability.listDimension()
if (typeListDimension < nullabilityListDimension) {
registerIssue(
message = "Cannot apply nullability on '$name', the nullability list dimension exceeds the one of the field type.",
sourceLocation = nullability.sourceLocation
)
return
}
fieldDefinition.type.withNullability(nullability, NullabilityValidationRegister(issues, name))
}

directives.forEach {
Expand All @@ -236,22 +226,6 @@ internal class ExecutableValidationScope(
}
}

private fun GQLType.listDimension(): Int {
return when (this) {
is GQLNonNullType -> this.type.listDimension()
is GQLListType -> 1 + this.type.listDimension()
else -> 0
}
}

private fun GQLNullability.listDimension(): Int {
return when (this) {
is GQLListNullability -> 1 + this.itemNullability.listDimension()
is GQLNullDesignator -> 0
is GQLNonNullDesignator -> 0
}
}

private fun GQLInlineFragment.validate(parentTypeDefinition: GQLTypeDefinition, selectionSetParent: GQLNode, path: String) {
val tc = typeCondition?.name ?: parentTypeDefinition.name
val inlineFragmentTypeDefinition = typeDefinitions[tc]
Expand Down Expand Up @@ -517,8 +491,8 @@ internal class ExecutableValidationScope(
val fieldA = fieldWithParentA.field
val fieldB = fieldWithParentB.field

val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability)
val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability)
val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability, NullabilityValidationIgnore)
val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability, NullabilityValidationIgnore)
if (typeA == null || typeB == null) {
// will be caught by other validation rules
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ internal class Parser(
return parseTopLevel(::parseTypeInternal)
}

fun parseNullability(): GQLNullability? {
return parseTopLevel(::parseNullabilityInternal)
}

private fun advance() {
lastToken = token
if (lookaheadToken != null) {
Expand Down Expand Up @@ -217,7 +221,7 @@ internal class Parser(
val arguments = parseArguments(const = false)
var nullability: GQLNullability? = null
if (allowClientControlledNullability) {
nullability = parseNullability()
nullability = parseNullabilityInternal()
}
val directives = parseDirectives(const = false)

Expand Down Expand Up @@ -253,7 +257,7 @@ internal class Parser(
}
}

private fun parseNullability(): GQLNullability? {
private fun parseNullabilityInternal(): GQLNullability? {
return when (token) {
is Token.LeftBracket -> {
parseListNullability()
Expand All @@ -265,17 +269,12 @@ internal class Parser(
}

private fun parseListNullability(): GQLListNullability {
val start = token
val sourceLocation = sourceLocation()

expectToken<Token.LeftBracket>()
val ofNullability = parseNullability()
val ofNullability = parseNullabilityInternal()
expectToken<Token.RightBracket>()

if (ofNullability == null) {
throw ParserException("List nullability must not be empty", start)
}

return GQLListNullability(
sourceLocation = sourceLocation,
itemNullability = ofNullability,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.apollographql.apollo3.graphql.ast.test

import com.apollographql.apollo3.ast.GQLListNullability
import com.apollographql.apollo3.ast.GQLNullDesignator
import com.apollographql.apollo3.ast.parseAsGQLNullability
import com.apollographql.apollo3.ast.parseAsGQLType
import com.apollographql.apollo3.ast.pretty
import com.apollographql.apollo3.ast.withNullability
Expand All @@ -15,4 +16,15 @@ class GQLTest {

assertEquals("[String]!", newType.pretty())
}

@Test
fun nullability() {
try {
"[[[String]]]".parseAsGQLType().getOrThrow().withNullability("[[[[!]]]]".parseAsGQLNullability().getOrThrow())
error("an exception was expected")
} catch (e: Exception) {
assertEquals(true, e.message?.contains("the nullability list dimension exceeds the one of the field type"))
}

}
}
Empty file.

This file was deleted.

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 @@ -3,6 +3,7 @@ query Foo {
nonNullable?
# too much nesting
nullable[!]!
# too much nesting
a0: deepList[[[[!]]]]
# this is ok
a1: deepList[[!]]
Expand Down
3 changes: 3 additions & 0 deletions tests/ccn/src/main/graphql/operation.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ query GetList {
enemies[!]! {
name
}
frenemies: enemies[]! {
name
}
}
}
4 changes: 3 additions & 1 deletion tests/ccn/src/test/kotlin/test/CcnTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class CcnTest {
{
"name": "nullability"
}
]
],
"frenemies": []
}
}
}
Expand All @@ -54,5 +55,6 @@ class CcnTest {
}
assertEquals(null, response.data!!.user!!.friends[0]?.name)
assertEquals("nullability", response.data!!.user!!.enemies[0].name)
assertEquals(0, response.data!!.user!!.frenemies.size)
}
}