Skip to content

Commit b12de27

Browse files
gscheibelrickfast
authored andcommitted
Add support for recursive polymorphic types (#46)
1 parent 4817dc1 commit b12de27

File tree

3 files changed

+94
-48
lines changed

3 files changed

+94
-48
lines changed

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

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,7 @@ internal class SchemaGenerator(
241241
GraphQLList.list(graphQLTypeOf(type.arguments.first().type!!, inputType))
242242

243243
private fun objectType(kClass: KClass<*>, interfaceType: GraphQLInterfaceType? = null): GraphQLType {
244-
if (cache.isTypeUnderConstruction(kClass)) {
245-
return GraphQLTypeReference.typeRef(kClass.simpleName)
246-
} else {
247-
cache.putTypeUnderConstruction(kClass)
248-
244+
return cache.buildIfNotUnderConstruction(kClass) {
249245
val builder = GraphQLObjectType.newObject()
250246

251247
builder.name(kClass.simpleName)
@@ -272,7 +268,7 @@ internal class SchemaGenerator(
272268
kClass.getValidFunctions(config.hooks)
273269
.forEach { builder.field(function(it)) }
274270

275-
return builder.build()
271+
builder.build()
276272
}
277273
}
278274

@@ -301,51 +297,59 @@ internal class SchemaGenerator(
301297
}
302298

303299
private fun interfaceType(kClass: KClass<*>): GraphQLType {
304-
val builder = GraphQLInterfaceType.newInterface()
300+
return cache.buildIfNotUnderConstruction(kClass) {
301+
val builder = GraphQLInterfaceType.newInterface()
305302

306-
builder.name(kClass.simpleName)
307-
builder.description(kClass.graphQLDescription())
303+
builder.name(kClass.simpleName)
304+
builder.description(kClass.graphQLDescription())
308305

309-
kClass.getValidProperties(config.hooks)
310-
.forEach { builder.field(property(it)) }
311-
312-
kClass.getValidFunctions(config.hooks)
313-
.forEach { builder.field(function(it, abstract = true)) }
314-
315-
builder.typeResolver { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.simpleName) }
316-
val interfaceType = builder.build()
317-
318-
val implementations = getSubTypesOf(kClass)
319-
implementations
320-
.filterNot { it.kotlin.isAbstract }
321-
.forEach {
322-
val objectType = objectType(it.kotlin, interfaceType)
323-
val key = TypesCacheKey(it.kotlin.createType(), false)
324-
additionTypes.add(objectType)
325-
cache.put(key, KGraphQLType(it.kotlin, objectType))
326-
}
306+
kClass.getValidProperties(config.hooks)
307+
.forEach { builder.field(property(it)) }
308+
309+
kClass.getValidFunctions(config.hooks)
310+
.forEach { builder.field(function(it, abstract = true)) }
311+
312+
builder.typeResolver { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.simpleName) }
313+
val interfaceType = builder.build()
327314

328-
return interfaceType
315+
val implementations = getSubTypesOf(kClass)
316+
implementations
317+
.filterNot { it.kotlin.isAbstract }
318+
.forEach {
319+
val objectType = objectType(it.kotlin, interfaceType)
320+
val key = TypesCacheKey(it.kotlin.createType(), false)
321+
additionTypes.add(objectType)
322+
cache.put(key, KGraphQLType(it.kotlin, objectType))
323+
}
324+
325+
interfaceType
326+
}
329327
}
330328

331329
private fun unionType(kClass: KClass<*>): GraphQLType {
332-
val builder = GraphQLUnionType.newUnionType()
330+
return cache.buildIfNotUnderConstruction(kClass) {
331+
val builder = GraphQLUnionType.newUnionType()
333332

334-
builder.name(kClass.simpleName)
335-
builder.description(kClass.graphQLDescription())
336-
builder.typeResolver { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.simpleName) }
337-
338-
val implementations = getSubTypesOf(kClass)
339-
implementations
340-
.filterNot { it.kotlin.isAbstract }
341-
.forEach {
342-
val objectType = objectType(it.kotlin)
343-
val key = TypesCacheKey(it.kotlin.createType(), false)
344-
builder.possibleType(objectType(it.kotlin) as GraphQLObjectType)
345-
cache.put(key, KGraphQLType(it.kotlin, objectType))
346-
}
333+
builder.name(kClass.simpleName)
334+
builder.description(kClass.graphQLDescription())
335+
builder.typeResolver { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.simpleName) }
347336

348-
return builder.build()
337+
val implementations = getSubTypesOf(kClass)
338+
implementations
339+
.filterNot { it.kotlin.isAbstract }
340+
.forEach {
341+
val objectType = objectType(it.kotlin)
342+
val key = TypesCacheKey(it.kotlin.createType(), false)
343+
if (objectType is GraphQLTypeReference) {
344+
builder.possibleType(objectType)
345+
} else {
346+
builder.possibleType(objectType as GraphQLObjectType)
347+
}
348+
cache.put(key, KGraphQLType(it.kotlin, objectType))
349+
}
350+
351+
builder.build()
352+
}
349353
}
350354

351355
private fun getSubTypesOf(kclass: KClass<*>): MutableSet<out Class<out Any>> =

src/main/kotlin/com/expedia/graphql/schema/generator/TypesCache.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.expedia.graphql.schema.exceptions.CouldNotGetNameOfEnumException
77
import com.expedia.graphql.schema.exceptions.TypeNotSupportedException
88
import com.expedia.graphql.schema.models.KGraphQLType
99
import graphql.schema.GraphQLType
10+
import graphql.schema.GraphQLTypeReference
1011
import kotlin.reflect.KClass
1112
import kotlin.reflect.KType
1213
import kotlin.reflect.full.isSubclassOf
@@ -88,7 +89,14 @@ internal class TypesCache(private val supportedPackages: List<String>) {
8889
}
8990
}
9091

91-
fun putTypeUnderConstruction(kClass: KClass<*>) = typeUnderConstruction.add(kClass)
92+
private fun putTypeUnderConstruction(kClass: KClass<*>) = typeUnderConstruction.add(kClass)
9293

93-
fun isTypeUnderConstruction(kClass: KClass<*>) = typeUnderConstruction.contains(kClass)
94+
fun buildIfNotUnderConstruction(kClass: KClass<*>, build: (KClass<*>) -> GraphQLType): GraphQLType {
95+
return if (typeUnderConstruction.contains(kClass)) {
96+
GraphQLTypeReference.typeRef(kClass.simpleName)
97+
} else {
98+
putTypeUnderConstruction(kClass)
99+
build(kClass)
100+
}
101+
}
94102
}

src/test/kotlin/com/expedia/graphql/schema/generator/PolymorphicTests.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ class PolymorphicTests {
2323
graphqlType?.let { union ->
2424
assertTrue(union.types.any { it.name == "LeftHand" })
2525
assertTrue(union.types.any { it.name == "RightHand" })
26+
assertTrue(union.types.any { it.name == "Arm" })
2627
}
2728

2829
assertNotNull(schema.getType("RightHand"))
29-
assertNotNull(schema.getType("LeftHand"))
30+
val leftHandType = schema.getType("LeftHand") as? GraphQLObjectType
31+
32+
assertNotNull(leftHandType)
33+
assertNotNull(leftHandType?.getFieldDefinition("associatedWith"))
3034
}
3135

3236
@Test
@@ -74,6 +78,14 @@ class PolymorphicTests {
7478
assertTrue { it.types.contains(carType) }
7579
}
7680
}
81+
82+
@Test
83+
fun `Interfaces can declare properties of their own type`() {
84+
val schema = toSchema(listOf(TopLevelObjectDef(QueryWithRecursiveType())), config = testSchemaConfig)
85+
86+
val personType = schema.getType("Person")
87+
assertNotNull(personType)
88+
}
7789
}
7890

7991
class QueryWithInterface {
@@ -101,20 +113,25 @@ data class AnImplementation(
101113
class QueryWithUnion {
102114
fun query(whichHand: String): BodyPart = when (whichHand) {
103115
"right" -> RightHand(12)
104-
else -> LeftHand("hello world")
116+
else -> LeftHand("hello world", Arm(true))
105117
}
106118
}
107119

108120
interface BodyPart
109121

110122
data class LeftHand(
111-
val field: String
123+
val field: String,
124+
val associatedWith: BodyPart
112125
) : BodyPart
113126

114127
data class RightHand(
115128
val property: Int
116129
) : BodyPart
117130

131+
data class Arm(
132+
val value: Boolean
133+
) : BodyPart
134+
118135
class QueryWithInterfaceAnUnion {
119136
fun product(): Product = Car("DB9", "black")
120137
}
@@ -126,3 +143,20 @@ interface Vehicle {
126143
interface Product
127144

128145
data class Car(val model: String, override val color: String) : Vehicle, Product
146+
147+
class QueryWithRecursiveType {
148+
fun query(): Person = Father("knock knock", Child())
149+
}
150+
151+
interface Person {
152+
val child: Person?
153+
}
154+
155+
data class Child(
156+
override val child: Person? = null
157+
) : Person
158+
159+
data class Father(
160+
val dadJoke: String,
161+
override val child: Person?
162+
) : Person

0 commit comments

Comments
 (0)