Skip to content

Commit

Permalink
feat: support directives with union (#1439)
Browse files Browse the repository at this point in the history
* feat: support directives with the union annotation (#1424)

* feat: support directives with the union annotation

* CR comment

* fix: willAddGraphQLTypeToSchema needs annotations from field (#1437)

* feat: use withDirective

* feat: update tests

Co-authored-by: bherrmann2 <[email protected]>
Co-authored-by: samvazquez <[email protected]>
  • Loading branch information
3 people authored May 9, 2022
1 parent 8fbec9d commit 5b058bf
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.expediagroup.graphql.generator

import com.expediagroup.graphql.generator.exceptions.InvalidPackagesException
import com.expediagroup.graphql.generator.internal.extensions.getKClass
import com.expediagroup.graphql.generator.internal.state.AdditionalType
import com.expediagroup.graphql.generator.internal.state.ClassScanner
import com.expediagroup.graphql.generator.internal.state.TypesCache
Expand Down Expand Up @@ -121,7 +122,7 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
this.additionalTypes.clear()
graphqlTypes.addAll(
currentlyProcessedTypes.map {
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, GraphQLKTypeMetadata(inputType = it.inputType)))
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, GraphQLKTypeMetadata(inputType = it.inputType, fieldAnnotations = it.kType.getKClass().annotations)))
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.expediagroup.graphql.generator.annotations

import kotlin.reflect.KClass

@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS)
annotation class GraphQLUnion(
val name: String,
val possibleTypes: Array<KClass<*>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.annotations.GraphQLType
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation

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

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

internal fun List<Annotation>.getUnionAnnotation(): GraphQLUnion? = this.filterIsInstance(GraphQLUnion::class.java).firstOrNull()
internal fun List<Annotation>.getUnionAnnotation(): GraphQLUnion? = this.filterIsInstance(GraphQLUnion::class.java).firstOrNull() ?: this.map { it.getMetaUnionAnnotation() }.firstOrNull()

internal fun List<Annotation>.getCustomUnionClassWithMetaUnionAnnotation(): KClass<*>? = this.firstOrNull { it.getMetaUnionAnnotation() != null }?.annotationClass

internal fun Annotation.getMetaUnionAnnotation(): GraphQLUnion? = this.annotationClass.annotations.filterIsInstance(GraphQLUnion::class.java).firstOrNull()

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ internal fun KClass<*>.isUnion(fieldAnnotations: List<Annotation> = emptyList())

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

internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = this.isInstance(Any::class) && fieldAnnotations.getUnionAnnotation() != null
internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = (this.isInstance(Any::class) || this.isAnnotation()) &&
fieldAnnotations.getUnionAnnotation() != null

internal fun KClass<*>.isAnnotation(): Boolean = this.isSubclassOf(Annotation::class)

/**
* Do not add interfaces as additional types if it expects all the types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import graphql.schema.GraphQLTypeReference
import java.io.Closeable
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.starProjectedType

Expand Down Expand Up @@ -88,7 +89,7 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
val unionAnnotation = typeInfo.fieldAnnotations.getUnionAnnotation()
if (unionAnnotation != null) {
if (type.getKClass().isAnnotationUnion(typeInfo.fieldAnnotations)) {
return TypesCacheKey(type, typeInfo.inputType, getCustomUnionNameKey(unionAnnotation))
return TypesCacheKey(Any::class.createType(), typeInfo.inputType, getCustomUnionNameKey(unionAnnotation))
} else {
throw InvalidCustomUnionException(type)
}
Expand Down Expand Up @@ -148,7 +149,8 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
typesUnderConstruction.add(cacheKey)
val newType = build(kClass)
if (newType !is GraphQLTypeReference && newType is GraphQLNamedType) {
put(cacheKey, KGraphQLType(kClass, newType))
val cacheKClass = if (kClass.isAnnotationUnion(typeInfo.fieldAnnotations)) Any::class else kClass
put(cacheKey, KGraphQLType(cacheKClass, newType))
}
typesUnderConstruction.remove(cacheKey)
newType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package com.expediagroup.graphql.generator.internal.types
import com.expediagroup.graphql.generator.SchemaGenerator
import com.expediagroup.graphql.generator.extensions.unwrapType
import com.expediagroup.graphql.generator.internal.extensions.getCustomTypeAnnotation
import com.expediagroup.graphql.generator.internal.extensions.getCustomUnionClassWithMetaUnionAnnotation
import com.expediagroup.graphql.generator.internal.extensions.getKClass
import com.expediagroup.graphql.generator.internal.extensions.getMetaUnionAnnotation
import com.expediagroup.graphql.generator.internal.extensions.getUnionAnnotation
import com.expediagroup.graphql.generator.internal.extensions.isAnnotation
import com.expediagroup.graphql.generator.internal.extensions.isEnum
import com.expediagroup.graphql.generator.internal.extensions.isInterface
import com.expediagroup.graphql.generator.internal.extensions.isListType
Expand All @@ -30,6 +33,7 @@ import graphql.schema.GraphQLType
import graphql.schema.GraphQLTypeReference
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType

/**
* Return a basic GraphQL type given all the information about the kotlin type.
Expand Down Expand Up @@ -61,7 +65,19 @@ private fun objectFromReflection(generator: SchemaGenerator, type: KType, typeIn

return generator.cache.buildIfNotUnderConstruction(kClass, typeInfo) {
val graphQLType = getGraphQLType(generator, kClass, type, typeInfo)
generator.config.hooks.willAddGraphQLTypeToSchema(type, graphQLType)

/*
* For a field using the meta union annotation, the `type` is `Any`, but we need to pass the annotation with the meta union annotation as the type
* since that is really the type generated from reflection and has any potential directives on it needed by the hook
*/
val metaUnion = typeInfo.fieldAnnotations.firstOrNull { it.getMetaUnionAnnotation() != null }
val resolvedType = if (kClass.isInstance(Any::class) && metaUnion != null) {
metaUnion.annotationClass.createType()
} else {
type
}

generator.config.hooks.willAddGraphQLTypeToSchema(resolvedType, graphQLType)
}
}

Expand All @@ -79,7 +95,12 @@ private fun getGraphQLType(
return when {
kClass.isEnum() -> @Suppress("UNCHECKED_CAST") (generateEnum(generator, kClass as KClass<Enum<*>>))
kClass.isListType() -> generateList(generator, type, typeInfo)
kClass.isUnion(typeInfo.fieldAnnotations) -> generateUnion(generator, kClass, typeInfo.fieldAnnotations.getUnionAnnotation())
kClass.isUnion(typeInfo.fieldAnnotations) -> generateUnion(
generator,
kClass,
typeInfo.fieldAnnotations.getUnionAnnotation(),
if (kClass.isAnnotation()) kClass else typeInfo.fieldAnnotations.getCustomUnionClassWithMetaUnionAnnotation()
)
kClass.isInterface() -> generateInterface(generator, kClass)
typeInfo.inputType -> generateInputObject(generator, kClass)
else -> generateObject(generator, kClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,28 @@ import graphql.schema.GraphQLUnionType
import kotlin.reflect.KClass
import kotlin.reflect.full.createType

internal fun generateUnion(generator: SchemaGenerator, kClass: KClass<*>, unionAnnotation: GraphQLUnion? = null): GraphQLUnionType {
internal fun generateUnion(generator: SchemaGenerator, kClass: KClass<*>, unionAnnotation: GraphQLUnion? = null, customUnionAnnotationClass: KClass<*>? = null): GraphQLUnionType {
return if (unionAnnotation != null) {
generateUnionFromAnnotation(generator, unionAnnotation, kClass)
generateUnionFromAnnotation(generator, unionAnnotation, kClass, customUnionAnnotationClass)
} else {
generateUnionFromKClass(generator, kClass)
}
}

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

val builder = GraphQLUnionType.newUnionType()
builder.name(unionName)
builder.description(unionAnnotation.description)

customUnionAnnotationClass?.let {
generateDirectives(generator, customUnionAnnotationClass, DirectiveLocation.UNION).forEach {
builder.withDirective(it)
}
}

val possibleTypes = unionAnnotation.possibleTypes.toList()

return createUnion(unionName, generator, builder, possibleTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import com.expediagroup.graphql.generator.SchemaGenerator
import com.expediagroup.graphql.generator.SchemaGeneratorConfig
import com.expediagroup.graphql.generator.TopLevelObject
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
import com.expediagroup.graphql.generator.exceptions.EmptyInputObjectTypeException
import com.expediagroup.graphql.generator.exceptions.EmptyInterfaceTypeException
import com.expediagroup.graphql.generator.exceptions.EmptyObjectTypeException
import com.expediagroup.graphql.generator.extensions.deepName
import com.expediagroup.graphql.generator.getTestSchemaConfigWithHooks
import com.expediagroup.graphql.generator.internal.extensions.getKClass
import com.expediagroup.graphql.generator.internal.extensions.getSimpleName
import com.expediagroup.graphql.generator.test.utils.graphqlUUIDType
import com.expediagroup.graphql.generator.testSchemaConfig
Expand All @@ -36,6 +38,7 @@ import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLScalarType
import graphql.schema.GraphQLSchema
import graphql.schema.GraphQLType
import graphql.schema.GraphQLUnionType
import graphql.schema.validation.InvalidSchemaException
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.reactive.asPublisher
Expand Down Expand Up @@ -221,19 +224,25 @@ class SchemaGeneratorHooksTest {
override fun willAddGraphQLTypeToSchema(type: KType, generatedType: GraphQLType): GraphQLType {
hookCalled = true
return when {
generatedType is GraphQLObjectType && generatedType.name == "SomeData" -> GraphQLObjectType.newObject(generatedType).description("My custom description").build()
generatedType is GraphQLInterfaceType && generatedType.name == "RandomData" ->
generatedType is GraphQLObjectType && generatedType.name == "SomeData" && type.getKClass() == SomeData::class ->
GraphQLObjectType.newObject(generatedType).description("My custom description").build()
generatedType is GraphQLInterfaceType && generatedType.name == "RandomData" && type.getKClass() == RandomData::class ->
GraphQLInterfaceType.newInterface(generatedType).description("My custom interface description").build()
generatedType is GraphQLUnionType && generatedType.name == "MyMetaUnion" && type.getKClass() == MyMetaUnion::class ->
GraphQLUnionType.newUnionType(generatedType).description("My meta union description").build()
generatedType is GraphQLUnionType && generatedType.name == "MyAdditionalMetaUnion" && type.getKClass() == MyAdditionalMetaUnion::class ->
GraphQLUnionType.newUnionType(generatedType).description("My additional meta union description").build()
else -> generatedType
}
}
}

val hooks = MockSchemaGeneratorHooks()
val schema = toSchema(
queries = listOf(TopLevelObject(TestQuery())),
config = getTestSchemaConfigWithHooks(hooks)
)
val generator = SchemaGenerator(getTestSchemaConfigWithHooks(hooks))
val schema = generator.use {
it.generateSchema(queries = listOf(TopLevelObject(TestQuery())), additionalTypes = setOf(MyAdditionalMetaUnion::class.createType()))
}

assertTrue(hooks.hookCalled)

val type = schema.getObjectType("SomeData")
Expand All @@ -243,6 +252,14 @@ class SchemaGeneratorHooksTest {
val interfaceType = schema.getType("RandomData") as? GraphQLInterfaceType
assertNotNull(interfaceType)
assertEquals(expected = "My custom interface description", actual = interfaceType.description)

val metaUnionType = schema.getType("MyMetaUnion") as? GraphQLUnionType
assertNotNull(metaUnionType)
assertEquals(expected = "My meta union description", actual = metaUnionType.description)

val additionalMetaUnionType = schema.getType("MyAdditionalMetaUnion") as? GraphQLUnionType
assertNotNull(additionalMetaUnionType)
assertEquals(expected = "My additional meta union description", actual = additionalMetaUnionType.description)
}

@Test
Expand Down Expand Up @@ -346,8 +363,16 @@ class SchemaGeneratorHooksTest {

class TestQuery {
fun query(): SomeData = SomeData("someData", 0)
@MyMetaUnion
fun unionQuery(): Any = SomeData("someData", 0)
}

@GraphQLUnion(name = "MyMetaUnion", possibleTypes = [SomeData::class])
annotation class MyMetaUnion

@GraphQLUnion(name = "MyAdditionalMetaUnion", possibleTypes = [SomeData::class])
annotation class MyAdditionalMetaUnion

class TestSubscription {
fun subscription(): Publisher<SomeData> = flowOf(SomeData("someData", 0)).asPublisher()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package com.expediagroup.graphql.generator.internal.extensions
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
import org.junit.jupiter.api.Test
import kotlin.reflect.KClass
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

Expand All @@ -38,11 +40,20 @@ class AnnotationExtensionsTest {
@property:Deprecated("property deprecated")
@property:GraphQLDescription("property description")
@property:GraphQLName("newName")
val id: String
val id: String,

@GraphQLUnion(name = "CustomUnion", possibleTypes = [NoAnnotations::class])
val union: Any,

@property:MetaUnion
val metaUnion: Any
)

private data class NoAnnotations(val id: String)

@GraphQLUnion(name = "MetaUnion", possibleTypes = [NoAnnotations::class])
annotation class MetaUnion

@Test
fun `verify @GraphQLName on classes`() {
@Suppress("DEPRECATION")
Expand Down Expand Up @@ -85,5 +96,25 @@ class AnnotationExtensionsTest {
assertFalse(NoAnnotations::class.isGraphQLIgnored())
}

@Test
fun `verify @GraphQLUnion`() {
@Suppress("DEPRECATION")
assertNotNull(WithAnnotations::class.findMemberProperty("union")?.annotations?.getUnionAnnotation())
@Suppress("DEPRECATION")
assertNull(WithAnnotations::class.findMemberProperty("union")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
@Suppress("DEPRECATION")
assertNotNull(WithAnnotations::class.findMemberProperty("metaUnion")?.annotations?.getUnionAnnotation())
@Suppress("DEPRECATION")
assertNotNull(WithAnnotations::class.findMemberProperty("metaUnion")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
@Suppress("DEPRECATION")
assertNull(WithAnnotations::class.findMemberProperty("id")?.annotations?.getUnionAnnotation())
@Suppress("DEPRECATION")
assertNull(WithAnnotations::class.findMemberProperty("id")?.annotations?.getCustomUnionClassWithMetaUnionAnnotation())
@Suppress("DEPRECATION")
assertNotNull(WithAnnotations::class.findMemberProperty("metaUnion")?.annotations?.firstOrNull { it is MetaUnion }?.getMetaUnionAnnotation())
@Suppress("DEPRECATION")
assertNull(WithAnnotations::class.findMemberProperty("union")?.annotations?.firstOrNull { it is GraphQLUnion }?.getMetaUnionAnnotation())
}

private fun KClass<*>.findMemberProperty(name: String) = this.declaredMemberProperties.find { it.name == name }
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,17 @@ open class KClassExtensionsTest {

@GraphQLUnion(name = "InvalidUnion", possibleTypes = [One::class, Two::class])
fun invalidCustomUnion(): Int = 1

@MetaUnion
fun customMetaUnion(): Any = One("1")

@MetaUnion
fun invalidCustomMetaUnion(): Int = 1
}

@GraphQLUnion(name = "MetaUnion", possibleTypes = [One::class, Two::class])
annotation class MetaUnion

private class FilterHooks : SchemaGeneratorHooks {
override fun isValidProperty(kClass: KClass<*>, property: KProperty<*>) =
property.name.contains("filteredProperty").not()
Expand Down Expand Up @@ -285,11 +294,23 @@ open class KClassExtensionsTest {
assertTrue(TestUnion::class.isUnion())
val customAnnotationUnion = TestQuery::customUnion
assertTrue(customAnnotationUnion.returnType.getKClass().isUnion(customAnnotationUnion.annotations))
val metaAnnotationUnion = TestQuery::customMetaUnion
assertTrue(metaAnnotationUnion.returnType.getKClass().isUnion(metaAnnotationUnion.annotations))
val metaUnionAnnotationClass = MetaUnion::class
assertTrue(metaUnionAnnotationClass.isUnion(metaAnnotationUnion.annotations))
assertFalse(InvalidPropertyUnionInterface::class.isUnion())
assertFalse(InvalidFunctionUnionInterface::class.isUnion())
assertFalse(Pet::class.isUnion())
val invalidAnnotationUnion = TestQuery::invalidCustomUnion
assertFalse(invalidAnnotationUnion.returnType.getKClass().isUnion(invalidAnnotationUnion.annotations))
val invalidMetaAnnotationUnion = TestQuery::invalidCustomMetaUnion
assertFalse(invalidMetaAnnotationUnion.returnType.getKClass().isUnion(invalidMetaAnnotationUnion.annotations))
}

@Test
fun `test isAnnotation extension`() {
assertTrue(MetaUnion::class.isAnnotation())
assertFalse(TestUnion::class.isAnnotation())
}

@Test
Expand Down
Loading

0 comments on commit 5b058bf

Please sign in to comment.