Skip to content

Commit

Permalink
[generator] support repeatable directives
Browse files Browse the repository at this point in the history
Kotlin 1.6 provides support for repeatable annotations with `RUNTIME` retention. This allows to finally support repeatable directives.

In order to make your directives repeatable, you need to annotate it with `kotlin.annotation.Repeatable` annotation.

```kotlin
@repeatable
@GraphQLDirective
annotation class MyRepeatableDirective(val value: String)
```

Generates the above directive as

```graphql
directive @myRepeatableDirective(value: String!) repeatable on OBJECT | INTERFACE
```

Related Issues:
* resolves ExpediaGroup#590
* depends on ExpediaGroup#1358
  • Loading branch information
Dariusz Kuc committed Feb 13, 2022
1 parent 15c45dd commit a71aed4
Show file tree
Hide file tree
Showing 30 changed files with 276 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -147,7 +147,7 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTy
return originalSchema.allTypesAsList
.asSequence()
.filterIsInstance<GraphQLObjectType>()
.filter { type -> type.getDirective(KEY_DIRECTIVE_NAME) != null }
.filter { type -> type.hasDirective(KEY_DIRECTIVE_NAME) }
.map { it.name }
.toSet()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,9 +28,6 @@ import graphql.introspection.Introspection.DirectiveLocation
* well as all the corresponding federated (i.e. extended) types. Key fields specified in the directive field set should correspond to a valid field on the underlying GraphQL interface/object.
* Federated extended types should also instrument all the referenced key fields with @external directive.
*
* NOTE: The Federation spec specifies that multiple @key directives can be applied on the field. The GraphQL spec has been recently changed to allow this behavior,
* but we are currently blocked and are tracking progress in [this issue](https://github.com/ExpediaGroup/graphql-kotlin/issues/590).
*
* Example:
* Given
*
Expand All @@ -55,6 +52,7 @@ import graphql.introspection.Introspection.DirectiveLocation
* @see ExtendsDirective
* @see ExternalDirective
*/
@Repeatable
@GraphQLDirective(
name = KEY_DIRECTIVE_NAME,
description = KEY_DIRECTIVE_DESCRIPTION,
Expand All @@ -70,4 +68,5 @@ internal val KEY_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schem
.description(KEY_DIRECTIVE_DESCRIPTION)
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE)
.argument(FIELD_SET_ARGUMENT)
.repeatable(true)
.build()
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,6 @@ import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIV
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
import graphql.schema.GraphQLDirectiveContainer

internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirective(KEY_DIRECTIVE_NAME) != null || isExtendedType()
internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirectives(KEY_DIRECTIVE_NAME).isNotEmpty() || isExtendedType()

internal fun GraphQLDirectiveContainer.isExtendedType() = this.getDirective(EXTENDS_DIRECTIVE_NAME) != null
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,7 +27,7 @@ internal fun GraphQLSchema.addDirectivesIfNotPresent(directives: List<GraphQLDir
val newBuilder = GraphQLSchema.newSchema(this)

directives.forEach {
if (this.getDirective(it.name) == null) {
if (!this.allDirectivesByName.containsKey(it.name)) {
newBuilder.additionalDirective(it)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,24 +48,24 @@ internal class FederatedSchemaValidator {
internal fun validateGraphQLType(type: GraphQLType) {
val unwrappedType = GraphQLTypeUtil.unwrapAll(type)
if (unwrappedType is GraphQLObjectType && unwrappedType.isFederatedType()) {
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.directivesByName)
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allDirectivesByName)
} else if (unwrappedType is GraphQLInterfaceType && unwrappedType.isFederatedType()) {
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.directivesByName)
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allDirectivesByName)
}
}

private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directives: Map<String, GraphQLDirective>) {
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directiveMap: Map<String, List<GraphQLDirective>>) {
val errors = mutableListOf<String>()
val fieldMap = fields.associateBy { it.name }
val extendedType = directives.containsKey(EXTENDS_DIRECTIVE_NAME)
val extendedType = directiveMap.containsKey(EXTENDS_DIRECTIVE_NAME)

// [OK] @key directive is specified
// [OK] @key references valid existing fields
// [OK] @key on @extended type references @external fields
// [ERROR] @key references fields resulting in list
// [ERROR] @key references fields resulting in union
// [ERROR] @key references fields resulting in interface
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directives, fieldMap, extendedType))
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap, extendedType))

for (field in fields) {
if (field.getDirective(REQUIRES_DIRECTIVE_NAME) != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,28 +24,30 @@ import graphql.schema.GraphQLFieldDefinition
internal fun validateDirective(
validatedType: String,
targetDirective: String,
directives: Map<String, GraphQLDirective>,
directiveMap: Map<String, List<GraphQLDirective>>,
fieldMap: Map<String, GraphQLFieldDefinition>,
extendedType: Boolean
): List<String> {
val validationErrors = mutableListOf<String>()
val directive = directives[targetDirective]
val directives = directiveMap[targetDirective]

if (directive == null) {
if (directives == null) {
validationErrors.add("@$targetDirective directive is missing on federated $validatedType type")
} else {
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
if (fieldSet.isEmpty()) {
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
} else {
// validate directive field set selection
val directiveInfo = DirectiveInfo(
directiveName = targetDirective,
fieldSet = fieldSet.joinToString(" "),
typeName = validatedType
)
validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors)
for (directive in directives) {
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
if (fieldSet.isEmpty()) {
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
} else {
// validate directive field set selection
val directiveInfo = DirectiveInfo(
directiveName = targetDirective,
fieldSet = fieldSet.joinToString(" "),
typeName = validatedType
)
validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors)
}
}
}
return validationErrors
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,7 +40,7 @@ internal fun validateProvidesDirective(federatedType: String, field: GraphQLFiel
validateDirective(
"$federatedType.${field.name}",
PROVIDES_DIRECTIVE_NAME,
field.directivesByName,
field.allDirectivesByName,
returnTypeFields,
true
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,7 +25,7 @@ import graphql.schema.GraphQLFieldDefinition
internal fun validateRequiresDirective(validatedType: String, validatedField: GraphQLFieldDefinition, fieldMap: Map<String, GraphQLFieldDefinition>, extendedType: Boolean): List<String> {
val errors = mutableListOf<String>()
if (extendedType) {
errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.directivesByName, fieldMap, extendedType))
errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.allDirectivesByName, fieldMap, extendedType))
} else {
errors.add("base $validatedType type has fields marked with @requires directive, validatedField=${validatedField.name}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -58,7 +58,7 @@ private val FEDERATED_SDL =
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
"Space separated list of primary keys needed to access federated object"
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
"Specifies required input field set from the base type for a resolver"
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
Expand All @@ -75,18 +75,20 @@ private val FEDERATED_SDL =
url: String!
) on SCALAR
interface Product @extends @key(fields : "id") {
interface Product @extends @key(fields : "id") @key(fields : "upc") {
id: String! @external
reviews: [Review!]!
upc: String! @external
}
union _Entity = Book | User
type Book implements Product @extends @key(fields : "id") {
type Book implements Product @extends @key(fields : "id") @key(fields : "upc") {
author: User! @provides(fields : "name")
id: String! @external
reviews: [Review!]!
shippingCost: String! @requires(fields : "weight")
upc: String! @external
weight: Float! @external
}
Expand Down Expand Up @@ -136,7 +138,7 @@ class FederatedSchemaGeneratorTest {
assertEquals(FEDERATED_SDL, schema.print().trim())
val productType = schema.getObjectType("Book")
assertNotNull(productType)
assertNotNull(productType.getDirective(KEY_DIRECTIVE_NAME))
assertNotNull(productType.hasDirective(KEY_DIRECTIVE_NAME))

val entityUnion = schema.getType(ENTITY_UNION_NAME) as? GraphQLUnionType
assertNotNull(entityUnion)
Expand Down Expand Up @@ -185,7 +187,7 @@ class FederatedSchemaGeneratorTest {
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
"Space separated list of primary keys needed to access federated object"
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
"Marks target object as extending part of the federated schema"
directive @extends on OBJECT | INTERFACE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._1
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._01

import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._2
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._02

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._3
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._03

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._4
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._04

import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
import com.expediagroup.graphql.generator.federation.directives.FieldSet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._5
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._05

import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective
import com.expediagroup.graphql.generator.federation.directives.FieldSet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._6
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._06

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._7
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._07

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._8
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._08

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.key.failure._9
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._09

import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Expand Down
Loading

0 comments on commit a71aed4

Please sign in to comment.