Skip to content

Commit 0c87da3

Browse files
authored
Directives on constructor args with no prefix (#320)
Fixes #278 Property (input/output) directives are now resolved by checking the property and the constructor argument for any directives. This is the same pattern for checking any other annotations we have
1 parent 148c983 commit 0c87da3

File tree

10 files changed

+166
-32
lines changed

10 files changed

+166
-32
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.expedia.graphql.generator.types.DirectiveBuilder
88
import com.expedia.graphql.generator.types.EnumBuilder
99
import com.expedia.graphql.generator.types.FunctionBuilder
1010
import com.expedia.graphql.generator.types.InputObjectBuilder
11+
import com.expedia.graphql.generator.types.InputPropertyBuilder
1112
import com.expedia.graphql.generator.types.InterfaceBuilder
1213
import com.expedia.graphql.generator.types.ListBuilder
1314
import com.expedia.graphql.generator.types.MutationBuilder
@@ -43,6 +44,7 @@ open class SchemaGenerator(val config: SchemaGeneratorConfig) {
4344
private val interfaceTypeBuilder = InterfaceBuilder(this)
4445
private val propertyTypeBuilder = PropertyBuilder(this)
4546
private val inputObjectTypeBuilder = InputObjectBuilder(this)
47+
private val inputPropertyBuilder = InputPropertyBuilder(this)
4648
private val listTypeBuilder = ListBuilder(this)
4749
private val functionTypeBuilder = FunctionBuilder(this)
4850
private val enumTypeBuilder = EnumBuilder(this)
@@ -85,6 +87,9 @@ open class SchemaGenerator(val config: SchemaGeneratorConfig) {
8587
open fun inputObjectType(kClass: KClass<*>) =
8688
inputObjectTypeBuilder.inputObjectType(kClass)
8789

90+
open fun inputProperty(prop: KProperty<*>, parentClass: KClass<*>) =
91+
inputPropertyBuilder.inputProperty(prop, parentClass)
92+
8893
open fun interfaceType(kClass: KClass<*>) =
8994
interfaceTypeBuilder.interfaceType(kClass)
9095

@@ -97,8 +102,8 @@ open class SchemaGenerator(val config: SchemaGeneratorConfig) {
97102
open fun scalarType(type: KType, annotatedAsID: Boolean = false) =
98103
scalarTypeBuilder.scalarType(type, annotatedAsID)
99104

100-
open fun directives(element: KAnnotatedElement): List<GraphQLDirective> =
101-
directiveTypeBuilder.directives(element)
105+
open fun directives(element: KAnnotatedElement, parentClass: KClass<*>? = null): List<GraphQLDirective> =
106+
directiveTypeBuilder.directives(element, parentClass)
102107

103108
open fun fieldDirectives(field: Field): List<GraphQLDirective> =
104109
directiveTypeBuilder.fieldDirectives(field)

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/extensions/kPropertyExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ internal fun KProperty<*>.getPropertyDescription(parentClass: KClass<*>): String
2929
internal fun KProperty<*>.getPropertyName(parentClass: KClass<*>): String? =
3030
this.getGraphQLName() ?: getConstructorParameter(parentClass)?.getGraphQLName() ?: this.name
3131

32+
internal fun KProperty<*>.getPropertyAnnotations(parentClass: KClass<*>): List<Annotation> =
33+
this.annotations.union(getConstructorParameter(parentClass)?.annotations.orEmpty()).toList()
34+
3235
private fun KProperty<*>.getConstructorParameter(parentClass: KClass<*>) = parentClass.findConstructorParamter(this.name)

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/DirectiveBuilder.kt

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

33
import com.expedia.graphql.generator.SchemaGenerator
44
import com.expedia.graphql.generator.TypeBuilder
5+
import com.expedia.graphql.generator.extensions.getPropertyAnnotations
56
import com.expedia.graphql.generator.extensions.getSimpleName
67
import com.expedia.graphql.generator.extensions.getValidProperties
78
import com.expedia.graphql.generator.extensions.safeCast
@@ -10,14 +11,22 @@ import graphql.schema.GraphQLArgument
1011
import graphql.schema.GraphQLDirective
1112
import java.lang.reflect.Field
1213
import kotlin.reflect.KAnnotatedElement
14+
import kotlin.reflect.KClass
15+
import kotlin.reflect.KProperty
1316
import com.expedia.graphql.annotations.GraphQLDirective as GraphQLDirectiveAnnotation
1417

1518
internal class DirectiveBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
1619

17-
internal fun directives(element: KAnnotatedElement): List<GraphQLDirective> =
18-
element.annotations
20+
internal fun directives(element: KAnnotatedElement, parentClass: KClass<*>?): List<GraphQLDirective> {
21+
val annotations = when {
22+
element is KProperty<*> && parentClass != null -> element.getPropertyAnnotations(parentClass)
23+
else -> element.annotations
24+
}
25+
26+
return annotations
1927
.mapNotNull { it.getDirectiveInfo() }
2028
.map(this::getDirective)
29+
}
2130

2231
internal fun fieldDirectives(field: Field): List<GraphQLDirective> =
2332
field.annotations

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/InputObjectBuilder.kt

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,11 @@ package com.expedia.graphql.generator.types
33
import com.expedia.graphql.generator.SchemaGenerator
44
import com.expedia.graphql.generator.TypeBuilder
55
import com.expedia.graphql.generator.extensions.getGraphQLDescription
6-
import com.expedia.graphql.generator.extensions.getPropertyDescription
7-
import com.expedia.graphql.generator.extensions.getPropertyName
86
import com.expedia.graphql.generator.extensions.getSimpleName
97
import com.expedia.graphql.generator.extensions.getValidProperties
10-
import com.expedia.graphql.generator.extensions.isPropertyGraphQLID
118
import com.expedia.graphql.generator.extensions.safeCast
12-
import graphql.schema.GraphQLInputObjectField
139
import graphql.schema.GraphQLInputObjectType
14-
import graphql.schema.GraphQLInputType
1510
import kotlin.reflect.KClass
16-
import kotlin.reflect.KProperty
1711

1812
internal class InputObjectBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
1913
internal fun inputObjectType(kClass: KClass<*>): GraphQLInputObjectType {
@@ -27,21 +21,8 @@ internal class InputObjectBuilder(generator: SchemaGenerator) : TypeBuilder(gene
2721
}
2822

2923
// It does not make sense to run functions against the input types so we only process the properties
30-
kClass.getValidProperties(config.hooks)
31-
.forEach { builder.field(inputProperty(it, kClass)) }
32-
33-
return config.hooks.onRewireGraphQLType(builder.build()).safeCast()
34-
}
35-
36-
private fun inputProperty(prop: KProperty<*>, parentClass: KClass<*>): GraphQLInputObjectField {
37-
val builder = GraphQLInputObjectField.newInputObjectField()
38-
39-
builder.description(prop.getPropertyDescription(parentClass))
40-
builder.name(prop.getPropertyName(parentClass))
41-
builder.type(graphQLTypeOf(prop.returnType, true, prop.isPropertyGraphQLID(parentClass)).safeCast<GraphQLInputType>())
42-
43-
generator.directives(prop).forEach {
44-
builder.withDirective(it)
24+
kClass.getValidProperties(config.hooks).forEach {
25+
builder.field(generator.inputProperty(it, kClass))
4526
}
4627

4728
return config.hooks.onRewireGraphQLType(builder.build()).safeCast()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.expedia.graphql.generator.types
2+
3+
import com.expedia.graphql.generator.SchemaGenerator
4+
import com.expedia.graphql.generator.TypeBuilder
5+
import com.expedia.graphql.generator.extensions.getPropertyDescription
6+
import com.expedia.graphql.generator.extensions.getPropertyName
7+
import com.expedia.graphql.generator.extensions.isPropertyGraphQLID
8+
import com.expedia.graphql.generator.extensions.safeCast
9+
import graphql.schema.GraphQLInputObjectField
10+
import graphql.schema.GraphQLInputType
11+
import kotlin.reflect.KClass
12+
import kotlin.reflect.KProperty
13+
14+
internal class InputPropertyBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
15+
16+
internal fun inputProperty(prop: KProperty<*>, parentClass: KClass<*>): GraphQLInputObjectField {
17+
val builder = GraphQLInputObjectField.newInputObjectField()
18+
19+
builder.description(prop.getPropertyDescription(parentClass))
20+
builder.name(prop.getPropertyName(parentClass))
21+
builder.type(graphQLTypeOf(prop.returnType, true, prop.isPropertyGraphQLID(parentClass)).safeCast<GraphQLInputType>())
22+
23+
generator.directives(prop, parentClass).forEach {
24+
builder.withDirective(it)
25+
}
26+
27+
return config.hooks.onRewireGraphQLType(builder.build()).safeCast()
28+
}
29+
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/PropertyBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal class PropertyBuilder(generator: SchemaGenerator) : TypeBuilder(generat
3030
fieldBuilder.withDirective(deprecatedDirectiveWithReason(it))
3131
}
3232

33-
generator.directives(prop).forEach {
33+
generator.directives(prop, parentClass).forEach {
3434
fieldBuilder.withDirective(it)
3535
}
3636

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/DirectiveBuilderTest.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.expedia.graphql.directives.DeprecatedDirective
66
import com.expedia.graphql.generator.SchemaGenerator
77
import com.expedia.graphql.generator.extensions.isTrue
88
import com.expedia.graphql.getTestSchemaConfigWithMockedDirectives
9+
import com.expedia.graphql.test.utils.SimpleDirective
910
import graphql.Directives
1011
import org.junit.jupiter.api.BeforeEach
1112
import org.junit.jupiter.api.Test
@@ -15,9 +16,6 @@ import kotlin.test.assertTrue
1516

1617
internal class DirectiveBuilderTest {
1718

18-
@GraphQLDirective
19-
internal annotation class SimpleDirective
20-
2119
@GraphQLDirective
2220
internal annotation class DirectiveWithString(val string: String)
2321

@@ -59,6 +57,12 @@ internal class DirectiveBuilderTest {
5957
fun directiveWithClass(string: String) = string
6058
}
6159

60+
internal data class MyClassWithConstructorArgs(
61+
@SimpleDirective val noPrefix: String,
62+
@property:SimpleDirective val propertyPrefix: String,
63+
val noDirective: String
64+
)
65+
6266
private lateinit var basicGenerator: SchemaGenerator
6367

6468
@BeforeEach
@@ -148,4 +152,24 @@ internal class DirectiveBuilderTest {
148152

149153
assertEquals(initialCount + 1, basicGenerator.state.directives.size)
150154
}
155+
156+
@Test
157+
fun `directives on constructor arguments can be used with or without annotation prefix`() {
158+
val noDirectiveResult = basicGenerator.directives(MyClassWithConstructorArgs::noDirective)
159+
assertEquals(expected = 0, actual = noDirectiveResult.size)
160+
161+
val propertyPrefixResult = basicGenerator.directives(MyClassWithConstructorArgs::propertyPrefix)
162+
assertEquals(expected = 1, actual = propertyPrefixResult.size)
163+
assertEquals(expected = "simpleDirective", actual = propertyPrefixResult.first().name)
164+
165+
val noPrefixResult = basicGenerator.directives(MyClassWithConstructorArgs::noPrefix, MyClassWithConstructorArgs::class)
166+
assertEquals(expected = 1, actual = noPrefixResult.size)
167+
assertEquals(expected = "simpleDirective", actual = noPrefixResult.first().name)
168+
}
169+
170+
@Test
171+
fun `directives on constructor arguments only works with parent class`() {
172+
val noPrefixResult = basicGenerator.directives(MyClassWithConstructorArgs::noPrefix, null)
173+
assertEquals(expected = 0, actual = noPrefixResult.size)
174+
}
151175
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.expedia.graphql.generator.types
2+
3+
import com.expedia.graphql.annotations.GraphQLDescription
4+
import com.expedia.graphql.annotations.GraphQLName
5+
import com.expedia.graphql.test.utils.SimpleDirective
6+
import org.junit.jupiter.api.Test
7+
import kotlin.test.assertEquals
8+
9+
internal class InputPropertyBuilderTest : TypeTestHelper() {
10+
11+
private lateinit var builder: InputPropertyBuilder
12+
13+
override fun beforeTest() {
14+
builder = InputPropertyBuilder(generator)
15+
}
16+
17+
private data class InputPropertyTestClass(
18+
@GraphQLDescription("Custom description")
19+
val description: String,
20+
21+
@GraphQLName("newName")
22+
val changeMe: String,
23+
24+
@SimpleDirective
25+
val directiveWithNoPrefix: String,
26+
27+
@property:SimpleDirective
28+
val directiveWithPrefix: String
29+
)
30+
31+
@Test
32+
fun `Input property can have a description`() {
33+
val result = builder.inputProperty(InputPropertyTestClass::description, InputPropertyTestClass::class)
34+
assertEquals("Custom description", result.description)
35+
}
36+
37+
@Test
38+
fun `Input property names can change`() {
39+
val result = builder.inputProperty(InputPropertyTestClass::changeMe, InputPropertyTestClass::class)
40+
assertEquals("newName", result.name)
41+
}
42+
43+
@Test
44+
fun `Input property can have directives`() {
45+
val resultWithNoPrefix = builder.inputProperty(InputPropertyTestClass::directiveWithNoPrefix, InputPropertyTestClass::class)
46+
assertEquals(1, resultWithNoPrefix.directives.size)
47+
assertEquals("simpleDirective", resultWithNoPrefix.directives.first().name)
48+
49+
val resultWithPrefix = builder.inputProperty(InputPropertyTestClass::directiveWithPrefix, InputPropertyTestClass::class)
50+
assertEquals(1, resultWithPrefix.directives.size)
51+
assertEquals("simpleDirective", resultWithPrefix.directives.first().name)
52+
}
53+
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/PropertyBuilderTest.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
1010
import com.expedia.graphql.generator.SchemaGenerator
1111
import com.expedia.graphql.generator.extensions.getSimpleName
1212
import com.expedia.graphql.hooks.SchemaGeneratorHooks
13+
import com.expedia.graphql.test.utils.SimpleDirective
1314
import graphql.Scalars
1415
import graphql.introspection.Introspection
1516
import graphql.schema.DataFetcher
@@ -51,7 +52,13 @@ internal class PropertyBuilderTest : TypeTestHelper() {
5152
val fooBar: String,
5253

5354
@GraphQLID
54-
val myId: String
55+
val myId: String,
56+
57+
@SimpleDirective
58+
val directiveWithNoPrefix: String,
59+
60+
@property:SimpleDirective
61+
val directiveWithPrefix: String
5562
)
5663

5764
private lateinit var builder: PropertyBuilder
@@ -127,6 +134,23 @@ internal class PropertyBuilderTest : TypeTestHelper() {
127134
)
128135
}
129136

137+
@Test
138+
fun `Properties with no directives are not set`() {
139+
val resultWithPrefix = builder.property(DataClassWithProperties::myId, DataClassWithProperties::class)
140+
assertEquals(0, resultWithPrefix.directives.size)
141+
}
142+
143+
@Test
144+
fun `Properties can have directives on the constructor args`() {
145+
val resultWithPrefix = builder.property(DataClassWithProperties::directiveWithPrefix, DataClassWithProperties::class)
146+
assertEquals(1, resultWithPrefix.directives.size)
147+
assertEquals("simpleDirective", resultWithPrefix.directives.first().name)
148+
149+
val resultWithNoPrefix = builder.property(DataClassWithProperties::directiveWithNoPrefix, DataClassWithProperties::class)
150+
assertEquals(1, resultWithNoPrefix.directives.size)
151+
assertEquals("simpleDirective", resultWithNoPrefix.directives.first().name)
152+
}
153+
130154
@Test
131155
fun `Test nullable property`() {
132156
val prop = ClassWithProperties::nullableCake

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/TypeTestHelper.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ internal open class TypeTestHelper {
4848
private var directiveBuilder: DirectiveBuilder? = null
4949
private var functionBuilder: FunctionBuilder? = null
5050
private var propertyBuilder: PropertyBuilder? = null
51+
private var inputPropertyBuilder: InputPropertyBuilder? = null
5152
private var unionBuilder: UnionBuilder? = null
5253
private var argumentBuilder: ArgumentBuilder? = null
5354
private var inputObjectBuilder: InputObjectBuilder? = null
@@ -81,6 +82,11 @@ internal open class TypeTestHelper {
8182
propertyBuilder!!.property(it.invocation.args[0] as KProperty<*>, it.invocation.args[1] as KClass<*>)
8283
}
8384

85+
inputPropertyBuilder = spyk(InputPropertyBuilder(generator))
86+
every { generator.inputProperty(any(), any()) } answers {
87+
inputPropertyBuilder!!.inputProperty(it.invocation.args[0] as KProperty<*>, it.invocation.args[1] as KClass<*>)
88+
}
89+
8490
scalarBuilder = spyk(ScalarBuilder(generator))
8591
every { generator.scalarType(any(), any()) } answers {
8692
scalarBuilder!!.scalarType(it.invocation.args[0] as KType, it.invocation.args[1] as Boolean)
@@ -117,8 +123,8 @@ internal open class TypeTestHelper {
117123
}
118124

119125
directiveBuilder = spyk(DirectiveBuilder(generator))
120-
every { generator.directives(any()) } answers {
121-
val directives = directiveBuilder!!.directives(it.invocation.args[0] as KAnnotatedElement)
126+
every { generator.directives(any(), any()) } answers {
127+
val directives = directiveBuilder!!.directives(it.invocation.args[0] as KAnnotatedElement, it.invocation.args[1] as? KClass<*>)
122128
for (directive in directives) {
123129
state.directives[directive.name] = directive
124130
}

0 commit comments

Comments
 (0)