Skip to content

Commit 20e512b

Browse files
smyrickShane Myrick
and
Shane Myrick
authored
Support sealed classes for interfaces (#645)
* Support sealed classes for interfaces * Update docs and examples with more sealed classes Co-authored-by: Shane Myrick <[email protected]>
1 parent 15810a7 commit 20e512b

File tree

9 files changed

+250
-129
lines changed

9 files changed

+250
-129
lines changed

docs/writing-schemas/interfaces.md

Lines changed: 131 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,131 @@
1-
---
2-
id: interfaces
3-
title: Interfaces
4-
---
5-
6-
Functions returning interfaces will automatically expose all the types implementing this interface that are available on
7-
the classpath. Due to the GraphQL distinction between interface and a union type, interfaces need to specify at least
8-
one common field (property or a function).
9-
10-
Abstract classes will also be converted to a GraphQL Interface.
11-
12-
```kotlin
13-
interface Animal {
14-
val type: AnimalType
15-
fun sound(): String
16-
}
17-
18-
enum class AnimalType {
19-
CAT,
20-
DOG
21-
}
22-
23-
class Dog : Animal {
24-
override val type = AnimalType.DOG
25-
26-
override fun sound() = "bark"
27-
28-
fun barkAtEveryone(): String = "bark at everyone"
29-
}
30-
31-
class Cat : Animal {
32-
override val type = AnimalType.CAT
33-
34-
override fun sound() = "meow"
35-
36-
fun ignoreEveryone(): String = "ignore everyone"
37-
}
38-
39-
class PolymorphicQuery {
40-
41-
fun animal(type: AnimalType): Animal? = when (type) {
42-
AnimalType.CAT -> Cat()
43-
AnimalType.DOG -> Dog()
44-
else -> null
45-
}
46-
}
47-
```
48-
49-
Code above will produce the following GraphQL schema
50-
51-
```graphql
52-
interface Animal {
53-
type: AnimalType!
54-
sound: String!
55-
}
56-
57-
enum AnimalType {
58-
CAT
59-
DOG
60-
}
61-
62-
type Cat implements Animal {
63-
type: AnimalType!
64-
ignoreEveryone: String!
65-
sound: String!
66-
}
67-
68-
type Dog implements Animal {
69-
type: AnimalType!
70-
barkAtEveryone: String!
71-
sound: String!
72-
}
73-
74-
type TopLevelQuery {
75-
animal(type: AnimalType!): Animal
76-
}
77-
78-
```
79-
80-
### Known Issues
81-
> NOTE: Due to a feature added in 1.0.0, we no longer support multiple levels of interfaces in a schema because the GraphQL spec does not support this feature. [See 419](https://github.com/ExpediaGroup/graphql-kotlin/issues/419). If you do have multiple interfaces you will have to either combine them into a single interface or ignore all the parent interfaces.
82-
83-
#### Invalid Schema
84-
```kotlin
85-
interface FirstLevel {
86-
val id: String
87-
}
88-
89-
interface SecondLevel : FirstLevel {
90-
val name: String
91-
}
92-
93-
class MyClass(override val id: String, override val name: String) : SecondLevel
94-
```
95-
96-
#### Valid Schema
97-
```kotlin
98-
@GraphQLIgnore
99-
interface FirstLevel {
100-
val id: String
101-
}
102-
103-
interface SecondLevel : FirstLevel {
104-
val name: String
105-
}
106-
107-
class MyClass(override val id: String, override val name: String) : SecondLevel
108-
```
109-
OR
110-
```kotlin
111-
interface FirstLevel {
112-
val id: String
113-
val name: String
114-
}
115-
116-
class MyClass(override val id: String, override val name: String) : FirstLevel
117-
```
1+
---
2+
id: interfaces
3+
title: Interfaces
4+
---
5+
6+
Functions returning interfaces will automatically expose all the types implementing this interface that are available on
7+
the classpath. Due to the GraphQL distinction between interface and a union type, interfaces need to specify at least
8+
one common field (property or a function).
9+
10+
Abstract and sealed classes will also be converted to a GraphQL Interface.
11+
12+
```kotlin
13+
interface Animal {
14+
val type: AnimalType
15+
fun sound(): String
16+
}
17+
18+
enum class AnimalType {
19+
CAT,
20+
DOG
21+
}
22+
23+
class Dog : Animal {
24+
override val type = AnimalType.DOG
25+
26+
override fun sound() = "bark"
27+
28+
fun barkAtEveryone(): String = "bark at everyone"
29+
}
30+
31+
class Cat : Animal {
32+
override val type = AnimalType.CAT
33+
34+
override fun sound() = "meow"
35+
36+
fun ignoreEveryone(): String = "ignore everyone"
37+
}
38+
39+
class PolymorphicQuery {
40+
41+
fun animal(type: AnimalType): Animal? = when (type) {
42+
AnimalType.CAT -> Cat()
43+
AnimalType.DOG -> Dog()
44+
else -> null
45+
}
46+
}
47+
```
48+
49+
The above code will produce the following GraphQL schema:
50+
51+
```graphql
52+
interface Animal {
53+
type: AnimalType!
54+
sound: String!
55+
}
56+
57+
enum AnimalType {
58+
CAT
59+
DOG
60+
}
61+
62+
type Cat implements Animal {
63+
type: AnimalType!
64+
ignoreEveryone: String!
65+
sound: String!
66+
}
67+
68+
type Dog implements Animal {
69+
type: AnimalType!
70+
barkAtEveryone: String!
71+
sound: String!
72+
}
73+
74+
type TopLevelQuery {
75+
animal(type: AnimalType!): Animal
76+
}
77+
78+
```
79+
80+
### Abstract and Sealed Classes
81+
[Abstract](https://kotlinlang.org/docs/reference/classes.html#abstract-classes) and [sealed](https://kotlinlang.org/docs/reference/sealed-classes.html) classes can also be used for interface types.
82+
83+
```kotlin
84+
abstract class Shape(val area: Double)
85+
class Circle(radius: Double) : Shape(PI * radius * radius)
86+
class Square(sideLength: Double) : Shape(sideLength * sideLength)
87+
88+
sealed class Pet(val name: String) {
89+
class Dog(name: String, val goodBoysReceived: Int) : Pet(name)
90+
class Cat(name: String, val livesRemaining: Int) : Pet(name)
91+
}
92+
```
93+
94+
### Known Issues
95+
> We currently do not support multiple levels of interfaces in a schema. We are waiting until graphql-java supports the newly added feature to the GraphQL spec. [See 589](https://github.com/ExpediaGroup/graphql-kotlin/issues/589). If you do have multiple interfaces you will have to either combine them into a single interface or ignore all the parent interfaces by marking them private or using `@GraphQLIgnore`.
96+
97+
#### Invalid Schema
98+
```kotlin
99+
interface FirstLevel {
100+
val id: String
101+
}
102+
103+
interface SecondLevel : FirstLevel {
104+
val name: String
105+
}
106+
107+
class MyClass(override val id: String, override val name: String) : SecondLevel
108+
```
109+
110+
#### Valid Schema
111+
```kotlin
112+
@GraphQLIgnore
113+
interface FirstLevel {
114+
val id: String
115+
}
116+
117+
interface SecondLevel : FirstLevel {
118+
val name: String
119+
}
120+
121+
class MyClass(override val id: String, override val name: String) : SecondLevel
122+
```
123+
OR
124+
```kotlin
125+
interface FirstLevel {
126+
val id: String
127+
val name: String
128+
}
129+
130+
class MyClass(override val id: String, override val name: String) : FirstLevel
131+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.model
18+
19+
sealed class Fruit(val color: String) {
20+
class Apple(private val variety: String) : Fruit(if (variety == "red delicious") "red" else "green")
21+
class Orange() : Fruit("orange")
22+
}

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/PolymorphicQuery.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.expediagroup.graphql.examples.model.AnimalType
2222
import com.expediagroup.graphql.examples.model.BodyPart
2323
import com.expediagroup.graphql.examples.model.Cat
2424
import com.expediagroup.graphql.examples.model.Dog
25+
import com.expediagroup.graphql.examples.model.Fruit
2526
import com.expediagroup.graphql.examples.model.LeftHand
2627
import com.expediagroup.graphql.examples.model.RightHand
2728
import com.expediagroup.graphql.spring.operations.Query
@@ -46,4 +47,7 @@ class PolymorphicQuery : Query {
4647
"right" -> RightHand(12)
4748
else -> LeftHand("hello world")
4849
}
50+
51+
@GraphQLDescription("Example of interfaces with sealed classes")
52+
fun getFruit(orange: Boolean) = if (orange) Fruit.Orange() else Fruit.Apple("granny smith")
4953
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal fun KClass<*>.getValidSuperclasses(hooks: SchemaGeneratorHooks): List<K
5858
internal fun KClass<*>.findConstructorParameter(name: String): KParameter? =
5959
this.primaryConstructor?.findParameterByName(name)
6060

61-
internal fun KClass<*>.isInterface(): Boolean = this.java.isInterface || this.isAbstract
61+
internal fun KClass<*>.isInterface(): Boolean = this.java.isInterface || this.isAbstract || this.isSealed
6262

6363
internal fun KClass<*>.isUnion(): Boolean =
6464
this.isInterface() && this.declaredMemberProperties.isEmpty() && this.declaredMemberFunctions.isEmpty()

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/PolymorphicTests.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,6 @@ data class Father(
257257

258258
class QueryWithAbstract {
259259
fun query(): MyAbstract = MyClass(id = 1, name = "JUnit")
260-
261-
fun queryImplementation(): MyClass = MyClass(id = 1, name = "JUnit_2")
262260
}
263261

264262
@Suppress("UnnecessaryAbstractClass")

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ open class KClassExtensionsTest {
131131

132132
interface TestInterface
133133

134+
sealed class Pet(val name: String) {
135+
class Dog(name: String, val goodBoysReceived: Int) : Pet(name)
136+
class Cat(name: String, val livesRemaining: Int) : Pet(name)
137+
}
138+
134139
private interface InvalidPropertyUnionInterface {
135140
val test: Int
136141
get() = 1
@@ -258,6 +263,7 @@ open class KClassExtensionsTest {
258263
fun `test graphql interface extension`() {
259264
assertTrue(TestInterface::class.isInterface())
260265
assertTrue(SomeAbstractClass::class.isInterface())
266+
assertTrue(Pet::class.isInterface())
261267
assertFalse(MyTestClass::class.isInterface())
262268
}
263269

@@ -266,6 +272,7 @@ open class KClassExtensionsTest {
266272
assertTrue(TestInterface::class.isUnion())
267273
assertFalse(InvalidPropertyUnionInterface::class.isUnion())
268274
assertFalse(InvalidFunctionUnionInterface::class.isUnion())
275+
assertFalse(Pet::class.isUnion())
269276
}
270277

271278
// TODO remove JUnit condition once we only build artifacts using Java 11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.types
18+
19+
import com.expediagroup.graphql.extensions.unwrapType
20+
import graphql.schema.GraphQLInterfaceType
21+
import org.junit.jupiter.api.Test
22+
import kotlin.reflect.full.createType
23+
import kotlin.test.assertEquals
24+
import kotlin.test.assertTrue
25+
26+
class GenerateGraphQLTypeKtTest : TypeTestHelper() {
27+
28+
@Test
29+
fun generateGraphQLType() {
30+
assertEquals(0, generator.additionalTypes.size)
31+
val result = generateGraphQLType(generator, Pet::class.createType()).unwrapType()
32+
assertTrue(result is GraphQLInterfaceType)
33+
assertEquals("name", result.fieldDefinitions.first().name)
34+
assertEquals(2, generator.additionalTypes.size)
35+
}
36+
37+
sealed class Pet(open val name: String) {
38+
data class Dog(override val name: String, val goodBoysReceived: Int) : Pet(name)
39+
data class Cat(override val name: String, val livesRemaining: Int) : Pet(name)
40+
}
41+
}

0 commit comments

Comments
 (0)