Skip to content

Commit

Permalink
Throw exception on multi-level interface
Browse files Browse the repository at this point in the history
Resolves #419

According to the spec, an interface can not implement another interface. This is not caught by our schema generator but instead just returns the first level interface as the type in the schema. We are now throwing an exception to help translate this understanding of GraphQL through the Kotlin code
  • Loading branch information
smyrick committed Oct 2, 2019
1 parent 748529f commit 8137641
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2019 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.exceptions

/**
* Thrown when a interface implements another interface or abstract class that is not exluded from the schema.
*
* This is an invalid schema until the GraphQL spec is updated
* https://github.com/ExpediaGroup/graphql-kotlin/issues/419
*/
class InvalidInterfaceException : GraphQLKotlinException("Interfaces can not have any superclasses")
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.expediagroup.graphql.generator.filters

import com.expediagroup.graphql.generator.extensions.isGraphQLIgnored
import com.expediagroup.graphql.generator.extensions.isInterface
import com.expediagroup.graphql.generator.extensions.isPublic
import com.expediagroup.graphql.generator.extensions.isUnion
Expand All @@ -26,5 +27,6 @@ private typealias SuperclassFilter = (KClass<*>) -> Boolean
private val isPublic: SuperclassFilter = { it.isPublic() }
private val isInterface: SuperclassFilter = { it.isInterface() }
private val isNotUnion: SuperclassFilter = { it.isUnion().not() }
private val isNotIgnored: SuperclassFilter = { it.isGraphQLIgnored().not() }

internal val superclassFilters: List<SuperclassFilter> = listOf(isPublic, isInterface, isNotUnion)
internal val superclassFilters: List<SuperclassFilter> = listOf(isPublic, isInterface, isNotUnion, isNotIgnored)
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package com.expediagroup.graphql.generator.types

import com.expediagroup.graphql.exceptions.InvalidInterfaceException
import com.expediagroup.graphql.generator.SchemaGenerator
import com.expediagroup.graphql.generator.TypeBuilder
import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
import com.expediagroup.graphql.generator.extensions.getSimpleName
import com.expediagroup.graphql.generator.extensions.getValidFunctions
import com.expediagroup.graphql.generator.extensions.getValidProperties
import com.expediagroup.graphql.generator.extensions.getValidSuperclasses
import com.expediagroup.graphql.generator.extensions.safeCast
import graphql.TypeResolutionEnvironment
import graphql.schema.GraphQLInterfaceType
Expand All @@ -34,6 +36,11 @@ import kotlin.reflect.full.createType
internal class InterfaceBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
internal fun interfaceType(kClass: KClass<*>): GraphQLType {
return state.cache.buildIfNotUnderConstruction(kClass) {

if (kClass.getValidSuperclasses(generator.config.hooks).isNotEmpty()) {
throw InvalidInterfaceException()
}

val builder = GraphQLInterfaceType.newInterface()

builder.name(kClass.getSimpleName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.expediagroup.graphql.generator.extensions

import com.expediagroup.graphql.annotations.GraphQLIgnore
import com.expediagroup.graphql.annotations.GraphQLName
import com.expediagroup.graphql.exceptions.CouldNotGetNameOfKClassException
import com.expediagroup.graphql.hooks.NoopSchemaGeneratorHooks
Expand Down Expand Up @@ -83,6 +84,13 @@ open class KClassExtensionsTest {

internal class UnionSuperclass : TestInterface

@GraphQLIgnore
internal interface IgnoredInterface {
val id: String
}

internal class ClassWithNoValidSuperclass(override val id: String) : IgnoredInterface

internal class InterfaceSuperclass : InvalidFunctionUnionInterface {
override fun getTest() = 2
}
Expand Down Expand Up @@ -175,6 +183,12 @@ open class KClassExtensionsTest {
assertTrue(superclasses.isEmpty())
}

@Test
fun `Superclasses are not included when marked as ignored`() {
val superclasses = ClassWithNoValidSuperclass::class.getValidSuperclasses(noopHooks)
assertTrue(superclasses.isEmpty())
}

@Test
fun `test findConstructorParamter`() {
assertNotNull(MyTestClass::class.findConstructorParamter("publicProperty"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2019 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.test.integration

import com.expediagroup.graphql.TopLevelObject
import com.expediagroup.graphql.exceptions.InvalidInterfaceException
import com.expediagroup.graphql.testSchemaConfig
import com.expediagroup.graphql.toSchema
import org.junit.jupiter.api.Test
import kotlin.test.assertFailsWith

class InterfaceOfInterfaceTest {

@Test
fun `interface of interface`() {
val queries = listOf(TopLevelObject(InterfaceOfInterfaceQuery()))

assertFailsWith(InvalidInterfaceException::class) {
toSchema(queries = queries, config = testSchemaConfig)
}
}

interface FirstLevel {
val id: String
}

interface SecondLevel : FirstLevel {
val name: String
}

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

class InterfaceOfInterfaceQuery {
fun getClass() = MyClass(id = "1", name = "fooBar")
}
}

0 comments on commit 8137641

Please sign in to comment.