Skip to content

Commit c96bf6c

Browse files
authored
[federation] cleanup validation rules (#1581)
Cleanup field set validation rules based on Federation v2 functionality. Validator now verifies: - @key, @provides and @requires field sets reference existing fields - @requires references @external fields - @provides references an object - field sets cannot reference unions - list and interfaces can only be referenced from `@requires` and `@provides`
1 parent 170aa5e commit c96bf6c

File tree

58 files changed

+1230
-1586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1230
-1586
lines changed

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/GraphQLDirectiveContainerExtensions.kt

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

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/DirectiveInfo.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,10 +24,12 @@ internal data class DirectiveInfo(
2424
val directiveName: String,
2525
val fieldSet: String,
2626
val typeName: String
27-
)
27+
) {
28+
private val formattedString: String by lazy {
29+
"@$directiveName(fields = \"$fieldSet\") directive on $typeName"
30+
}
2831

29-
/**
30-
* Extension method to get the GraphQL schema format
31-
* of the directive info that we print to errors.
32-
*/
33-
internal fun DirectiveInfo.getErrorString() = "@$directiveName(fields = $fieldSet) directive on $typeName"
32+
override fun toString(): String {
33+
return formattedString
34+
}
35+
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/FederatedSchemaValidator.kt

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616

1717
package com.expediagroup.graphql.generator.federation.validation
1818

19-
import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_NAME
2019
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
2120
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
2221
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
2322
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
24-
import com.expediagroup.graphql.generator.federation.extensions.isFederatedType
2523
import graphql.schema.GraphQLAppliedDirective
24+
import graphql.schema.GraphQLDirectiveContainer
2625
import graphql.schema.GraphQLFieldDefinition
2726
import graphql.schema.GraphQLInterfaceType
2827
import graphql.schema.GraphQLObjectType
2928
import graphql.schema.GraphQLType
29+
import graphql.schema.GraphQLTypeReference
3030
import graphql.schema.GraphQLTypeUtil
3131

3232
/**
@@ -38,11 +38,11 @@ internal class FederatedSchemaValidator {
3838
* Validates target GraphQLType whether it is a valid federated object.
3939
*
4040
* Verifies:
41-
* - base type doesn't declare any @external fields
42-
* - @key directive references existing fields
43-
* - @key directive on extended types references @external fields
44-
* - @requires directive is only applicable on extended types and references @external fields
45-
* - @provides directive references valid @external fields
41+
* - @key, @provides and @requires field sets reference existing fields
42+
* - @requires references @external fields
43+
* - @provides references an object
44+
* - field sets cannot reference unions
45+
* - list and interfaces can only be referenced from `@requires` and `@provides`
4646
*/
4747
internal fun validateGraphQLType(type: GraphQLType) {
4848
val unwrappedType = GraphQLTypeUtil.unwrapAll(type)
@@ -56,28 +56,37 @@ internal class FederatedSchemaValidator {
5656
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directiveMap: Map<String, List<GraphQLAppliedDirective>>) {
5757
val errors = mutableListOf<String>()
5858
val fieldMap = fields.associateBy { it.name }
59-
val extendedType = directiveMap.containsKey(EXTENDS_DIRECTIVE_NAME)
60-
61-
// [OK] @key directive is specified
62-
// [OK] @key references valid existing fields
63-
// [OK] @key on @extended type references @external fields
64-
// [ERROR] @key references fields resulting in list
65-
// [ERROR] @key references fields resulting in union
66-
// [ERROR] @key references fields resulting in interface
67-
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap, extendedType))
6859

60+
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap))
6961
for (field in fields) {
7062
if (field.getAppliedDirective(REQUIRES_DIRECTIVE_NAME) != null) {
71-
errors.addAll(validateDirective("$federatedType.${field.name}", REQUIRES_DIRECTIVE_NAME, field.allAppliedDirectivesByName, fieldMap, extendedType))
63+
errors.addAll(validateDirective("$federatedType.${field.name}", REQUIRES_DIRECTIVE_NAME, field.allAppliedDirectivesByName, fieldMap))
7264
}
7365

7466
if (field.getAppliedDirective(PROVIDES_DIRECTIVE_NAME) != null) {
75-
errors.addAll(validateProvidesDirective(federatedType, field))
67+
when (val returnType = GraphQLTypeUtil.unwrapAll(field.type)) {
68+
is GraphQLObjectType -> {
69+
val returnTypeFields = returnType.fieldDefinitions.associateBy { it.name }
70+
errors.addAll(
71+
validateDirective(
72+
"$federatedType.${field.name}",
73+
PROVIDES_DIRECTIVE_NAME,
74+
field.allAppliedDirectivesByName,
75+
returnTypeFields
76+
)
77+
)
78+
}
79+
// skip validation for nested object types as they are still under construction
80+
is GraphQLTypeReference -> continue
81+
else -> errors.add("@provides directive is specified on a $federatedType.${field.name} field but it does not return an object type")
82+
}
7683
}
7784
}
7885

7986
if (errors.isNotEmpty()) {
8087
throw InvalidFederatedSchema(errors)
8188
}
8289
}
90+
91+
private fun GraphQLDirectiveContainer.isFederatedType() = this.getAppliedDirectives(KEY_DIRECTIVE_NAME).isNotEmpty()
8392
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.expediagroup.graphql.generator.federation.validation
2+
3+
/**
4+
* Simple representation of a FieldSet selection set.
5+
*/
6+
internal data class FieldSetSelection(val field: String, val subSelections: MutableList<FieldSetSelection> = ArrayList())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2022 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.federation.validation
18+
19+
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
20+
import graphql.schema.GraphQLEnumType
21+
import graphql.schema.GraphQLInterfaceType
22+
import graphql.schema.GraphQLList
23+
import graphql.schema.GraphQLObjectType
24+
import graphql.schema.GraphQLScalarType
25+
import graphql.schema.GraphQLType
26+
import graphql.schema.GraphQLTypeUtil
27+
import graphql.schema.GraphQLUnionType
28+
29+
internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection: FieldSetSelection, targetType: GraphQLType, errors: MutableList<String>) {
30+
when (val unwrapped = GraphQLTypeUtil.unwrapNonNull(targetType)) {
31+
is GraphQLScalarType, is GraphQLEnumType -> {
32+
if (selection.subSelections.isNotEmpty()) {
33+
errors.add("$validatedDirective specifies invalid field set - field set specifies selection set on a leaf node, field=${selection.field}")
34+
}
35+
}
36+
is GraphQLUnionType -> errors.add("$validatedDirective specifies invalid field set - field set references GraphQLUnionType, field=${selection.field}")
37+
is GraphQLList -> {
38+
if (KEY_DIRECTIVE_NAME == validatedDirective.directiveName) {
39+
errors.add("$validatedDirective specifies invalid field set - field set references GraphQLList, field=${selection.field}")
40+
} else {
41+
validateFieldSelection(validatedDirective, selection, GraphQLTypeUtil.unwrapOne(targetType), errors)
42+
}
43+
}
44+
is GraphQLInterfaceType -> {
45+
if (KEY_DIRECTIVE_NAME == validatedDirective.directiveName) {
46+
errors.add("$validatedDirective specifies invalid field set - field set references GraphQLInterfaceType, field=${selection.field}")
47+
} else if (selection.subSelections.isEmpty()) {
48+
errors.add("$validatedDirective specifies invalid field set - ${selection.field} interface does not specify selection set")
49+
} else {
50+
validateFieldSetSelection(
51+
validatedDirective,
52+
selection.subSelections,
53+
unwrapped.fieldDefinitions.associateBy { it.name },
54+
errors
55+
)
56+
}
57+
}
58+
is GraphQLObjectType -> {
59+
if (selection.subSelections.isEmpty()) {
60+
errors.add("$validatedDirective specifies invalid field set - ${selection.field} object does not specify selection set")
61+
} else {
62+
validateFieldSetSelection(
63+
validatedDirective,
64+
selection.subSelections,
65+
unwrapped.fieldDefinitions.associateBy { it.name },
66+
errors
67+
)
68+
}
69+
}
70+
}
71+
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateDirective.kt

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.expediagroup.graphql.generator.federation.validation
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
20+
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
2021
import com.expediagroup.graphql.generator.federation.types.FIELD_SET_ARGUMENT_NAME
2122
import graphql.schema.GraphQLAppliedDirective
2223
import graphql.schema.GraphQLFieldDefinition
@@ -26,7 +27,6 @@ internal fun validateDirective(
2627
targetDirective: String,
2728
directiveMap: Map<String, List<GraphQLAppliedDirective>>,
2829
fieldMap: Map<String, GraphQLFieldDefinition>,
29-
extendedType: Boolean
3030
): List<String> {
3131
val validationErrors = mutableListOf<String>()
3232
val directives = directiveMap[targetDirective]
@@ -35,20 +35,57 @@ internal fun validateDirective(
3535
validationErrors.add("@$targetDirective directive is missing on federated $validatedType type")
3636
} else {
3737
for (directive in directives) {
38-
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value
39-
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
38+
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value ?: ""
39+
val fieldSet = fieldSetValue.split(" ").filter { it.isNotEmpty() }
4040
if (fieldSet.isEmpty()) {
4141
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
4242
} else {
43-
// validate directive field set selection
4443
val directiveInfo = DirectiveInfo(
4544
directiveName = targetDirective,
46-
fieldSet = fieldSet.joinToString(" "),
45+
fieldSet = fieldSetValue,
4746
typeName = validatedType
4847
)
49-
validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors)
48+
validateFieldSet(directiveInfo, fieldSet)
49+
val selections = parseFieldSet(directiveInfo, fieldSet.iterator())
50+
validateFieldSetSelection(directiveInfo, selections, fieldMap, validationErrors)
5051
}
5152
}
5253
}
5354
return validationErrors
5455
}
56+
57+
private fun validateFieldSet(directiveInfo: DirectiveInfo, fieldSet: List<String>) {
58+
var isOpen = 0
59+
for (field in fieldSet) {
60+
when (field) {
61+
"{" -> isOpen++
62+
"}" -> isOpen--
63+
}
64+
65+
if (isOpen < 0) {
66+
break
67+
}
68+
}
69+
70+
if (isOpen != 0) {
71+
throw InvalidFederatedSchema(listOf("$directiveInfo specifies malformed field set: ${directiveInfo.fieldSet}"))
72+
}
73+
}
74+
75+
internal fun parseFieldSet(directiveInfo: DirectiveInfo, iterator: Iterator<String>): List<FieldSetSelection> {
76+
val selections = mutableListOf<FieldSetSelection>()
77+
var previous: FieldSetSelection? = null
78+
while (iterator.hasNext()) {
79+
when (val currentField = iterator.next()) {
80+
"{" -> previous?.subSelections?.addAll(parseFieldSet(directiveInfo, iterator))
81+
"}" -> break
82+
else -> {
83+
val current = FieldSetSelection(currentField)
84+
selections.add(current)
85+
86+
previous = current
87+
}
88+
}
89+
}
90+
return selections
91+
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateFieldSelection.kt

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

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateFieldSet.kt

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

0 commit comments

Comments
 (0)