Skip to content

Commit fd28dbc

Browse files
filipncsapottere
authored andcommitted
Add failing multi-union example to EndToEndSpec.kt (#83)
* Add failing multi-union example to EndToEndSpec.kt * Special type handling for unions, in particular nested unions Exclude unions from the type-class bimap Expand possible types for unions recursively
1 parent ed8554f commit fd28dbc

File tree

4 files changed

+65
-5
lines changed

4 files changed

+65
-5
lines changed

src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
119119
// The dictionary doesn't need to know what classes are used with scalars.
120120
// In addition, scalars can have duplicate classes so that breaks the bi-map.
121121
// Input types can also be excluded from the dictionary, since it's only used for interfaces, unions, and enums.
122+
// Union types can also be excluded, as their possible types are resolved recursively later
122123
val dictionary = try {
123124
Maps.unmodifiableBiMap(HashBiMap.create<TypeDefinition, Class<*>>().also {
124-
dictionary.filter { it.value.typeClass != null && it.key !is InputObjectTypeDefinition }.mapValuesTo(it) { it.value.typeClass }
125+
dictionary.filter { it.value.typeClass != null && it.key !is InputObjectTypeDefinition && it.key !is UnionTypeDefinition}.mapValuesTo(it) { it.value.typeClass }
125126
})
126127
} catch (t: Throwable) {
127128
throw SchemaClassScannerError("Error creating bimap of type => class", t)
@@ -173,8 +174,9 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
173174
}
174175

175176
private fun getAllObjectTypeMembersOfDiscoveredUnions(): List<ObjectTypeDefinition> {
177+
val unionTypeNames = dictionary.keys.filterIsInstance<UnionTypeDefinition>().map { union -> union.name }.toSet()
176178
return dictionary.keys.filterIsInstance<UnionTypeDefinition>().map { union ->
177-
union.memberTypes.filterIsInstance<TypeName>().map { objectDefinitionsByName[it.name] ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $union") }
179+
union.memberTypes.filterIsInstance<TypeName>().filter { !unionTypeNames.contains(it.name) }.map { objectDefinitionsByName[it.name] ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $union") }
178180
}.flatten().distinct()
179181
}
180182

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,27 @@ class SchemaParser internal constructor(private val dictionary: TypeClassDiction
190190
.description(getDocumentation(definition))
191191
.typeResolver(TypeResolverProxy())
192192

193+
getLeafUnionObjects(definition, types).forEach { builder.possibleType(it) }
194+
return builder.build()
195+
}
196+
197+
private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): List<GraphQLObjectType> {
198+
val name = definition.name
199+
val leafObjects = mutableListOf<GraphQLObjectType>()
200+
193201
definition.memberTypes.forEach {
194202
val typeName = (it as TypeName).name
195-
builder.possibleType(types.find { it.name == typeName } ?: throw SchemaError("Expected object type '$typeName' for union type '$name', but found none!"))
196-
}
197203

198-
return builder.build()
204+
// Is this a nested union? If so, expand
205+
val nestedUnion : UnionTypeDefinition? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name }
206+
207+
if (nestedUnion != null) {
208+
leafObjects.addAll(getLeafUnionObjects(nestedUnion, types))
209+
} else {
210+
leafObjects.add(types.find { it.name == typeName } ?: throw SchemaError("Expected object type '$typeName' for union type '$name', but found none!"))
211+
}
212+
}
213+
return leafObjects
199214
}
200215

201216
private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition : FieldDefinition): GraphQLFieldDefinition.Builder {

src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,31 @@ class EndToEndSpec extends Specification {
113113
data.allItems
114114
}
115115

116+
def "generated schema should handle nested union types"() {
117+
when:
118+
def data = Utils.assertNoGraphQlErrors(gql) {
119+
'''
120+
{
121+
nestedUnionItems {
122+
... on Item {
123+
itemId: id
124+
}
125+
... on OtherItem {
126+
otherItemId: id
127+
}
128+
... on ThirdItem {
129+
thirdItemId: id
130+
}
131+
}
132+
}
133+
'''
134+
}
135+
136+
then:
137+
// TODO, flimsy. Need to test with two different numbers of items, because the mutation test may have run before this test.
138+
data.nestedUnionItems == [[itemId: 0], [itemId: 1], [otherItemId: 0], [otherItemId: 1], [thirdItemId: 100]] || data.nestedUnionItems == [[itemId: 0], [itemId: 1], [itemId: 2], [otherItemId: 0], [otherItemId: 1], [thirdItemId: 100]]
139+
}
140+
116141
def "generated schema should handle scalar types"() {
117142
when:
118143
def data = Utils.assertNoGraphQlErrors(gql) {

src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpec.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fun createSchema() = SchemaParser.newParser()
1313
.resolvers(Query(), Mutation(), Subscription(), ItemResolver(), UnusedRootResolver(), UnusedResolver())
1414
.scalars(CustomUUIDScalar)
1515
.dictionary("OtherItem", OtherItemWithWrongName::class.java)
16+
.dictionary("ThirdItem", ThirdItem::class.java)
1617
.build()
1718
.makeExecutableSchema()
1819

@@ -27,6 +28,8 @@ type Query {
2728
items(itemsInput: ItemSearchInput!): [Item!]
2829
optionalItem(itemsInput: ItemSearchInput!): Item
2930
allItems: [AllItems!]
31+
otherUnionItems: [OtherUnion!]
32+
nestedUnionItems: [NestedUnion!]
3033
itemsByInterface: [ItemInterface!]
3134
itemByUUID(uuid: UUID!): Item
3235
itemsWithOptionalInput(itemsInput: ItemSearchInput): [Item!]
@@ -115,6 +118,14 @@ interface ItemInterface {
115118
116119
union AllItems = Item | OtherItem
117120
121+
type ThirdItem {
122+
id: Int!
123+
}
124+
125+
union OtherUnion = Item | ThirdItem
126+
127+
union NestedUnion = OtherUnion | OtherItem
128+
118129
type Tag {
119130
id: Int!
120131
name: String!
@@ -132,11 +143,17 @@ val otherItems = mutableListOf(
132143
OtherItemWithWrongName(1, "otherItem2", Type.TYPE_2, UUID.fromString("38f685f1-b460-4a54-d17f-7fd69e8cf3f8"))
133144
)
134145

146+
val thirdItems = mutableListOf(
147+
ThirdItem(100)
148+
)
149+
135150
class Query: GraphQLQueryResolver, ListListResolver<String>() {
136151
fun isEmpty() = items.isEmpty()
137152
fun items(input: ItemSearchInput): List<Item> = items.filter { it.name == input.name }
138153
fun optionalItem(input: ItemSearchInput) = items(input).firstOrNull()?.let { Optional.of(it) } ?: Optional.empty()
139154
fun allItems(): List<Any> = items + otherItems
155+
fun otherUnionItems(): List<Any> = items + thirdItems
156+
fun nestedUnionItems(): List<Any> = items + otherItems + thirdItems
140157
fun itemsByInterface(): List<ItemInterface> = items + otherItems
141158
fun itemByUUID(uuid: UUID): Item? = items.find { it.uuid == uuid }
142159
fun itemsWithOptionalInput(input: ItemSearchInput?) = if(input == null) items else items(input)
@@ -195,6 +212,7 @@ interface ItemInterface {
195212
enum class Type { TYPE_1, TYPE_2 }
196213
data class Item(val id: Int, override val name: String, override val type: Type, override val uuid:UUID, val tags: List<Tag>) : ItemInterface
197214
data class OtherItemWithWrongName(val id: Int, override val name: String, override val type: Type, override val uuid:UUID) : ItemInterface
215+
data class ThirdItem(val id: Int)
198216
data class Tag(val id: Int, val name: String)
199217
data class ItemSearchInput(val name: String)
200218
data class NewItemInput(val name: String, val type: Type)

0 commit comments

Comments
 (0)