Skip to content

Add annotation to include extension function in schema #384

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

Closed
wants to merge 4 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.expediagroup.graphql.examples.model

import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.annotations.GraphQLExtensionFunction
import com.expediagroup.graphql.federation.directives.FieldSet
import com.expediagroup.graphql.federation.directives.KeyDirective
import java.util.UUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,31 @@
package com.expediagroup.graphql.examples.model

import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.annotations.GraphQLExtensionFunction
import com.expediagroup.graphql.annotations.GraphQLIgnore

@GraphQLDescription("A useful widget")
data class Widget(
@GraphQLDescription("The widget's value that can be null")
var value: Int? = null,
override var value: Int? = null,
@Deprecated(message = "This field is deprecated", replaceWith = ReplaceWith("value"))
@GraphQLDescription("The widget's deprecated value that shouldn't be used")
val deprecatedValue: Int? = value,
@GraphQLIgnore
val ignoredField: String? = "ignored",
private val hiddenField: String? = "hidden"
) {
): IWidget {

@GraphQLDescription("returns original value multiplied by target OR null if original value was null")
fun multiplyValueBy(multiplier: Int) = value?.times(multiplier)
}


interface IWidget{
var value: Int?
}
@GraphQLExtensionFunction
fun Widget.extension() = "this is an extension function"

@GraphQLExtensionFunction
fun IWidget.extensionWithParam(int: Int) = int
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.expediagroup.graphql.examples.query

import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.examples.model.IWidget
import com.expediagroup.graphql.spring.operations.Query
import com.expediagroup.graphql.examples.model.Widget
import org.springframework.stereotype.Component
Expand All @@ -29,4 +30,6 @@ class WidgetQuery: Query {

@GraphQLDescription("creates new widget for given ID")
fun widgetById(@GraphQLDescription("The special ingredient") id: Int): Widget? = Widget(id)

fun iWidgetById(@GraphQLDescription("The special ingredient") id: Int): IWidget? = Widget(id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.annotations

/**
* Used to indicate that an extension function should be included in the GraphQL schema
*/
@Target(AnnotationTarget.FUNCTION)
annotation class GraphQLExtensionFunction
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

import java.lang.reflect.Method

/**
* Thrown when a base type and super type both have an extension function with the same name
*/
open class ConflictingExtensionFunction(function: String, baseType: String, superType: String) :
GraphQLKotlinException("Base type $baseType and super type $superType have conflicting definitions of $function")
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

import java.lang.reflect.Method

/**
* Thrown when a function or property is annotated as an extension but is not a global (static) method
*/
open class InvalidExtensionFunction(function: Method) :
GraphQLKotlinException("Function ${function.name} is not a global Kotlin extension function")
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.expediagroup.graphql.generator

import com.expediagroup.graphql.annotations.GraphQLExtensionFunction
import com.expediagroup.graphql.exceptions.ConflictingExtensionFunction
import com.expediagroup.graphql.generator.extensions.asExtensionFunction
import com.expediagroup.graphql.generator.extensions.getFunctionName
import com.expediagroup.graphql.generator.extensions.getSimpleName
import com.expediagroup.graphql.generator.filters.functionFilters
import org.reflections.Reflections
import org.reflections.scanners.MethodAnnotationsScanner
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.allSupertypes

internal class ExtensionFunctionMapper(supportedPackages: List<String>) {

private val reflections = Reflections(supportedPackages, MethodAnnotationsScanner())

// currently throwing an error when something is annotated incorrectly. Other option would be just to filter it out
private val extensionFunctions: Map<String, List<KFunction<*>>> = reflections.getMethodsAnnotatedWith(GraphQLExtensionFunction::class.java)
.map { it.asExtensionFunction() }
.groupBy { it.parameters.first().type.getSimpleName() }

fun getValidExtensionFunctions(kclass: KClass<*>): List<KFunction<*>> {
val allSuperClasses = kclass.allSuperclasses
val functions = getExtensionFunctions(kclass).toMutableList()
val functionNames = functions.map { it.getFunctionName() }.toMutableSet()
allSuperClasses.forEach { superClass ->
val superClassExtensionFunction = getExtensionFunctions(superClass)
superClassExtensionFunction.forEach {
if (functionNames.add(it.getFunctionName()).not()) {
throw ConflictingExtensionFunction(it.getFunctionName(), kclass.getSimpleName(), superClass.getSimpleName())
}
}
functions.addAll(superClassExtensionFunction)
}
return functions
}

private fun getExtensionFunctions(kclass: KClass<*>): List<KFunction<*>> = (extensionFunctions[kclass.getSimpleName()]
?: listOf()).filter { func ->
functionFilters.all { it.invoke(func) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ open class SchemaGenerator(val config: SchemaGeneratorConfig) {

internal val state = SchemaGeneratorState(config.supportedPackages)
internal val subTypeMapper = SubTypeMapper(config.supportedPackages)
internal val extensionFunctionMapper = ExtensionFunctionMapper(config.supportedPackages)
internal val codeRegistry = GraphQLCodeRegistry.newCodeRegistry()

private val queryBuilder = QueryBuilder(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal open class TypeBuilder constructor(protected val generator: SchemaGener
protected val state: SchemaGeneratorState = generator.state
protected val config: SchemaGeneratorConfig = generator.config
protected val subTypeMapper: SubTypeMapper = generator.subTypeMapper
protected val extensionFunctionMapper: ExtensionFunctionMapper = generator.extensionFunctionMapper
protected val codeRegistry: GraphQLCodeRegistry.Builder = generator.codeRegistry

internal fun graphQLTypeOf(type: KType, inputType: Boolean = false, annotatedAsID: Boolean = false): GraphQLType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.generator.extensions

import com.expediagroup.graphql.exceptions.InvalidExtensionFunction
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import kotlin.reflect.KFunction
import kotlin.reflect.jvm.kotlinFunction

internal fun Method.isStatic() = Modifier.isStatic(this.modifiers)

@Throws(InvalidExtensionFunction::class)
internal fun Method.asExtensionFunction(): KFunction<*> {
// has an open setter so storing this in a variable
val function = this.kotlinFunction
if (function == null || this.isStatic().not() || this.parameterTypes.isEmpty()) {
throw InvalidExtensionFunction(this)
}
return function
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ internal class InterfaceBuilder(generator: SchemaGenerator) : TypeBuilder(genera
kClass.getValidFunctions(config.hooks)
.forEach { builder.field(generator.function(it, kClass.getSimpleName(), abstract = true)) }

extensionFunctionMapper.getValidExtensionFunctions(kClass)
.forEach { builder.field(generator.function(it, kClass.getSimpleName(), abstract = true)) }

subTypeMapper.getSubTypesOf(kClass)
.map { graphQLTypeOf(it.createType()) }
.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ internal class ObjectBuilder(generator: SchemaGenerator) : TypeBuilder(generator
kClass.getValidFunctions(config.hooks)
.forEach { builder.field(generator.function(it, name)) }

extensionFunctionMapper.getValidExtensionFunctions(kClass)
.forEach { builder.field(generator.function(it, name)) }

config.hooks.onRewireGraphQLType(builder.build()).safeCast()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.graphql.generator

import com.expediagroup.graphql.TopLevelObject
import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.annotations.GraphQLExtensionFunction
import com.expediagroup.graphql.annotations.GraphQLID
import com.expediagroup.graphql.annotations.GraphQLIgnore
import com.expediagroup.graphql.annotations.GraphQLName
Expand Down Expand Up @@ -369,6 +370,14 @@ class SchemaGeneratorTest {
assertEquals(expected = 1, actual = errors.size)
}

@Test
fun `SchemaGenerator defines extension type on class`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithExtensionFunction())), config = testSchemaConfig)
val extensionType = schema.getObjectType("EmptyClass")
val extensionField = extensionType.getFieldDefinition("extension").type as? GraphQLNonNull
assertEquals(Scalars.GraphQLString, extensionField?.wrappedType)
}

class QueryObject {
@GraphQLDescription("A GraphQL query method")
fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf())
Expand Down Expand Up @@ -568,4 +577,13 @@ class SchemaGeneratorTest {
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
}
}

class QueryWithExtensionFunction {
fun query(something: String): EmptyClass? = EmptyClass()
}

class EmptyClass
}

@GraphQLExtensionFunction
fun SchemaGeneratorTest.EmptyClass.extension(message: String): String = message
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.graphql.spring

import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -39,6 +40,7 @@ class PlaygroundAutoConfiguration(
) {

@Bean
@ConditionalOnMissingBean
@ExperimentalCoroutinesApi
fun playgroundRoute(): RouterFunction<ServerResponse> {
val body = playgroundHtml.inputStream.bufferedReader().use { reader ->
Expand Down