Skip to content

Support sealed classes for interfaces #645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 131 additions & 117 deletions docs/writing-schemas/interfaces.md
Original file line number Diff line number Diff line change
@@ -1,117 +1,131 @@
---
id: interfaces
title: Interfaces
---

Functions returning interfaces will automatically expose all the types implementing this interface that are available on
the classpath. Due to the GraphQL distinction between interface and a union type, interfaces need to specify at least
one common field (property or a function).

Abstract classes will also be converted to a GraphQL Interface.

```kotlin
interface Animal {
val type: AnimalType
fun sound(): String
}

enum class AnimalType {
CAT,
DOG
}

class Dog : Animal {
override val type = AnimalType.DOG

override fun sound() = "bark"

fun barkAtEveryone(): String = "bark at everyone"
}

class Cat : Animal {
override val type = AnimalType.CAT

override fun sound() = "meow"

fun ignoreEveryone(): String = "ignore everyone"
}

class PolymorphicQuery {

fun animal(type: AnimalType): Animal? = when (type) {
AnimalType.CAT -> Cat()
AnimalType.DOG -> Dog()
else -> null
}
}
```

Code above will produce the following GraphQL schema

```graphql
interface Animal {
type: AnimalType!
sound: String!
}

enum AnimalType {
CAT
DOG
}

type Cat implements Animal {
type: AnimalType!
ignoreEveryone: String!
sound: String!
}

type Dog implements Animal {
type: AnimalType!
barkAtEveryone: String!
sound: String!
}

type TopLevelQuery {
animal(type: AnimalType!): Animal
}

```

### Known Issues
> 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.

#### Invalid Schema
```kotlin
interface FirstLevel {
val id: String
}

interface SecondLevel : FirstLevel {
val name: String
}

class MyClass(override val id: String, override val name: String) : SecondLevel
```

#### Valid Schema
```kotlin
@GraphQLIgnore
interface FirstLevel {
val id: String
}

interface SecondLevel : FirstLevel {
val name: String
}

class MyClass(override val id: String, override val name: String) : SecondLevel
```
OR
```kotlin
interface FirstLevel {
val id: String
val name: String
}

class MyClass(override val id: String, override val name: String) : FirstLevel
```
---
id: interfaces
title: Interfaces
---

Functions returning interfaces will automatically expose all the types implementing this interface that are available on
the classpath. Due to the GraphQL distinction between interface and a union type, interfaces need to specify at least
one common field (property or a function).

Abstract and sealed classes will also be converted to a GraphQL Interface.

```kotlin
interface Animal {
val type: AnimalType
fun sound(): String
}

enum class AnimalType {
CAT,
DOG
}

class Dog : Animal {
override val type = AnimalType.DOG

override fun sound() = "bark"

fun barkAtEveryone(): String = "bark at everyone"
}

class Cat : Animal {
override val type = AnimalType.CAT

override fun sound() = "meow"

fun ignoreEveryone(): String = "ignore everyone"
}

class PolymorphicQuery {

fun animal(type: AnimalType): Animal? = when (type) {
AnimalType.CAT -> Cat()
AnimalType.DOG -> Dog()
else -> null
}
}
```

The above code will produce the following GraphQL schema:

```graphql
interface Animal {
type: AnimalType!
sound: String!
}

enum AnimalType {
CAT
DOG
}

type Cat implements Animal {
type: AnimalType!
ignoreEveryone: String!
sound: String!
}

type Dog implements Animal {
type: AnimalType!
barkAtEveryone: String!
sound: String!
}

type TopLevelQuery {
animal(type: AnimalType!): Animal
}

```

### Abstract and Sealed Classes
[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.

```kotlin
abstract class Shape(val area: Double)
class Circle(radius: Double) : Shape(PI * radius * radius)
class Square(sideLength: Double) : Shape(sideLength * sideLength)

sealed class Pet(val name: String) {
class Dog(name: String, val goodBoysReceived: Int) : Pet(name)
class Cat(name: String, val livesRemaining: Int) : Pet(name)
}
```

### Known Issues
> 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`.

#### Invalid Schema
```kotlin
interface FirstLevel {
val id: String
}

interface SecondLevel : FirstLevel {
val name: String
}

class MyClass(override val id: String, override val name: String) : SecondLevel
```

#### Valid Schema
```kotlin
@GraphQLIgnore
interface FirstLevel {
val id: String
}

interface SecondLevel : FirstLevel {
val name: String
}

class MyClass(override val id: String, override val name: String) : SecondLevel
```
OR
```kotlin
interface FirstLevel {
val id: String
val name: String
}

class MyClass(override val id: String, override val name: String) : FirstLevel
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.examples.model

sealed class Fruit(val color: String) {
class Apple(private val variety: String) : Fruit(if (variety == "red delicious") "red" else "green")
class Orange() : Fruit("orange")
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.expediagroup.graphql.examples.model.AnimalType
import com.expediagroup.graphql.examples.model.BodyPart
import com.expediagroup.graphql.examples.model.Cat
import com.expediagroup.graphql.examples.model.Dog
import com.expediagroup.graphql.examples.model.Fruit
import com.expediagroup.graphql.examples.model.LeftHand
import com.expediagroup.graphql.examples.model.RightHand
import com.expediagroup.graphql.spring.operations.Query
Expand All @@ -46,4 +47,7 @@ class PolymorphicQuery : Query {
"right" -> RightHand(12)
else -> LeftHand("hello world")
}

@GraphQLDescription("Example of interfaces with sealed classes")
fun getFruit(orange: Boolean) = if (orange) Fruit.Orange() else Fruit.Apple("granny smith")
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ internal fun KClass<*>.getValidSuperclasses(hooks: SchemaGeneratorHooks): List<K
internal fun KClass<*>.findConstructorParameter(name: String): KParameter? =
this.primaryConstructor?.findParameterByName(name)

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

internal fun KClass<*>.isUnion(): Boolean =
this.isInterface() && this.declaredMemberProperties.isEmpty() && this.declaredMemberFunctions.isEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,6 @@ data class Father(

class QueryWithAbstract {
fun query(): MyAbstract = MyClass(id = 1, name = "JUnit")

fun queryImplementation(): MyClass = MyClass(id = 1, name = "JUnit_2")
}

@Suppress("UnnecessaryAbstractClass")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ open class KClassExtensionsTest {

interface TestInterface

sealed class Pet(val name: String) {
class Dog(name: String, val goodBoysReceived: Int) : Pet(name)
class Cat(name: String, val livesRemaining: Int) : Pet(name)
}

private interface InvalidPropertyUnionInterface {
val test: Int
get() = 1
Expand Down Expand Up @@ -258,6 +263,7 @@ open class KClassExtensionsTest {
fun `test graphql interface extension`() {
assertTrue(TestInterface::class.isInterface())
assertTrue(SomeAbstractClass::class.isInterface())
assertTrue(Pet::class.isInterface())
assertFalse(MyTestClass::class.isInterface())
}

Expand All @@ -266,6 +272,7 @@ open class KClassExtensionsTest {
assertTrue(TestInterface::class.isUnion())
assertFalse(InvalidPropertyUnionInterface::class.isUnion())
assertFalse(InvalidFunctionUnionInterface::class.isUnion())
assertFalse(Pet::class.isUnion())
}

// TODO remove JUnit condition once we only build artifacts using Java 11
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2020 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.types

import com.expediagroup.graphql.extensions.unwrapType
import graphql.schema.GraphQLInterfaceType
import org.junit.jupiter.api.Test
import kotlin.reflect.full.createType
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class GenerateGraphQLTypeKtTest : TypeTestHelper() {

@Test
fun generateGraphQLType() {
assertEquals(0, generator.additionalTypes.size)
val result = generateGraphQLType(generator, Pet::class.createType()).unwrapType()
assertTrue(result is GraphQLInterfaceType)
assertEquals("name", result.fieldDefinitions.first().name)
assertEquals(2, generator.additionalTypes.size)
}

sealed class Pet(open val name: String) {
data class Dog(override val name: String, val goodBoysReceived: Int) : Pet(name)
data class Cat(override val name: String, val livesRemaining: Int) : Pet(name)
}
}
Loading