Skip to content

Commit fb6245f

Browse files
authored
Allow explicit CCN syntax (#5148)
* allow explicit CCN syntax * update test fixtures * better file names * centralize validation and fix tests * internal all the things
1 parent be9f47b commit fb6245f

File tree

13 files changed

+105
-74
lines changed

13 files changed

+105
-74
lines changed

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ fun String.parseAsGQLType(options: ParserOptions = ParserOptions.Default): GQLRe
163163
}
164164
}
165165

166+
internal fun String.parseAsGQLNullability(options: ParserOptions = ParserOptions.Default): GQLResult<GQLNullability> {
167+
@Suppress("DEPRECATION")
168+
check (!options.useAntlr)
169+
return parseInternal(null, options) { parseNullability() ?: error("No nullability") }
170+
}
171+
166172
fun String.parseAsGQLSelections(options: ParserOptions = ParserOptions.Default): GQLResult<List<GQLSelection>> {
167173
@Suppress("DEPRECATION")
168174
return if (options.useAntlr) {

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,15 +1529,17 @@ class GQLNullDesignator(override val sourceLocation: SourceLocation? = null) : G
15291529
@ApolloExperimental
15301530
class GQLListNullability(
15311531
override val sourceLocation: SourceLocation? = null,
1532-
val itemNullability: GQLNullability,
1532+
val itemNullability: GQLNullability?,
15331533
val selfNullability: GQLNullability?,
15341534
) : GQLNullability {
15351535
override val children: List<GQLNode>
1536-
get() = listOf(itemNullability)
1536+
get() = listOfNotNull(itemNullability)
15371537

15381538
override fun writeInternal(writer: SDLWriter) {
15391539
writer.write("[")
1540-
writer.write(itemNullability)
1540+
if (itemNullability != null) {
1541+
writer.write(itemNullability)
1542+
}
15411543
writer.write("]")
15421544
if (selfNullability != null) {
15431545
writer.write(selfNullability)
@@ -1552,7 +1554,7 @@ class GQLListNullability(
15521554

15531555
fun copy(
15541556
sourceLocation: SourceLocation? = this.sourceLocation,
1555-
ofNullability: GQLNullability = this.itemNullability,
1557+
ofNullability: GQLNullability? = this.itemNullability,
15561558
selfNullability: GQLNullability? = this.selfNullability,
15571559
): GQLListNullability {
15581560
return GQLListNullability(

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqltype.kt

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ internal fun isVariableUsageAllowed(variableDefinition: GQLVariableDefinition, u
3535
}
3636

3737
internal fun areTypesCompatible(variableType: GQLType, locationType: GQLType): Boolean {
38-
return if(locationType is GQLNonNullType) {
38+
return if (locationType is GQLNonNullType) {
3939
if (variableType !is GQLNonNullType) {
4040
false
4141
} else {
4242
areTypesCompatible(variableType.type, locationType.type)
4343
}
4444
} else if (variableType is GQLNonNullType) {
4545
areTypesCompatible(variableType.type, locationType)
46-
} else if (locationType is GQLListType){
46+
} else if (locationType is GQLListType) {
4747
if (variableType !is GQLListType) {
4848
false
4949
} else {
@@ -84,43 +84,77 @@ internal fun GQLType.isOutputType(typeDefinitions: Map<String, GQLTypeDefinition
8484
}
8585
}
8686

87-
private fun GQLNullability?.selfNullability(): GQLNullability? {
88-
return when (this) {
89-
is GQLListNullability -> this.selfNullability
90-
else -> this
87+
private fun GQLType.withItemNullability(itemNullability: GQLNullability?, validation: NullabilityValidation): GQLType {
88+
if (itemNullability == null) {
89+
return this
9190
}
92-
}
9391

94-
private fun GQLType.withListNullability(nullability: GQLNullability?): GQLType {
95-
if (this is GQLListType && nullability is GQLListNullability) {
96-
return copy(type = type.withNullability(nullability.itemNullability))
97-
} else if (this is GQLListType && nullability !is GQLListNullability) {
98-
return this
99-
} else if (this !is GQLListType && nullability is GQLListNullability) {
100-
return this
101-
} else if (this !is GQLListType && nullability !is GQLListNullability) {
102-
return this
103-
} else {
104-
error("")
92+
if (this !is GQLListType) {
93+
when (validation) {
94+
is NullabilityValidationThrow -> {
95+
check(this is GQLListType) {
96+
"Cannot apply nullability, the nullability list dimension exceeds the one of the field type."
97+
}
98+
}
99+
100+
is NullabilityValidationIgnore -> {
101+
return this
102+
103+
}
104+
105+
is NullabilityValidationRegister -> {
106+
validation.issues.add(
107+
Issue.ValidationError(
108+
"Cannot apply nullability on '${validation.fieldName}', the nullability list dimension exceeds the one of the field type.",
109+
itemNullability.sourceLocation,
110+
)
111+
)
112+
return this
113+
}
114+
}
105115
}
116+
117+
return this.copy(type = type.withNullability(itemNullability, validation))
106118
}
107119

108120
@ApolloExperimental
109121
fun GQLType.withNullability(nullability: GQLNullability?): GQLType {
110-
val selfNullability = nullability.selfNullability()
122+
return withNullability(nullability, NullabilityValidationThrow)
123+
}
124+
125+
internal sealed interface NullabilityValidation
126+
127+
internal object NullabilityValidationIgnore: NullabilityValidation
128+
internal object NullabilityValidationThrow: NullabilityValidation
129+
internal class NullabilityValidationRegister(val issues: MutableList<Issue>, val fieldName: String): NullabilityValidation
111130

112-
if (this is GQLNonNullType && selfNullability == null) {
113-
return this.copy(type = type.withListNullability(nullability))
131+
internal fun GQLType.withNullability(nullability: GQLNullability?, validation: NullabilityValidation): GQLType {
132+
val selfNullability: GQLNullability?
133+
val itemNullability: GQLNullability?
134+
135+
when (nullability) {
136+
is GQLListNullability -> {
137+
selfNullability = nullability.selfNullability
138+
itemNullability = nullability.itemNullability
139+
}
140+
141+
else -> {
142+
selfNullability = nullability
143+
itemNullability = null
144+
}
145+
}
146+
return if (this is GQLNonNullType && selfNullability == null) {
147+
this.copy(type = type.withItemNullability(itemNullability, validation))
114148
} else if (this is GQLNonNullType && selfNullability is GQLNonNullDesignator) {
115-
return this.copy(type = type.withListNullability(nullability))
149+
this.copy(type = type.withItemNullability(itemNullability, validation))
116150
} else if (this is GQLNonNullType && selfNullability is GQLNullDesignator) {
117-
return this.type.withListNullability(nullability)
151+
this.type.withItemNullability(itemNullability, validation)
118152
} else if (this !is GQLNonNullType && selfNullability == null) {
119-
return this.withListNullability(nullability)
153+
this.withItemNullability(itemNullability, validation)
120154
} else if (this !is GQLNonNullType && selfNullability is GQLNonNullDesignator) {
121-
return GQLNonNullType(type = this.withListNullability(nullability))
155+
GQLNonNullType(type = this.withItemNullability(itemNullability, validation))
122156
} else if (this !is GQLNonNullType && selfNullability is GQLNullDesignator) {
123-
return this.withListNullability(nullability)
157+
this.withItemNullability(itemNullability, validation)
124158
} else {
125159
error("")
126160
}

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/ExecutableValidationScope.kt

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,15 @@ import com.apollographql.apollo3.ast.GQLFragmentDefinition
1212
import com.apollographql.apollo3.ast.GQLFragmentSpread
1313
import com.apollographql.apollo3.ast.GQLInlineFragment
1414
import com.apollographql.apollo3.ast.GQLIntValue
15-
import com.apollographql.apollo3.ast.GQLListNullability
1615
import com.apollographql.apollo3.ast.GQLListType
1716
import com.apollographql.apollo3.ast.GQLListValue
1817
import com.apollographql.apollo3.ast.GQLNamedType
1918
import com.apollographql.apollo3.ast.GQLNode
2019
import com.apollographql.apollo3.ast.GQLNonNullType
2120
import com.apollographql.apollo3.ast.GQLNullValue
22-
import com.apollographql.apollo3.ast.GQLNullability
2321
import com.apollographql.apollo3.ast.GQLObjectTypeDefinition
2422
import com.apollographql.apollo3.ast.GQLObjectValue
2523
import com.apollographql.apollo3.ast.GQLOperationDefinition
26-
import com.apollographql.apollo3.ast.GQLNullDesignator
27-
import com.apollographql.apollo3.ast.GQLNonNullDesignator
2824
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
2925
import com.apollographql.apollo3.ast.GQLSelection
3026
import com.apollographql.apollo3.ast.GQLStringValue
@@ -34,6 +30,8 @@ import com.apollographql.apollo3.ast.GQLValue
3430
import com.apollographql.apollo3.ast.GQLVariableValue
3531
import com.apollographql.apollo3.ast.InferredVariable
3632
import com.apollographql.apollo3.ast.Issue
33+
import com.apollographql.apollo3.ast.NullabilityValidationIgnore
34+
import com.apollographql.apollo3.ast.NullabilityValidationRegister
3735
import com.apollographql.apollo3.ast.Schema
3836
import com.apollographql.apollo3.ast.SourceLocation
3937
import com.apollographql.apollo3.ast.VariableUsage
@@ -218,15 +216,7 @@ internal class ExecutableValidationScope(
218216
}
219217

220218
if (nullability != null) {
221-
val typeListDimension = fieldDefinition.type.listDimension()
222-
val nullabilityListDimension = nullability.listDimension()
223-
if (typeListDimension < nullabilityListDimension) {
224-
registerIssue(
225-
message = "Cannot apply nullability on '$name', the nullability list dimension exceeds the one of the field type.",
226-
sourceLocation = nullability.sourceLocation
227-
)
228-
return
229-
}
219+
fieldDefinition.type.withNullability(nullability, NullabilityValidationRegister(issues, name))
230220
}
231221

232222
directives.forEach {
@@ -236,22 +226,6 @@ internal class ExecutableValidationScope(
236226
}
237227
}
238228

239-
private fun GQLType.listDimension(): Int {
240-
return when (this) {
241-
is GQLNonNullType -> this.type.listDimension()
242-
is GQLListType -> 1 + this.type.listDimension()
243-
else -> 0
244-
}
245-
}
246-
247-
private fun GQLNullability.listDimension(): Int {
248-
return when (this) {
249-
is GQLListNullability -> 1 + this.itemNullability.listDimension()
250-
is GQLNullDesignator -> 0
251-
is GQLNonNullDesignator -> 0
252-
}
253-
}
254-
255229
private fun GQLInlineFragment.validate(parentTypeDefinition: GQLTypeDefinition, selectionSetParent: GQLNode, path: String) {
256230
val tc = typeCondition?.name ?: parentTypeDefinition.name
257231
val inlineFragmentTypeDefinition = typeDefinitions[tc]
@@ -517,8 +491,8 @@ internal class ExecutableValidationScope(
517491
val fieldA = fieldWithParentA.field
518492
val fieldB = fieldWithParentB.field
519493

520-
val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability)
521-
val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability)
494+
val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability, NullabilityValidationIgnore)
495+
val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability, NullabilityValidationIgnore)
522496
if (typeA == null || typeB == null) {
523497
// will be caught by other validation rules
524498
return

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ internal class Parser(
4242
return parseTopLevel(::parseTypeInternal)
4343
}
4444

45+
fun parseNullability(): GQLNullability? {
46+
return parseTopLevel(::parseNullabilityInternal)
47+
}
48+
4549
private fun advance() {
4650
lastToken = token
4751
if (lookaheadToken != null) {
@@ -217,7 +221,7 @@ internal class Parser(
217221
val arguments = parseArguments(const = false)
218222
var nullability: GQLNullability? = null
219223
if (allowClientControlledNullability) {
220-
nullability = parseNullability()
224+
nullability = parseNullabilityInternal()
221225
}
222226
val directives = parseDirectives(const = false)
223227

@@ -253,7 +257,7 @@ internal class Parser(
253257
}
254258
}
255259

256-
private fun parseNullability(): GQLNullability? {
260+
private fun parseNullabilityInternal(): GQLNullability? {
257261
return when (token) {
258262
is Token.LeftBracket -> {
259263
parseListNullability()
@@ -265,17 +269,12 @@ internal class Parser(
265269
}
266270

267271
private fun parseListNullability(): GQLListNullability {
268-
val start = token
269272
val sourceLocation = sourceLocation()
270273

271274
expectToken<Token.LeftBracket>()
272-
val ofNullability = parseNullability()
275+
val ofNullability = parseNullabilityInternal()
273276
expectToken<Token.RightBracket>()
274277

275-
if (ofNullability == null) {
276-
throw ParserException("List nullability must not be empty", start)
277-
}
278-
279278
return GQLListNullability(
280279
sourceLocation = sourceLocation,
281280
itemNullability = ofNullability,

libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/GQLTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.apollographql.apollo3.graphql.ast.test
22

33
import com.apollographql.apollo3.ast.GQLListNullability
44
import com.apollographql.apollo3.ast.GQLNullDesignator
5+
import com.apollographql.apollo3.ast.parseAsGQLNullability
56
import com.apollographql.apollo3.ast.parseAsGQLType
67
import com.apollographql.apollo3.ast.pretty
78
import com.apollographql.apollo3.ast.withNullability
@@ -15,4 +16,15 @@ class GQLTest {
1516

1617
assertEquals("[String]!", newType.pretty())
1718
}
19+
20+
@Test
21+
fun nullability() {
22+
try {
23+
"[[[String]]]".parseAsGQLType().getOrThrow().withNullability("[[[[!]]]]".parseAsGQLNullability().getOrThrow())
24+
error("an exception was expected")
25+
} catch (e: Exception) {
26+
assertEquals(true, e.message?.contains("the nullability list dimension exceeds the one of the field type"))
27+
}
28+
29+
}
1830
}

libraries/apollo-ast/test-fixtures/parser/ccn-empty-list.expected

Whitespace-only changes.

libraries/apollo-ast/test-fixtures/parser/ccn-invalid.expected

Lines changed: 0 additions & 2 deletions
This file was deleted.

libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.expected

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)