Skip to content

Commit 39d412c

Browse files
gscheibeldariuszkuc
authored andcommitted
Standardize the creation and usage of custom directives (#10)
* Standardize the creation and usage of custom directives * Remove @GraphQLExperimental * Pruning builtin directive support * Add custom directive to the schema definition * Add directive created from object type to the schema * Assert the present of custom directive in the schema * Add naming convention to the directive doc * Applying directive naming convention to the deprecated directive
1 parent 6416453 commit 39d412c

File tree

10 files changed

+129
-78
lines changed

10 files changed

+129
-78
lines changed

README.md

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,10 @@ schema {
528528

529529
type TopLevelQuery {
530530

531-
"""DEPRECATED: old query that should not be used always returns false"""
531+
"""old query that should not be used always returns false
532+
533+
Directives: deprecated
534+
"""
532535
simpleDeprecatedQuery: Boolean! @deprecated(reason: "this query is deprecated, replace with shinyNewQuery")
533536

534537
"""new query that always returns true"""
@@ -538,34 +541,48 @@ type TopLevelQuery {
538541

539542
While you can deprecate any fields/methods in your code, GraphQL only supports deprecation directive on the queries, mutations and output types. All deprecated objects will have "DEPRECATED" prefix in their description.
540543

541-
### `@GraphQLExperimental`
542544

543-
Schemas are often evoling over time and while some feature are getting removed others can be added. Some of those new features may be experimental meaning they are still being tested out and can change without any notice. Functions annotated with `@GraphQLExperimental` annotations will have set `@experimental` directive. You can access those directives during instrumentation to provide some custom logic. Experimental methods will also have `EXPERIMENTAL` prefix in their description.
545+
### Custom directives
546+
547+
Custom directives can be added to the schema using custom annotations:
544548

545549
```kotlin
546-
class SimpleQuery {
550+
@GraphQLDirective(
551+
name = "Awesome",
552+
description = "This element is great",
553+
locations = [FIELD, FIELD_DEFINITION]
554+
)
555+
annotation class AwesomeDirective(val value: String)
547556

548-
/*
549-
* NOTE: currently GraphQL directives are not exposed in the schema through introspection but they
550-
* are available on the server that exposes the schema
551-
*/
552-
@GraphQLExperimental("this is an experimental feature")
553-
@GraphQLDescription("echoes back the msg")
554-
fun experimentalEcho(msg: String): String = msg
557+
class MyQuery {
558+
@AwesomeDirective("cool stuff")
559+
val somethingGreat: String = "Hello World"
555560
}
556561
```
557562

563+
The directive will then added to the schema as:
558564

559-
Will translate to
560565
```graphql
561-
type TopLevelQuery {
562-
"""EXPERIMENTAL: echoes back the msg"""
563-
experimentalEcho(msg: String!): String!
566+
# This element is great
567+
directive @awesome(value: String) on FIELD | FIELD_DEFINITION
568+
569+
# Directives: awesome
570+
type MyQuery {
571+
somethingGreat: String @awesome("cool stuff")
564572
}
565573
```
566574

575+
Directives can be added to various places in the schema, to see the full list see the [graphql.introspection.Introspection.DirectiveLocation enum](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/introspection/Introspection.java#L296) from graphql-java.
576+
567577
Note that GraphQL directives are currently not available through introspection. See: https://github.com/facebook/graphql/issues/300 and https://github.com/graphql-java/graphql-java/issues/1017 for more details.
568578

579+
#### Naming Convention
580+
581+
As described in the example above, the directive name in the schema will by default come from the `@GraphQLDirective.name` attribute.
582+
If this value is not specified like an empty string, the directive name will be the name of the annotated annotation (eg: `AwesomeDirective`).
583+
584+
For more readibility, the name used by the schema will be decapitalized so `Awesome` becomes `awesome` and `AwesomeDirective` would be `awesomeDirective`.
585+
569586
## Configuration
570587

571588
### Documentation Enforcement
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.expedia.graphql.sample.directives
2+
3+
import com.expedia.graphql.annotations.GraphQLDirective
4+
import graphql.introspection.Introspection.DirectiveLocation.FIELD
5+
6+
@GraphQLDirective(
7+
name = "CustomMarkup",
8+
description = "Add markup to a field",
9+
locations = [FIELD]
10+
)
11+
annotation class CustomDirective

example/src/main/kotlin/com.expedia.graphql.sample/query/SimpleQuery.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.expedia.graphql.sample.query
22

33
import com.expedia.graphql.annotations.GraphQLDescription
4-
import com.expedia.graphql.annotations.GraphQLExperimental
54
import com.expedia.graphql.annotations.GraphQLIgnore
5+
import com.expedia.graphql.sample.directives.CustomDirective
66
import org.springframework.stereotype.Component
77
import java.util.Random
88

@@ -19,11 +19,6 @@ class SimpleQuery: Query {
1919
@GraphQLDescription("new query that always returns true")
2020
fun shinyNewQuery(): Boolean = true
2121

22-
/*
23-
* NOTE: currently GraphQL directives are not exposed in the schema through introspection but they
24-
* are available on the server that exposes the schema
25-
*/
26-
@GraphQLExperimental("this is an experimental feature")
2722
@GraphQLDescription("echoes back the msg")
2823
fun experimentalEcho(msg: String): String = msg
2924

@@ -33,7 +28,9 @@ class SimpleQuery: Query {
3328
private fun privateFunctionsAreNotVisible() = "ignored private function"
3429

3530
@GraphQLDescription("performs some operation")
36-
fun doSomething(@GraphQLDescription("super important value") value: Int): Boolean = true
31+
@CustomDirective
32+
fun doSomething(@GraphQLDescription("super important value")
33+
value: Int): Boolean = true
3734

3835
@GraphQLDescription("generates pseudo random int and returns it if it is less than 50")
3936
fun generateNullableNumber(): Int? {
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
package com.expedia.graphql.annotations
22

3+
import graphql.introspection.Introspection.DirectiveLocation
4+
import graphql.introspection.Introspection.DirectiveLocation.QUERY
5+
import graphql.introspection.Introspection.DirectiveLocation.MUTATION
6+
import graphql.introspection.Introspection.DirectiveLocation.FIELD
7+
import graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION
8+
import graphql.introspection.Introspection.DirectiveLocation.OBJECT
9+
310
/**
411
* Meta annotation used to denote an annotation as a GraphQL directive.
512
*/
613
@Target(AnnotationTarget.ANNOTATION_CLASS)
7-
annotation class GraphQLDirective(val name: String = "")
14+
annotation class GraphQLDirective(
15+
val name: String = "",
16+
val description: String = "",
17+
val locations: Array<DirectiveLocation> = [QUERY, MUTATION, FIELD, FIELD_DEFINITION, OBJECT]
18+
)

src/main/kotlin/com/expedia/graphql/annotations/GraphQLExperimental.kt

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

src/main/kotlin/com/expedia/graphql/schema/SchemaGenerator.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.expedia.graphql.schema
22

33
import com.expedia.graphql.TopLevelObjectDef
4-
import com.expedia.graphql.schema.directives.ExperimentalDirective
54
import com.expedia.graphql.schema.exceptions.ConflictingTypesException
65
import com.expedia.graphql.schema.exceptions.CouldNotGetNameOfEnumException
76
import com.expedia.graphql.schema.exceptions.TypeNotSupportedException
87
import com.expedia.graphql.schema.models.KGraphQLType
98
import graphql.TypeResolutionEnvironment
109
import graphql.schema.DataFetcher
1110
import graphql.schema.GraphQLArgument
11+
import graphql.schema.GraphQLDirective
1212
import graphql.schema.GraphQLEnumType
1313
import graphql.schema.GraphQLFieldDefinition
1414
import graphql.schema.GraphQLInputObjectField
@@ -40,11 +40,10 @@ internal class SchemaGenerator(
4040

4141
private val typesCache: MutableMap<String, KGraphQLType> = mutableMapOf()
4242
private val additionTypes = mutableSetOf<GraphQLType>()
43+
private val directives = mutableSetOf<GraphQLDirective>()
4344

4445
internal fun generate(): GraphQLSchema {
4546
val builder = generateWithReflection()
46-
builder.additionalDirective(ExperimentalDirective)
47-
builder.additionalDirectives(config.directives)
4847
return config.hooks.willBuildSchema(builder).build()
4948
}
5049

@@ -53,12 +52,13 @@ internal class SchemaGenerator(
5352
addQueries(builder)
5453
addMutations(builder)
5554
addAdditionalTypes(builder)
55+
addDirectives(builder)
5656
return builder
5757
}
5858

59-
private fun addAdditionalTypes(builder: GraphQLSchema.Builder) {
60-
builder.additionalTypes(additionTypes)
61-
}
59+
private fun addAdditionalTypes(builder: GraphQLSchema.Builder) = builder.additionalTypes(additionTypes)
60+
61+
private fun addDirectives(builder: GraphQLSchema.Builder)= builder.additionalDirectives(directives)
6262

6363
private fun addQueries(builder: GraphQLSchema.Builder) {
6464
val queryBuilder = GraphQLObjectType.Builder()
@@ -104,8 +104,9 @@ internal class SchemaGenerator(
104104
builder.deprecate(it)
105105
}
106106

107-
if (fn.isGraphQLExperimental()) {
108-
builder.withDirective(ExperimentalDirective)
107+
fn.directives().forEach {
108+
builder.withDirective(it)
109+
directives.add(it)
109110
}
110111

111112
val args = mutableMapOf<String, Parameter>()
@@ -227,6 +228,7 @@ internal class SchemaGenerator(
227228

228229
klass.directives().map {
229230
builder.withDirective(it)
231+
directives.add(it)
230232
}
231233

232234
if (interfaceType != null) builder.withInterface(interfaceType)

src/main/kotlin/com/expedia/graphql/schema/SchemaGeneratorConfig.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,5 @@ data class SchemaGeneratorConfig(
1111
val supportedPackages: String,
1212
val topLevelQueryName: String = "TopLevelQuery",
1313
val topLevelMutationName: String = "TopLevelMutation",
14-
val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks(),
15-
val directives: Set<GraphQLDirective> = emptySet()
14+
val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks()
1615
)

src/main/kotlin/com/expedia/graphql/schema/directives/ExperimentalDirective.kt

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

src/main/kotlin/com/expedia/graphql/schema/schemaAnnotations.kt

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.expedia.graphql.schema
22

33
import com.expedia.graphql.annotations.GraphQLContext
44
import com.expedia.graphql.annotations.GraphQLDescription
5-
import com.expedia.graphql.annotations.GraphQLExperimental
65
import com.expedia.graphql.annotations.GraphQLIgnore
76
import com.expedia.graphql.schema.exceptions.CouldNotGetNameOfAnnotationException
87
import com.google.common.base.CaseFormat
@@ -11,31 +10,49 @@ import graphql.schema.GraphQLDirective
1110
import graphql.schema.GraphQLInputType
1211
import kotlin.reflect.KAnnotatedElement
1312
import kotlin.reflect.KClass
13+
import kotlin.reflect.KFunction
1414
import kotlin.reflect.KParameter
15+
import kotlin.reflect.KProperty1
1516
import kotlin.reflect.KType
1617
import kotlin.reflect.full.declaredMemberFunctions
1718
import kotlin.reflect.full.findAnnotation
19+
import kotlin.reflect.full.memberProperties
1820
import com.expedia.graphql.annotations.GraphQLDirective as DirectiveAnnotation
1921

2022
internal fun KAnnotatedElement.graphQLDescription(): String? {
21-
var prefix = this.getDeprecationReason()?.let { "DEPRECATED" }
22-
if (null == prefix && this.isGraphQLExperimental()) {
23-
prefix = "EXPERIMENTAL"
24-
}
23+
val directiveNames = listOfDirectives().map { it.normalizeDirectiveName() }
2524

2625
val description = this.findAnnotation<GraphQLDescription>()?.value
2726

28-
return if (null != description) {
29-
if (null != prefix) {
30-
"$prefix: $description"
31-
} else {
32-
description
27+
return when {
28+
description != null && directiveNames.isNotEmpty() -> {
29+
"""$description
30+
|
31+
|Directives: ${directiveNames.joinToString(", ")}
32+
""".trimMargin()
33+
}
34+
description == null && directiveNames.isNotEmpty() -> {
35+
"Directives: ${directiveNames.joinToString(", ")}"
3336
}
34-
} else {
35-
prefix
37+
else -> description
3638
}
3739
}
3840

41+
private fun KAnnotatedElement.listOfDirectives(): List<String> {
42+
val directiveNames = mutableListOf(this.getDeprecationReason()?.let { "deprecated" })
43+
44+
directiveNames.addAll(this.annotations
45+
.filter { it.getDirectiveInfo() != null }
46+
.map {
47+
when {
48+
it.getDirectiveInfo()?.name != "" -> "@${it.getDirectiveInfo()?.name}"
49+
else -> "@${it.annotationClass.simpleName}"
50+
}
51+
}
52+
)
53+
return directiveNames.filterNotNull()
54+
}
55+
3956
internal fun KType.graphQLDescription(): String? = (classifier as? KClass<*>)?.graphQLDescription()
4057

4158
internal fun KAnnotatedElement.getDeprecationReason(): String? {
@@ -51,8 +68,6 @@ internal fun KAnnotatedElement.getDeprecationReason(): String? {
5168

5269
internal fun KAnnotatedElement.isGraphQLIgnored() = this.findAnnotation<GraphQLIgnore>() != null
5370

54-
internal fun KAnnotatedElement.isGraphQLExperimental() = this.findAnnotation<GraphQLExperimental>() != null
55-
5671
internal fun Annotation.getDirectiveInfo() =
5772
this.annotationClass.annotations.find { it is DirectiveAnnotation } as? DirectiveAnnotation
5873

@@ -67,18 +82,22 @@ internal fun KAnnotatedElement.directives() =
6782
} else {
6883
annotation.annotationClass.simpleName ?: throw CouldNotGetNameOfAnnotationException(annotation.annotationClass)
6984
}
70-
builder.name(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, name))
85+
builder.name(name.normalizeDirectiveName())
86+
.validLocations(*directiveInfo.locations)
87+
.description(directiveInfo.description)
7188

72-
annotation::class.properties().forEach { prop ->
89+
annotation::class.properties().forEach{ prop ->
7390
val propertyName = prop.name
7491
val value = prop.call(annotation)
7592
@Suppress("Detekt.UnsafeCast")
7693
val type = graphQLScalar(prop.returnType) as GraphQLInputType
7794
builder.argument(GraphQLArgument.newArgument().name(propertyName).value(value).type(type).build())
7895
}
7996

80-
builder.build()
81-
}
97+
builder.build()
8298
}
99+
}
83100

84101
internal fun KParameter.isGraphQLContext() = this.findAnnotation<GraphQLContext>() != null
102+
103+
private fun String.normalizeDirectiveName() = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, this)

0 commit comments

Comments
 (0)