Skip to content

Commit cba605c

Browse files
authored
feat: support directives with the union annotation (#1424)
* feat: support directives with the union annotation * CR comment
1 parent a0a8bad commit cba605c

File tree

13 files changed

+282
-14
lines changed

13 files changed

+282
-14
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.expediagroup.graphql.generator
1818

1919
import com.expediagroup.graphql.generator.exceptions.InvalidPackagesException
20+
import com.expediagroup.graphql.generator.internal.extensions.getKClass
2021
import com.expediagroup.graphql.generator.internal.state.AdditionalType
2122
import com.expediagroup.graphql.generator.internal.state.ClassScanner
2223
import com.expediagroup.graphql.generator.internal.state.TypesCache
@@ -121,7 +122,7 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
121122
this.additionalTypes.clear()
122123
graphqlTypes.addAll(
123124
currentlyProcessedTypes.map {
124-
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, GraphQLKTypeMetadata(inputType = it.inputType)))
125+
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, GraphQLKTypeMetadata(inputType = it.inputType, fieldAnnotations = it.kType.getKClass().annotations)))
125126
}
126127
)
127128
}

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/annotations/GraphQLUnion.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package com.expediagroup.graphql.generator.annotations
1818

1919
import kotlin.reflect.KClass
2020

21-
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
21+
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS)
2222
annotation class GraphQLUnion(
2323
val name: String,
2424
val possibleTypes: Array<KClass<*>>,

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/annotationExtensions.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLName
2222
import com.expediagroup.graphql.generator.annotations.GraphQLType
2323
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
2424
import kotlin.reflect.KAnnotatedElement
25+
import kotlin.reflect.KClass
2526
import kotlin.reflect.full.findAnnotation
2627

2728
internal fun KAnnotatedElement.getGraphQLDescription(): String? = this.findAnnotation<GraphQLDescription>()?.value
@@ -32,7 +33,11 @@ internal fun KAnnotatedElement.getDeprecationReason(): String? = this.findAnnota
3233

3334
internal fun KAnnotatedElement.isGraphQLIgnored(): Boolean = this.findAnnotation<GraphQLIgnore>() != null
3435

35-
internal fun List<Annotation>.getUnionAnnotation(): GraphQLUnion? = this.filterIsInstance(GraphQLUnion::class.java).firstOrNull()
36+
internal fun List<Annotation>.getUnionAnnotation(): GraphQLUnion? = this.filterIsInstance(GraphQLUnion::class.java).firstOrNull() ?: this.map { it.getMetaUnionAnnotation() }.firstOrNull()
37+
38+
internal fun List<Annotation>.getCustomUnionClassWithMetaUnionAnnotation(): KClass<*>? = this.firstOrNull { it.getMetaUnionAnnotation() != null }?.annotationClass
39+
40+
private fun Annotation.getMetaUnionAnnotation(): GraphQLUnion? = this.annotationClass.annotations.filterIsInstance(GraphQLUnion::class.java).firstOrNull()
3641

3742
internal fun List<Annotation>.getCustomTypeAnnotation(): GraphQLType? = this.filterIsInstance(GraphQLType::class.java).firstOrNull()
3843

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kClassExtensions.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ internal fun KClass<*>.isUnion(fieldAnnotations: List<Annotation> = emptyList())
6464

6565
private fun KClass<*>.isDeclaredUnion() = this.isInterface() && this.declaredMemberProperties.isEmpty() && this.declaredMemberFunctions.isEmpty()
6666

67-
internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = this.isInstance(Any::class) && fieldAnnotations.getUnionAnnotation() != null
67+
internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = (this.isInstance(Any::class) || this.isAnnotation()) &&
68+
fieldAnnotations.getUnionAnnotation() != null
69+
70+
internal fun KClass<*>.isAnnotation(): Boolean = this.isSubclassOf(Annotation::class)
6871

6972
/**
7073
* Do not add interfaces as additional types if it expects all the types

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCache.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import graphql.schema.GraphQLTypeReference
3434
import java.io.Closeable
3535
import kotlin.reflect.KClass
3636
import kotlin.reflect.KType
37+
import kotlin.reflect.full.createType
3738
import kotlin.reflect.full.isSubclassOf
3839
import kotlin.reflect.full.starProjectedType
3940

@@ -88,7 +89,7 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
8889
val unionAnnotation = typeInfo.fieldAnnotations.getUnionAnnotation()
8990
if (unionAnnotation != null) {
9091
if (type.getKClass().isAnnotationUnion(typeInfo.fieldAnnotations)) {
91-
return TypesCacheKey(type, typeInfo.inputType, getCustomUnionNameKey(unionAnnotation))
92+
return TypesCacheKey(Any::class.createType(), typeInfo.inputType, getCustomUnionNameKey(unionAnnotation))
9293
} else {
9394
throw InvalidCustomUnionException(type)
9495
}
@@ -148,7 +149,8 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
148149
typesUnderConstruction.add(cacheKey)
149150
val newType = build(kClass)
150151
if (newType !is GraphQLTypeReference && newType is GraphQLNamedType) {
151-
put(cacheKey, KGraphQLType(kClass, newType))
152+
val cacheKClass = if (kClass.isAnnotationUnion(typeInfo.fieldAnnotations)) Any::class else kClass
153+
put(cacheKey, KGraphQLType(cacheKClass, newType))
152154
}
153155
typesUnderConstruction.remove(cacheKey)
154156
newType

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateGraphQLType.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package com.expediagroup.graphql.generator.internal.types
1919
import com.expediagroup.graphql.generator.SchemaGenerator
2020
import com.expediagroup.graphql.generator.extensions.unwrapType
2121
import com.expediagroup.graphql.generator.internal.extensions.getCustomTypeAnnotation
22+
import com.expediagroup.graphql.generator.internal.extensions.getCustomUnionClassWithMetaUnionAnnotation
2223
import com.expediagroup.graphql.generator.internal.extensions.getKClass
2324
import com.expediagroup.graphql.generator.internal.extensions.getUnionAnnotation
25+
import com.expediagroup.graphql.generator.internal.extensions.isAnnotation
2426
import com.expediagroup.graphql.generator.internal.extensions.isEnum
2527
import com.expediagroup.graphql.generator.internal.extensions.isInterface
2628
import com.expediagroup.graphql.generator.internal.extensions.isListType
@@ -79,7 +81,12 @@ private fun getGraphQLType(
7981
return when {
8082
kClass.isEnum() -> @Suppress("UNCHECKED_CAST") (generateEnum(generator, kClass as KClass<Enum<*>>))
8183
kClass.isListType() -> generateList(generator, type, typeInfo)
82-
kClass.isUnion(typeInfo.fieldAnnotations) -> generateUnion(generator, kClass, typeInfo.fieldAnnotations.getUnionAnnotation())
84+
kClass.isUnion(typeInfo.fieldAnnotations) -> generateUnion(
85+
generator,
86+
kClass,
87+
typeInfo.fieldAnnotations.getUnionAnnotation(),
88+
if (kClass.isAnnotation()) kClass else typeInfo.fieldAnnotations.getCustomUnionClassWithMetaUnionAnnotation()
89+
)
8390
kClass.isInterface() -> generateInterface(generator, kClass)
8491
typeInfo.inputType -> generateInputObject(generator, kClass)
8592
else -> generateObject(generator, kClass)

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateUnion.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,28 @@ import graphql.schema.GraphQLUnionType
3232
import kotlin.reflect.KClass
3333
import kotlin.reflect.full.createType
3434

35-
internal fun generateUnion(generator: SchemaGenerator, kClass: KClass<*>, unionAnnotation: GraphQLUnion? = null): GraphQLUnionType {
35+
internal fun generateUnion(generator: SchemaGenerator, kClass: KClass<*>, unionAnnotation: GraphQLUnion? = null, customUnionAnnotationClass: KClass<*>? = null): GraphQLUnionType {
3636
return if (unionAnnotation != null) {
37-
generateUnionFromAnnotation(generator, unionAnnotation, kClass)
37+
generateUnionFromAnnotation(generator, unionAnnotation, kClass, customUnionAnnotationClass)
3838
} else {
3939
generateUnionFromKClass(generator, kClass)
4040
}
4141
}
4242

43-
private fun generateUnionFromAnnotation(generator: SchemaGenerator, unionAnnotation: GraphQLUnion, kClass: KClass<*>): GraphQLUnionType {
43+
private fun generateUnionFromAnnotation(generator: SchemaGenerator, unionAnnotation: GraphQLUnion, kClass: KClass<*>, customUnionAnnotationClass: KClass<*>?): GraphQLUnionType {
4444
val unionName = unionAnnotation.name
4545
validateGraphQLName(unionName, kClass)
4646

4747
val builder = GraphQLUnionType.newUnionType()
4848
builder.name(unionName)
4949
builder.description(unionAnnotation.description)
5050

51+
customUnionAnnotationClass?.let {
52+
generateDirectives(generator, customUnionAnnotationClass, DirectiveLocation.UNION).forEach {
53+
builder.withAppliedDirective(it)
54+
}
55+
}
56+
5157
val possibleTypes = unionAnnotation.possibleTypes.toList()
5258

5359
return createUnion(unionName, generator, builder, possibleTypes)

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/AnnotationExtensionsTest.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ package com.expediagroup.graphql.generator.internal.extensions
1919
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2020
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
2121
import com.expediagroup.graphql.generator.annotations.GraphQLName
22+
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
2223
import org.junit.jupiter.api.Test
2324
import kotlin.reflect.KClass
2425
import kotlin.reflect.full.declaredMemberProperties
2526
import kotlin.test.assertEquals
2627
import kotlin.test.assertFalse
28+
import kotlin.test.assertNotNull
2729
import kotlin.test.assertNull
2830
import kotlin.test.assertTrue
2931

@@ -38,11 +40,20 @@ class AnnotationExtensionsTest {
3840
@property:Deprecated("property deprecated")
3941
@property:GraphQLDescription("property description")
4042
@property:GraphQLName("newName")
41-
val id: String
43+
val id: String,
44+
45+
@GraphQLUnion(name = "CustomUnion", possibleTypes = [NoAnnotations::class])
46+
val union: Any,
47+
48+
@property:MetaUnion
49+
val metaUnion: Any
4250
)
4351

4452
private data class NoAnnotations(val id: String)
4553

54+
@GraphQLUnion(name = "MetaUnion", possibleTypes = [NoAnnotations::class])
55+
annotation class MetaUnion
56+
4657
@Test
4758
fun `verify @GraphQLName on classes`() {
4859
@Suppress("DEPRECATION")
@@ -85,5 +96,21 @@ class AnnotationExtensionsTest {
8596
assertFalse(NoAnnotations::class.isGraphQLIgnored())
8697
}
8798

99+
@Test
100+
fun `verify @GraphQLUnion`() {
101+
@Suppress("DEPRECATION")
102+
assertNotNull(WithAnnotations::class.findMemberProperty("union")?.annotations?.getUnionAnnotation())
103+
@Suppress("DEPRECATION")
104+
assertNull(WithAnnotations::class.findMemberProperty("union")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
105+
@Suppress("DEPRECATION")
106+
assertNotNull(WithAnnotations::class.findMemberProperty("metaUnion")?.annotations?.getUnionAnnotation())
107+
@Suppress("DEPRECATION")
108+
assertNotNull(WithAnnotations::class.findMemberProperty("metaUnion")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
109+
@Suppress("DEPRECATION")
110+
assertNull(WithAnnotations::class.findMemberProperty("id")?.annotations?.getUnionAnnotation())
111+
@Suppress("DEPRECATION")
112+
assertNull(WithAnnotations::class.findMemberProperty("id")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
113+
}
114+
88115
private fun KClass<*>.findMemberProperty(name: String) = this.declaredMemberProperties.find { it.name == name }
89116
}

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KClassExtensionsTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,17 @@ open class KClassExtensionsTest {
157157

158158
@GraphQLUnion(name = "InvalidUnion", possibleTypes = [One::class, Two::class])
159159
fun invalidCustomUnion(): Int = 1
160+
161+
@MetaUnion
162+
fun customMetaUnion(): Any = One("1")
163+
164+
@MetaUnion
165+
fun invalidCustomMetaUnion(): Int = 1
160166
}
161167

168+
@GraphQLUnion(name = "MetaUnion", possibleTypes = [One::class, Two::class])
169+
annotation class MetaUnion
170+
162171
private class FilterHooks : SchemaGeneratorHooks {
163172
override fun isValidProperty(kClass: KClass<*>, property: KProperty<*>) =
164173
property.name.contains("filteredProperty").not()
@@ -283,11 +292,23 @@ open class KClassExtensionsTest {
283292
assertTrue(TestUnion::class.isUnion())
284293
val customAnnotationUnion = TestQuery::customUnion
285294
assertTrue(customAnnotationUnion.returnType.getKClass().isUnion(customAnnotationUnion.annotations))
295+
val metaAnnotationUnion = TestQuery::customMetaUnion
296+
assertTrue(metaAnnotationUnion.returnType.getKClass().isUnion(metaAnnotationUnion.annotations))
297+
val metaUnionAnnotationClass = MetaUnion::class
298+
assertTrue(metaUnionAnnotationClass.isUnion(metaAnnotationUnion.annotations))
286299
assertFalse(InvalidPropertyUnionInterface::class.isUnion())
287300
assertFalse(InvalidFunctionUnionInterface::class.isUnion())
288301
assertFalse(Pet::class.isUnion())
289302
val invalidAnnotationUnion = TestQuery::invalidCustomUnion
290303
assertFalse(invalidAnnotationUnion.returnType.getKClass().isUnion(invalidAnnotationUnion.annotations))
304+
val invalidMetaAnnotationUnion = TestQuery::invalidCustomMetaUnion
305+
assertFalse(invalidMetaAnnotationUnion.returnType.getKClass().isUnion(invalidMetaAnnotationUnion.annotations))
306+
}
307+
308+
@Test
309+
fun `test isAnnotation extension`() {
310+
assertTrue(MetaUnion::class.isAnnotation())
311+
assertFalse(TestUnion::class.isAnnotation())
291312
}
292313

293314
@Test

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCacheTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import graphql.schema.GraphQLNamedType
2424
import io.mockk.every
2525
import io.mockk.mockk
2626
import org.junit.jupiter.api.Test
27+
import kotlin.reflect.full.createType
2728
import kotlin.reflect.full.findParameterByName
2829
import kotlin.reflect.full.starProjectedType
2930
import kotlin.test.assertEquals
@@ -49,6 +50,10 @@ class TypesCacheTest {
4950
every { name } returns "CustomUnion"
5051
}
5152

53+
private val metaUnionGraphQLType: GraphQLNamedType = mockk {
54+
every { name } returns "MetaUnion"
55+
}
56+
5257
class MyClass {
5358
fun listFun(list: List<String>) = list.joinToString(separator = ",") { it }
5459

@@ -57,10 +62,19 @@ class TypesCacheTest {
5762
@GraphQLUnion(name = "CustomUnion", possibleTypes = [MyType::class, Int::class])
5863
fun customUnion(): Any = MyType(1)
5964

65+
@MetaUnion
66+
fun metaUnion(): Any = MyType(1)
67+
6068
@GraphQLUnion(name = "InvalidUnion", possibleTypes = [MyType::class, Int::class])
6169
fun invalidUnion(): String = "foobar"
70+
71+
@MetaUnion
72+
fun invalidMetaUnion(): String = "foobar"
6273
}
6374

75+
@GraphQLUnion(name = "MetaUnion", possibleTypes = [MyType::class, Int::class])
76+
annotation class MetaUnion
77+
6478
@Test
6579
fun `basic get and put with non input type`() {
6680
val cache = TypesCache(listOf("com.expediagroup.graphql.generator"))
@@ -150,6 +164,37 @@ class TypesCacheTest {
150164
}
151165
}
152166

167+
@Test
168+
fun `meta unions are cached by special name`() {
169+
val cache = TypesCache(listOf("com.expediagroup.graphql.generator"))
170+
val type = MyClass::metaUnion.returnType
171+
val annotations = MyClass::metaUnion.annotations
172+
val typeInfo = GraphQLKTypeMetadata(inputType = false, fieldAnnotations = annotations)
173+
174+
val cacheKey = TypesCacheKey(type = type, typeInfo.inputType, name = "MetaUnion[MyType,Int]")
175+
val cacheValue = KGraphQLType(type.getKClass(), metaUnionGraphQLType)
176+
177+
assertNull(cache.get(cacheKey))
178+
assertNull(cache.get(type = type, typeInfo))
179+
assertNotNull(cache.put(cacheKey, cacheValue))
180+
assertNotNull(cache.get(type = type, typeInfo))
181+
}
182+
183+
@Test
184+
fun `invalid meta unions throw an exception`() {
185+
val cache = TypesCache(listOf("com.expediagroup.graphql.generator"))
186+
val type = MyClass::invalidMetaUnion.returnType
187+
val annotations = MyClass::invalidMetaUnion.annotations
188+
val typeInfo = GraphQLKTypeMetadata(fieldAnnotations = annotations)
189+
190+
val cacheKey = TypesCacheKey(type = type, inputType = typeInfo.inputType, name = "InvalidMetaUnion[MyType,Int]")
191+
192+
assertNull(cache.get(cacheKey))
193+
assertFailsWith(InvalidCustomUnionException::class) {
194+
cache.get(type = type, typeInfo)
195+
}
196+
}
197+
153198
@Test
154199
fun `verify doesNotContainGraphQLType()`() {
155200
val cache = TypesCache(listOf("com.expediagroup.graphql.generator"))
@@ -197,4 +242,20 @@ class TypesCacheTest {
197242
assertNotNull(cacheHit)
198243
assertEquals(expected = cacheValue.graphQLType, actual = cacheHit)
199244
}
245+
246+
@Test
247+
fun `buildIfNotUnderConstruction puts custom union into the cache`() {
248+
val cache = TypesCache(listOf("com.expediagroup.graphql.generator"))
249+
val annotations = MyClass::customUnion.annotations
250+
val typeInfo = GraphQLKTypeMetadata(inputType = false, fieldAnnotations = annotations)
251+
252+
val cacheKey = TypesCacheKey(type = Any::class.createType(), typeInfo.inputType, name = "CustomUnion[MyType,Int]")
253+
254+
cache.buildIfNotUnderConstruction(MyClass::customUnion.returnType.getKClass(), typeInfo) {
255+
customUnionGraphQLType
256+
}
257+
258+
val cacheValue = cache.get(cacheKey)
259+
assertNotNull(cacheValue)
260+
}
200261
}

0 commit comments

Comments
 (0)