diff --git a/build.gradle b/build.gradle index c29bee2e..55bf7241 100644 --- a/build.gradle +++ b/build.gradle @@ -2,16 +2,13 @@ group 'com.github.pgutkowski' version '0.3.0-beta' buildscript { - ext.kotlin_version = '1.3.0-rc-116' + ext.kotlin_version = '1.3.0' repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } - maven { - url "http://dl.bintray.com/kotlin/kotlin-eap" - } jcenter() } dependencies { @@ -42,10 +39,10 @@ repositories { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2-eap13' + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0' - compile "com.fasterxml.jackson.core:jackson-databind:2.9.3" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.3" + compile "com.fasterxml.jackson.core:jackson-databind:2.9.7" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" compile "com.github.ben-manes.caffeine:caffeine:1.0.0" diff --git a/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/dsl/SchemaConfigurationDSL.kt b/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/dsl/SchemaConfigurationDSL.kt index 15f18a9d..1e5607a8 100644 --- a/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/dsl/SchemaConfigurationDSL.kt +++ b/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/dsl/SchemaConfigurationDSL.kt @@ -4,23 +4,23 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.pgutkowski.kgraphql.configuration.SchemaConfiguration -import kotlinx.coroutines.CommonPool import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers class SchemaConfigurationDSL { var useDefaultPrettyPrinter: Boolean = false var useCachingDocumentParser: Boolean = true var objectMapper: ObjectMapper = jacksonObjectMapper() - var documentParserCacheMaximumSize : Long = 1000L - var acceptSingleValueAsArray : Boolean = true - var coroutineDispatcher: CoroutineDispatcher = CommonPool + var documentParserCacheMaximumSize: Long = 1000L + var acceptSingleValueAsArray: Boolean = true + var coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default - internal fun update(block : SchemaConfigurationDSL.() -> Unit) = block() + internal fun update(block: SchemaConfigurationDSL.() -> Unit) = block() - internal fun build() : SchemaConfiguration { + internal fun build(): SchemaConfiguration { objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, acceptSingleValueAsArray) - return SchemaConfiguration ( + return SchemaConfiguration( useCachingDocumentParser, documentParserCacheMaximumSize, objectMapper, @@ -28,4 +28,4 @@ class SchemaConfigurationDSL { coroutineDispatcher ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/execution/ParallelRequestExecutor.kt b/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/execution/ParallelRequestExecutor.kt index f04f5226..1b557e65 100644 --- a/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/execution/ParallelRequestExecutor.kt +++ b/src/main/kotlin/com/github/pgutkowski/kgraphql/schema/execution/ParallelRequestExecutor.kt @@ -18,18 +18,22 @@ import com.github.pgutkowski.kgraphql.schema.scalar.serializeScalar import com.github.pgutkowski.kgraphql.schema.structure2.Field import com.github.pgutkowski.kgraphql.schema.structure2.InputValue import com.github.pgutkowski.kgraphql.schema.structure2.Type +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.defer import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlin.coroutines.CoroutineContext import kotlin.reflect.KProperty1 @Suppress("UNCHECKED_CAST") // For valid structure there is no risk of ClassCastException -class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { +class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, CoroutineScope { data class ExecutionContext(val variables: Variables, val requestContext: Context) + override val coroutineContext: CoroutineContext = Job() + private val argumentsHandler = ArgumentsHandler(schema) private val jsonNodeFactory = JsonNodeFactory.instance @@ -37,15 +41,15 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { private val dispatcher = schema.configuration.coroutineDispatcher private val objectWriter = schema.configuration.objectMapper.writer().let { - if(schema.configuration.useDefaultPrettyPrinter){ + if (schema.configuration.useDefaultPrettyPrinter) { it.withDefaultPrettyPrinter() } else { it } } - override suspend fun suspendExecute(plan : ExecutionPlan, variables: VariablesJson, context: Context) : String { - val root = jsonNodeFactory.objectNode() + override suspend fun suspendExecute(plan: ExecutionPlan, variables: VariablesJson, context: Context): String { + val root = jsonNodeFactory.objectNode() val data = root.putObject("data") val channel = Channel>() val jobs = plan @@ -71,27 +75,27 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { try { val (execution, jsonNode) = channel.receive() resultMap.put(execution, jsonNode) - } catch(e : Exception){ - jobs.forEach{ it.cancel() } + } catch (e: Exception) { + jobs.forEach { it.cancel() } throw e } } channel.close() - for(operation in plan){ + for (operation in plan) { data.set(operation.aliasOrKey, resultMap[operation]) } return objectWriter.writeValueAsString(root) } - override fun execute(plan : ExecutionPlan, variables: VariablesJson, context: Context) : String = runBlocking { + override fun execute(plan: ExecutionPlan, variables: VariablesJson, context: Context): String = runBlocking { suspendExecute(plan, variables, context) } - private suspend fun writeOperation(ctx: ExecutionContext, node: Execution.Node, operation: FunctionWrapper) : JsonNode { + private suspend fun writeOperation(ctx: ExecutionContext, node: Execution.Node, operation: FunctionWrapper): JsonNode { node.field.checkAccess(null, ctx.requestContext) - val operationResult : T? = operation.invoke ( + val operationResult: T? = operation.invoke( funName = node.field.name, receiver = null, inputValues = node.field.arguments, @@ -105,7 +109,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { private suspend fun createUnionOperationNode(ctx: ExecutionContext, parent: T, node: Execution.Union, unionProperty: Field.Union): JsonNode { node.field.checkAccess(parent, ctx.requestContext) - val operationResult : Any? = unionProperty.invoke( + val operationResult: Any? = unionProperty.invoke( funName = unionProperty.name, receiver = parent, inputValues = node.field.arguments, @@ -115,21 +119,21 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { val returnType = unionProperty.returnType.possibleTypes.find { it.isInstance(operationResult) } - if(returnType == null) throw ExecutionException ( - "Unexpected type of union property value, expected one of : ${unionProperty.type.possibleTypes }." + + if (returnType == null) throw ExecutionException( + "Unexpected type of union property value, expected one of : ${unionProperty.type.possibleTypes}." + " value was $operationResult" ) return createNode(ctx, operationResult, node, returnType) } - private suspend fun createNode(ctx: ExecutionContext, value : T?, node: Execution.Node, returnType: Type) : JsonNode { + private suspend fun createNode(ctx: ExecutionContext, value: T?, node: Execution.Node, returnType: Type): JsonNode { return when { value == null -> createNullNode(node, returnType) //check value, not returnType, because this method can be invoked with element value value is Collection<*> -> { - if(returnType.isList()){ + if (returnType.isList()) { val arrayNode = jsonNodeFactory.arrayNode(value.size) value.forEach { element -> arrayNode.add(createNode(ctx, element, node, returnType.unwrapList())) } arrayNode @@ -153,7 +157,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { } } - private fun createSimpleValueNode(returnType: Type, value: T) : JsonNode { + private fun createSimpleValueNode(returnType: Type, value: T): JsonNode { val unwrapped = returnType.unwrapped() return when (unwrapped) { is Type.Scalar<*> -> { @@ -175,10 +179,10 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { } } - private suspend fun createObjectNode(ctx: ExecutionContext, value : T, node: Execution.Node, type: Type): ObjectNode { + private suspend fun createObjectNode(ctx: ExecutionContext, value: T, node: Execution.Node, type: Type): ObjectNode { val objectNode = jsonNodeFactory.objectNode() - for(child in node.children){ - if(child is Execution.Fragment){ + for (child in node.children) { + if (child is Execution.Fragment) { objectNode.setAll(handleFragment(ctx, value, child)) } else { val (key, jsonNode) = handleProperty(ctx, value, child, type) @@ -190,11 +194,11 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { private suspend fun handleProperty(ctx: ExecutionContext, value: T, child: Execution, type: Type): Pair { when (child) { - //Union is subclass of Node so check it first + //Union is subclass of Node so check it first is Execution.Union -> { val field = type.unwrapped()[child.key] ?: throw IllegalStateException("Execution unit ${child.key} is not contained by operation return type") - if(field is Field.Union<*>){ + if (field is Field.Union<*>) { return child.aliasOrKey to createUnionOperationNode(ctx, value, child, field as Field.Union) } else { throw ExecutionException("Unexpected non-union field for union execution node") @@ -215,10 +219,10 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { val expectedType = container.condition.type val include = determineInclude(ctx, container.directives) - if(include){ - if(expectedType.kind == TypeKind.OBJECT || expectedType.kind == TypeKind.INTERFACE){ - if(expectedType.isInstance(value)){ - return container.elements.map { handleProperty(ctx, value, it, expectedType)}.toMap() + if (include) { + if (expectedType.kind == TypeKind.OBJECT || expectedType.kind == TypeKind.INTERFACE) { + if (expectedType.isInstance(value)) { + return container.elements.map { handleProperty(ctx, value, it, expectedType) }.toMap() } } else { throw IllegalStateException("fragments can be specified on object types, interfaces, and unions") @@ -228,18 +232,18 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { return emptyMap() } - private suspend fun createPropertyNode(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field) : JsonNode? { + private suspend fun createPropertyNode(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field): JsonNode? { val include = determineInclude(ctx, node.directives) node.field.checkAccess(parentValue, ctx.requestContext) - if(include){ - when(field){ - is Field.Kotlin<*,*> -> { + if (include) { + when (field) { + is Field.Kotlin<*, *> -> { field.kProperty as KProperty1 val rawValue = field.kProperty.get(parentValue) - val value : Any? - value = if(field.transformation != null){ - field.transformation.invoke ( + val value: Any? + value = if (field.transformation != null) { + field.transformation.invoke( funName = field.name, receiver = rawValue, inputValues = field.arguments, @@ -263,7 +267,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { } } - suspend fun handleFunctionProperty(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field.Function<*, *>) : JsonNode { + suspend fun handleFunctionProperty(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field.Function<*, *>): JsonNode { val result = field.invoke( funName = field.name, receiver = parentValue, @@ -282,7 +286,8 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { receiver = null, args = arguments, ctx = ctx - )?.include ?: throw ExecutionException("Illegal directive implementation returning null result") + )?.include + ?: throw ExecutionException("Illegal directive implementation returning null result") }?.reduce { acc, b -> acc && b } ?: true } @@ -296,7 +301,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { val transformedArgs = argumentsHandler.transformArguments(funName, inputValues, args, ctx.variables, ctx.requestContext) //exceptions are not caught on purpose to pass up business logic errors - return if(hasReceiver){ + return if (hasReceiver) { suspendInvoke(receiver, *transformedArgs.toTypedArray()) } else { suspendInvoke(*transformedArgs.toTypedArray())