From 24e595196b36d37296d5b196e99b8cc2fc790a72 Mon Sep 17 00:00:00 2001 From: Mervyn McCreight Date: Sun, 2 Feb 2025 20:52:31 +0100 Subject: [PATCH 1/3] Introduce general functionality to memoize functions --- gradle/libs.versions.toml | 2 +- kgraphql/build.gradle.kts | 2 +- .../apurebase/kgraphql/function/Memoize.kt | 15 +++++++ .../kgraphql/request/CachingDocumentParser.kt | 22 ++++------ .../kgraphql/function/MemoizeTest.kt | 43 +++++++++++++++++++ 5 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt create mode 100644 kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65413442..221d97f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } jackson-core-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } -caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.2.0" } +aedile = { module = "com.sksamuel.aedile:aedile-core", version = "2.0.3" } deferredJsonBuilder = { module = "com.apurebase:DeferredJsonBuilder", version = "1.0.0" } ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" } diff --git a/kgraphql/build.gradle.kts b/kgraphql/build.gradle.kts index c696f819..292746ab 100644 --- a/kgraphql/build.gradle.kts +++ b/kgraphql/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.jackson.core.databind) implementation(libs.jackson.module.kotlin) - implementation(libs.caffeine) + implementation(libs.aedile) implementation(libs.deferredJsonBuilder) testImplementation(libs.hamcrest) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt new file mode 100644 index 00000000..8e2d2fe9 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt @@ -0,0 +1,15 @@ +package com.apurebase.kgraphql.function + +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.asLoadingCache +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.EmptyCoroutineContext + +fun (suspend (X) -> Y).memoized(scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext), memorySize: Long): suspend (X) -> Y { + val cache = Caffeine + .newBuilder() + .maximumSize(memorySize) + .asLoadingCache(scope) { this(it) } + + return { cache.get(it) } +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt index 6ed7fdcb..aff95e1b 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt @@ -1,16 +1,11 @@ package com.apurebase.kgraphql.request import com.apurebase.kgraphql.schema.model.ast.DocumentNode +import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine class CachingDocumentParser(cacheMaximumSize: Long = 1000L) { - - sealed class Result { - class Success(val document: DocumentNode) : Result() - class Exception(val exception: kotlin.Exception) : Result() - } - - val cache = Caffeine.newBuilder().maximumSize(cacheMaximumSize).build() + private val cache: Cache = Caffeine.newBuilder().maximumSize(cacheMaximumSize).build() fun parseDocument(input: String): DocumentNode { val result = cache.get(input) { @@ -22,13 +17,14 @@ class CachingDocumentParser(cacheMaximumSize: Long = 1000L) { } } - when (result) { - is Result.Success -> return result.document + return when (result) { + is Result.Success -> result.document is Result.Exception -> throw result.exception - else -> { - cache.invalidateAll() - error("Internal error of CachingDocumentParser") - } } } + + private sealed class Result { + class Success(val document: DocumentNode) : Result() + class Exception(val exception: kotlin.Exception) : Result() + } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt new file mode 100644 index 00000000..5192b303 --- /dev/null +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt @@ -0,0 +1,43 @@ +package com.apurebase.kgraphql.function + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.milliseconds + +@OptIn(ExperimentalCoroutinesApi::class) +class MemoizeTest { + private val slowFunctionDuration = 1000.milliseconds + + @Test + fun `calls function on invocation`() = runTest { + assertEquals(2, ::slowPlusOne.memoized(this,1).invoke(1)) + assertEquals(slowFunctionDuration.inWholeMilliseconds, testScheduler.currentTime) + } + + @Test + fun `calls function once on multiple invocations with same input`() = runTest { + val memoized = ::slowPlusOne.memoized(this, 2) + + repeat(2) { assertEquals(2, memoized(1)) } + assertEquals(slowFunctionDuration.inWholeMilliseconds, testScheduler.currentTime) + } + + @Test + fun `different memoized instances do not share their memory`() = runTest { + val one = ::slowPlusOne.memoized(this, 2) + val two = ::slowPlusOne.memoized(this, 2) + + one(1) + two(2) + + assertEquals((slowFunctionDuration * 2).inWholeMilliseconds, testScheduler.currentTime) + } + + private suspend fun slowPlusOne(x: Int): Int { + delay(slowFunctionDuration) + return x + 1 + } +} From cc8036529061bd10f68756b65dc6fce3525108b9 Mon Sep 17 00:00:00 2001 From: Mervyn McCreight Date: Wed, 5 Feb 2025 16:10:07 +0100 Subject: [PATCH 2/3] Cache document parsing result --- kgraphql/build.gradle.kts | 6 ++++ .../kgraphql/RequestCachingBenchmark.kt | 2 +- .../apurebase/kgraphql/function/Memoize.kt | 5 ++-- .../kgraphql/request/CachingDocumentParser.kt | 30 ------------------- .../com/apurebase/kgraphql/request/Parser.kt | 7 +---- .../kgraphql/schema/DefaultSchema.kt | 23 ++++++++++++-- 6 files changed, 31 insertions(+), 42 deletions(-) delete mode 100644 kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt diff --git a/kgraphql/build.gradle.kts b/kgraphql/build.gradle.kts index 292746ab..12fc16aa 100644 --- a/kgraphql/build.gradle.kts +++ b/kgraphql/build.gradle.kts @@ -24,6 +24,12 @@ benchmark { jmhVersion = "1.37" } } + + configurations { + register("requestCachingBenchmark") { + include("com.apurebase.kgraphql.RequestCachingBenchmark") + } + } } dependencies { diff --git a/kgraphql/src/jvm/kotlin/com/apurebase/kgraphql/RequestCachingBenchmark.kt b/kgraphql/src/jvm/kotlin/com/apurebase/kgraphql/RequestCachingBenchmark.kt index 0327bf28..60a4aba1 100644 --- a/kgraphql/src/jvm/kotlin/com/apurebase/kgraphql/RequestCachingBenchmark.kt +++ b/kgraphql/src/jvm/kotlin/com/apurebase/kgraphql/RequestCachingBenchmark.kt @@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit @State(Scope.Benchmark) @Warmup(iterations = 10) @Measurement(iterations = 5) -@OutputTimeUnit(TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) open class RequestCachingBenchmark { @Param("true", "false") diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt index 8e2d2fe9..fbd6b1f8 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt @@ -3,13 +3,12 @@ package com.apurebase.kgraphql.function import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.asLoadingCache import kotlinx.coroutines.CoroutineScope -import kotlin.coroutines.EmptyCoroutineContext -fun (suspend (X) -> Y).memoized(scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext), memorySize: Long): suspend (X) -> Y { +internal fun (suspend (X) -> Y).memoized(scope: CoroutineScope, memorySize: Long): suspend (X) -> Y { val cache = Caffeine .newBuilder() .maximumSize(memorySize) .asLoadingCache(scope) { this(it) } - return { cache.get(it) } + return cache::get } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt deleted file mode 100644 index aff95e1b..00000000 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.apurebase.kgraphql.request - -import com.apurebase.kgraphql.schema.model.ast.DocumentNode -import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine - -class CachingDocumentParser(cacheMaximumSize: Long = 1000L) { - private val cache: Cache = Caffeine.newBuilder().maximumSize(cacheMaximumSize).build() - - fun parseDocument(input: String): DocumentNode { - val result = cache.get(input) { - val parser = Parser(input) - try { - Result.Success(parser.parseDocument()) - } catch (e: Exception) { - Result.Exception(e) - } - } - - return when (result) { - is Result.Success -> result.document - is Result.Exception -> throw result.exception - } - } - - private sealed class Result { - class Success(val document: DocumentNode) : Result() - class Exception(val exception: kotlin.Exception) : Result() - } -} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt index cf456dc3..b235d553 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt @@ -45,7 +45,7 @@ import com.apurebase.kgraphql.schema.model.ast.TypeNode import com.apurebase.kgraphql.schema.model.ast.ValueNode import com.apurebase.kgraphql.schema.model.ast.VariableDefinitionNode -open class Parser { +internal class Parser { private val options: Options private val lexer: Lexer @@ -56,11 +56,6 @@ open class Parser { constructor(source: String, options: Options? = null) : this(Source(source), options) - constructor(lexer: Lexer, options: Options? = null) { - this.options = options ?: Options() - this.lexer = lexer - } - /** * Converts a name lex token into a name parse node. */ diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt index caf99b98..7b28141e 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt @@ -3,6 +3,7 @@ package com.apurebase.kgraphql.schema import com.apurebase.kgraphql.Context import com.apurebase.kgraphql.ValidationException import com.apurebase.kgraphql.configuration.SchemaConfiguration +import com.apurebase.kgraphql.function.memoized import com.apurebase.kgraphql.request.Introspection import com.apurebase.kgraphql.request.Parser import com.apurebase.kgraphql.request.VariablesJson @@ -14,18 +15,21 @@ import com.apurebase.kgraphql.schema.execution.Executor.Parallel import com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor import com.apurebase.kgraphql.schema.execution.RequestExecutor import com.apurebase.kgraphql.schema.introspection.__Schema +import com.apurebase.kgraphql.schema.model.ast.DocumentNode import com.apurebase.kgraphql.schema.model.ast.NameNode import com.apurebase.kgraphql.schema.structure.LookupSchema import com.apurebase.kgraphql.schema.structure.RequestInterpreter import com.apurebase.kgraphql.schema.structure.SchemaModel import com.apurebase.kgraphql.schema.structure.Type +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope +import kotlin.coroutines.CoroutineContext import kotlin.reflect.KClass class DefaultSchema( override val configuration: SchemaConfiguration, internal val model: SchemaModel -) : Schema, __Schema by model, LookupSchema { +) : Schema, __Schema by model, LookupSchema, CoroutineScope { companion object { val OPERATION_NAME_PARAM = NameNode("operationName", null) @@ -40,6 +44,16 @@ class DefaultSchema( private val requestInterpreter: RequestInterpreter = RequestInterpreter(model) + private val parseRequest: (suspend (String) -> DocumentNode) = + if (configuration.useCachingDocumentParser) { + ::parseDocument.memoized( + this, + configuration.documentParserCacheMaximumSize + ) + } else { + ::parseDocument + } + override suspend fun execute( request: String, variables: String?, @@ -55,7 +69,7 @@ class DefaultSchema( ?.let { VariablesJson.Defined(configuration.objectMapper, variables) } ?: VariablesJson.Empty() - val document = Parser(request).parseDocument() + val document = parseRequest(request) val executor = options.executor?.let(this@DefaultSchema::getExecutor) ?: defaultRequestExecutor @@ -73,4 +87,9 @@ class DefaultSchema( override fun inputTypeByKClass(kClass: KClass<*>): Type? = model.inputTypes[kClass] override fun findTypeByName(name: String): Type? = model.allTypesByName[name] + + @Suppress("RedundantSuspendModifier") + private suspend fun parseDocument(input: String): DocumentNode = Parser(input).parseDocument() + + override val coroutineContext: CoroutineContext = configuration.coroutineDispatcher } From 7ee31e4468b04af95b56d6b0186272b53c9d70a3 Mon Sep 17 00:00:00 2001 From: Mervyn McCreight Date: Wed, 5 Feb 2025 17:21:47 +0100 Subject: [PATCH 3/3] Remove state from `Parser` class --- .../apurebase/kgraphql/function/Memoize.kt | 6 +- .../com/apurebase/kgraphql/request/Parser.kt | 261 +++++++++--------- .../kgraphql/schema/DefaultSchema.kt | 23 +- .../kgraphql/function/MemoizeTest.kt | 9 +- .../apurebase/kgraphql/request/ParserTest.kt | 22 +- 5 files changed, 157 insertions(+), 164 deletions(-) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt index fbd6b1f8..4e458907 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt @@ -4,11 +4,11 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.asLoadingCache import kotlinx.coroutines.CoroutineScope -internal fun (suspend (X) -> Y).memoized(scope: CoroutineScope, memorySize: Long): suspend (X) -> Y { +internal inline fun memoize(scope: CoroutineScope, memorySize: Long, crossinline f: suspend (X) -> Y): suspend (X) -> Y { val cache = Caffeine .newBuilder() .maximumSize(memorySize) - .asLoadingCache(scope) { this(it) } + .asLoadingCache(scope) { f(it) } - return cache::get + return { cache.get(it) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt index b235d553..d50f957c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt @@ -47,19 +47,15 @@ import com.apurebase.kgraphql.schema.model.ast.VariableDefinitionNode internal class Parser { private val options: Options - private val lexer: Lexer - constructor(source: Source, options: Options? = null) { + constructor(options: Options? = null) { this.options = options ?: Options() - lexer = Lexer(source) } - constructor(source: String, options: Options? = null) : this(Source(source), options) - /** * Converts a name lex token into a name parse node. */ - private fun parseName(): NameNode { + private fun Lexer.parseName(): NameNode { val token = expectToken(NAME) return NameNode( value = token.value!!, @@ -72,16 +68,17 @@ internal class Parser { /** * Document : Definition+ */ - fun parseDocument(): DocumentNode { + fun parseDocument(input: Source): DocumentNode { + val lexer = Lexer(input) val start = lexer.token return DocumentNode( - definitions = many( + definitions = lexer.many( SOF, - ::parseDefinition, + { lexer.parseDefinition() }, EOF ), - loc = loc(start) + loc = lexer.loc(start) ) } @@ -95,9 +92,9 @@ internal class Parser { * - OperationDefinition * - FragmentDefinition */ - private fun parseDefinition(): DefinitionNode { + private fun Lexer.parseDefinition(): DefinitionNode { return when { - peek(NAME) -> when (lexer.token.value) { + peek(NAME) -> when (token.value) { "query", "mutation", "subscription" -> parseOperationDefinition() "fragment" -> parseFragmentDefinition() "schema", "scalar", "type", "interface", "union", "enum", "input", "directive" -> parseTypeSystemDefinition() @@ -116,8 +113,8 @@ internal class Parser { * - SelectionSet * - OperationType Name? VariableDefinitions? Directives? SelectionSet */ - private fun parseOperationDefinition(): DefinitionNode.ExecutableDefinitionNode.OperationDefinitionNode { - val start = lexer.token + private fun Lexer.parseOperationDefinition(): DefinitionNode.ExecutableDefinitionNode.OperationDefinitionNode { + val start = token if (peek(BRACE_L)) { return DefinitionNode.ExecutableDefinitionNode.OperationDefinitionNode( operation = QUERY, @@ -146,7 +143,7 @@ internal class Parser { /** * OperationType : one of query mutation subscription */ - private fun parseOperationType(): OperationTypeNode { + private fun Lexer.parseOperationType(): OperationTypeNode { val operationToken = expectToken(NAME) return when (operationToken.value) { "query" -> QUERY @@ -159,17 +156,17 @@ internal class Parser { /** * VariableDefinitions : ( VariableDefinition+ ) */ - private fun parseVariableDefinitions() = optionalMany( + private fun Lexer.parseVariableDefinitions() = optionalMany( PAREN_L, - ::parseVariableDefinition, + { parseVariableDefinition() }, PAREN_R ) /** * VariableDefinition : Variable : Type DefaultValue? Directives{Const}? */ - private fun parseVariableDefinition(): VariableDefinitionNode { - val start = lexer.token + private fun Lexer.parseVariableDefinition(): VariableDefinitionNode { + val start = token return VariableDefinitionNode( variable = parseVariable(), type = expectToken(COLON).let { parseTypeReference() }, @@ -186,8 +183,8 @@ internal class Parser { /** * Variable : $ Name */ - private fun parseVariable(): ValueNode.VariableNode { - val start = lexer.token + private fun Lexer.parseVariable(): ValueNode.VariableNode { + val start = token expectToken(DOLLAR) return ValueNode.VariableNode( name = parseName(), @@ -198,8 +195,8 @@ internal class Parser { /** * SelectionSet : { Selection+ } */ - private fun parseSelectionSet(parent: SelectionNode?): SelectionSetNode { - val start = lexer.token + private fun Lexer.parseSelectionSet(parent: SelectionNode?): SelectionSetNode { + val start = token return SelectionSetNode( selections = many( BRACE_L, @@ -216,7 +213,7 @@ internal class Parser { * - FragmentSpread * - InlineFragment */ - private fun parseSelection(parent: SelectionNode?): SelectionNode { + private fun Lexer.parseSelection(parent: SelectionNode?): SelectionNode { return if (peek(SPREAD)) parseFragment(parent) else parseField(parent) } @@ -225,8 +222,8 @@ internal class Parser { * * Alias : Name : */ - private fun parseField(parent: SelectionNode?): SelectionNode.FieldNode { - val start = lexer.token + private fun Lexer.parseField(parent: SelectionNode?): SelectionNode.FieldNode { + val start = token val nameOrAlias = parseName() val alias: NameNode? @@ -256,16 +253,16 @@ internal class Parser { /** * Arguments{Const} : ( Argument[?Const]+ ) */ - private fun parseArguments(isConst: Boolean): MutableList { - val item = if (isConst) ::parseConstArgument else ::parseArgument + private fun Lexer.parseArguments(isConst: Boolean): MutableList { + val item = if (isConst) ({ parseConstArgument() }) else ({ parseArgument() }) return optionalMany(PAREN_L, item, PAREN_R) } /** * Argument{Const} : Name : Value[?Const] */ - private fun parseArgument(): ArgumentNode { - val start = lexer.token + private fun Lexer.parseArgument(): ArgumentNode { + val start = token val name = parseName() expectToken(COLON) @@ -276,8 +273,8 @@ internal class Parser { ) } - private fun parseConstArgument(): ArgumentNode { - val start = lexer.token + private fun Lexer.parseConstArgument(): ArgumentNode { + val start = token return ArgumentNode( name = parseName(), value = expectToken(COLON).let { parseValueLiteral(true) }, @@ -292,8 +289,8 @@ internal class Parser { * * InlineFragment : ... TypeCondition? Directives? SelectionSet */ - private fun parseFragment(parent: SelectionNode?): SelectionNode.FragmentNode { - val start = lexer.token + private fun Lexer.parseFragment(parent: SelectionNode?): SelectionNode.FragmentNode { + val start = token expectToken(SPREAD) val hasTypeCondition = expectOptionalKeyword("on") @@ -323,8 +320,8 @@ internal class Parser { * * TypeCondition : NamedType */ - private fun parseFragmentDefinition(): DefinitionNode.ExecutableDefinitionNode.FragmentDefinitionNode { - val start = lexer.token + private fun Lexer.parseFragmentDefinition(): DefinitionNode.ExecutableDefinitionNode.FragmentDefinitionNode { + val start = token expectKeyword("fragment") return DefinitionNode.ExecutableDefinitionNode.FragmentDefinitionNode( name = parseFragmentName(), @@ -338,8 +335,8 @@ internal class Parser { /** * FragmentName : Name but not `on` */ - private fun parseFragmentName(): NameNode { - if (lexer.token.value == "on") { + private fun Lexer.parseFragmentName(): NameNode { + if (token.value == "on") { throw unexpected() } return parseName() @@ -363,14 +360,14 @@ internal class Parser { * * EnumValue : Name but not `true`, `false` or `null` */ - fun parseValueLiteral(isConst: Boolean): ValueNode { - val token = lexer.token + internal fun Lexer.parseValueLiteral(isConst: Boolean): ValueNode { + val token = token return when (token.kind) { BRACKET_L -> parseList(isConst) BRACE_L -> parseObject(isConst) INT -> { - lexer.advance() + advance() ValueNode.NumberValueNode( value = token.value!!, loc = loc(token) @@ -378,7 +375,7 @@ internal class Parser { } FLOAT -> { - lexer.advance() + advance() ValueNode.DoubleValueNode( value = token.value!!, loc = loc(token) @@ -388,16 +385,16 @@ internal class Parser { STRING, BLOCK_STRING -> parseStringLiteral() NAME -> { if (token.value == "true" || token.value == "false") { - lexer.advance() + advance() ValueNode.BooleanValueNode( value = token.value == "true", loc = loc(token) ) } else if (token.value == "null") { - lexer.advance() + advance() ValueNode.NullValueNode(loc(token)) } else { - lexer.advance() + advance() ValueNode.EnumValueNode( value = token.value!!, loc = loc(token) @@ -410,9 +407,9 @@ internal class Parser { } } - private fun parseStringLiteral(): ValueNode.StringValueNode { - val token = lexer.token - lexer.advance() + private fun Lexer.parseStringLiteral(): ValueNode.StringValueNode { + val token = token + advance() return ValueNode.StringValueNode( value = token.value!!, block = token.kind == BLOCK_STRING, @@ -425,8 +422,8 @@ internal class Parser { * - [ ] * - [ Value[?Const]+ ] */ - private fun parseList(isConst: Boolean): ValueNode.ListValueNode { - val start = lexer.token + private fun Lexer.parseList(isConst: Boolean): ValueNode.ListValueNode { + val start = token val item = { parseValueLiteral(isConst) } return ValueNode.ListValueNode( @@ -440,8 +437,8 @@ internal class Parser { * - { } * - { ObjectField[?Const]+ } */ - private fun parseObject(isConst: Boolean): ValueNode.ObjectValueNode { - val start = lexer.token + private fun Lexer.parseObject(isConst: Boolean): ValueNode.ObjectValueNode { + val start = token val item = { parseObjectField(isConst) } return ValueNode.ObjectValueNode( fields = any(BRACE_L, item, BRACE_R), @@ -452,8 +449,8 @@ internal class Parser { /** * ObjectField{Const} : Name : Value[?Const] */ - private fun parseObjectField(isConst: Boolean): ValueNode.ObjectValueNode.ObjectFieldNode { - val start = lexer.token + private fun Lexer.parseObjectField(isConst: Boolean): ValueNode.ObjectValueNode.ObjectFieldNode { + val start = token val name = parseName() expectToken(COLON) @@ -467,7 +464,7 @@ internal class Parser { /** * Directives{Const} : Directive[?Const]+ */ - private fun parseDirectives(isConst: Boolean): MutableList { + private fun Lexer.parseDirectives(isConst: Boolean): MutableList { val directives = mutableListOf() while (peek(AT)) { directives.add(parseDirective(isConst)) @@ -478,8 +475,8 @@ internal class Parser { /** * Directive{Const} : @ Name Arguments[?Const]? */ - private fun parseDirective(isConst: Boolean): DirectiveNode { - val start = lexer.token + private fun Lexer.parseDirective(isConst: Boolean): DirectiveNode { + val start = token expectToken(AT) return DirectiveNode( name = parseName(), @@ -494,8 +491,8 @@ internal class Parser { * - ListType * - NonNullType */ - internal fun parseTypeReference(): TypeNode { - val start = lexer.token + internal fun Lexer.parseTypeReference(): TypeNode { + val start = token var type: TypeNode? if (expectOptionalToken(BRACKET_L) != null) { type = parseTypeReference() @@ -520,8 +517,8 @@ internal class Parser { /** * NamedType : Name */ - private fun parseNamedType(): TypeNode.NamedTypeNode { - val start = lexer.token + private fun Lexer.parseNamedType(): TypeNode.NamedTypeNode { + val start = token return TypeNode.NamedTypeNode( name = parseName(), loc = loc(start) @@ -542,12 +539,12 @@ internal class Parser { * - EnumTypeDefinition * - InputObjectTypeDefinition */ - private fun parseTypeSystemDefinition(): DefinitionNode.TypeSystemDefinitionNode { + private fun Lexer.parseTypeSystemDefinition(): DefinitionNode.TypeSystemDefinitionNode { // Many definitions begin with a description and require a lookahead. val keywordToken = if (peekDescription()) { - lexer.lookahead() + lookahead() } else { - lexer.token + token } if (keywordToken.kind == NAME) { @@ -566,12 +563,12 @@ internal class Parser { throw unexpected(keywordToken) } - private fun peekDescription() = peek(STRING) || peek(BLOCK_STRING) + private fun Lexer.peekDescription() = peek(STRING) || peek(BLOCK_STRING) /** * Description : StringValue */ - private fun parseDescription() = if (peekDescription()) { + private fun Lexer.parseDescription() = if (peekDescription()) { parseStringLiteral() } else { null @@ -580,13 +577,13 @@ internal class Parser { /** * SchemaDefinition : schema Directives{Const}? { OperationTypeDefinition+ } */ - private fun parseSchemaDefinition(): DefinitionNode.TypeSystemDefinitionNode.SchemaDefinitionNode { - val start = lexer.token + private fun Lexer.parseSchemaDefinition(): DefinitionNode.TypeSystemDefinitionNode.SchemaDefinitionNode { + val start = token expectKeyword("schema") val directives = parseDirectives(true) val operationTypes = many( BRACE_L, - ::parseOperationTypeDefinition, + { parseOperationTypeDefinition() }, BRACE_R ) return DefinitionNode.TypeSystemDefinitionNode.SchemaDefinitionNode( @@ -599,8 +596,8 @@ internal class Parser { /** * OperationTypeDefinition : OperationType : NamedType */ - private fun parseOperationTypeDefinition(): OperationTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseOperationTypeDefinition(): OperationTypeDefinitionNode { + val start = token val operation = parseOperationType() expectToken(COLON) val type = parseNamedType() @@ -614,8 +611,8 @@ internal class Parser { /** * ScalarTypeDefinition : Description? scalar Name Directives{Const}? */ - private fun parseScalarTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.ScalarTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseScalarTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.ScalarTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("scalar") val name = parseName() @@ -633,8 +630,8 @@ internal class Parser { * Description? * type Name ImplementsInterfaces? Directives{Const}? FieldsDefinition? */ - private fun parseObjectTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.ObjectTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseObjectTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.ObjectTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("type") val name = parseName() @@ -656,7 +653,7 @@ internal class Parser { * - implements `&`? NamedType * - ImplementsInterfaces & NamedType */ - private fun parseImplementsInterfaces(): MutableList { + private fun Lexer.parseImplementsInterfaces(): MutableList { val types = mutableListOf() if (expectOptionalKeyword("implements")) { // Optional leading ampersand @@ -666,7 +663,7 @@ internal class Parser { } while ( expectOptionalToken(AMP) != null || // Legacy support for the SDL? - (options.allowLegacySDLImplementsInterfaces && peek(NAME)) + (this@Parser.options.allowLegacySDLImplementsInterfaces && peek(NAME)) ) } return types @@ -675,20 +672,20 @@ internal class Parser { /** * FieldsDefinition : { FieldDefinition+ } */ - private fun parseFieldsDefinition(): MutableList { + private fun Lexer.parseFieldsDefinition(): MutableList { // Legacy support for the SDL? if ( - options.allowLegacySDLEmptyFields && + this@Parser.options.allowLegacySDLEmptyFields && peek(BRACE_L) && - lexer.lookahead().kind == BRACE_R + lookahead().kind == BRACE_R ) { - lexer.advance() - lexer.advance() + advance() + advance() return mutableListOf() } return optionalMany( BRACE_L, - ::parseFieldDefinition, + { parseFieldDefinition() }, BRACE_R ) } @@ -697,8 +694,8 @@ internal class Parser { * FieldDefinition : * - Description? Name ArgumentsDefinition? : Type Directives{Const}? */ - private fun parseFieldDefinition(): FieldDefinitionNode { - val start = lexer.token + private fun Lexer.parseFieldDefinition(): FieldDefinitionNode { + val start = token val description = parseDescription() val name = parseName() val args = parseArgumentDefs() @@ -719,9 +716,9 @@ internal class Parser { /** * ArgumentsDefinition : ( InputValueDefinition+ ) */ - private fun parseArgumentDefs() = optionalMany( + private fun Lexer.parseArgumentDefs() = optionalMany( PAREN_L, - ::parseInputValueDef, + { parseInputValueDef() }, PAREN_R ) @@ -729,8 +726,8 @@ internal class Parser { * InputValueDefinition : * - Description? Name ArgumentsDefinition? : Type DefaultValue? Directives{Const}? */ - private fun parseInputValueDef(): InputValueDefinitionNode { - val start = lexer.token + private fun Lexer.parseInputValueDef(): InputValueDefinitionNode { + val start = token val description = parseDescription() val name = parseName() val args = parseArgumentDefs() @@ -754,8 +751,8 @@ internal class Parser { * InterfaceTypeDefinition : * - Description? interface Name Directives{Const}? FieldsDefinition? */ - private fun parseInterfaceTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.InterfaceTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseInterfaceTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.InterfaceTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("interface") val name = parseName() @@ -775,8 +772,8 @@ internal class Parser { * UnionTypeDefinition : * - Description? union Name Directives{Const}? UnionMemberTypes? */ - private fun parseUnionTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.UnionTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseUnionTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.UnionTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("union") val name = parseName() @@ -797,7 +794,7 @@ internal class Parser { * - = `|`? NamedType * - UnionMemberTypes | NamedType */ - private fun parseUnionMemberTypes(): MutableList { + private fun Lexer.parseUnionMemberTypes(): MutableList { val types = mutableListOf() if (expectOptionalToken(EQUALS) != null) { // Optional leading pipe @@ -813,8 +810,8 @@ internal class Parser { * EnumTypeDefinition : * - Description? enum Name Directives{Const}? EnumValuesDefinition? */ - private fun parseEnumTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.EnumTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseEnumTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.EnumTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("enum") val name = parseName() @@ -832,9 +829,9 @@ internal class Parser { /** * EnumValuesDefinition : { EnumValueDefinition+ } */ - private fun parseEnumValuesDefinition() = optionalMany( + private fun Lexer.parseEnumValuesDefinition() = optionalMany( BRACE_L, - ::parseEnumValueDefinition, + { parseEnumValueDefinition() }, BRACE_R ) @@ -843,8 +840,8 @@ internal class Parser { * * EnumValue : Name */ - private fun parseEnumValueDefinition(): EnumValueDefinitionNode { - val start = lexer.token + private fun Lexer.parseEnumValueDefinition(): EnumValueDefinitionNode { + val start = token val description = parseDescription() val name = parseName() val directives = parseDirectives(true) @@ -861,8 +858,8 @@ internal class Parser { * InputObjectTypeDefinition : * - Description? input Name Directives{Const}? InputFieldsDefinition? */ - private fun parseInputObjectTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.InputObjectTypeDefinitionNode { - val start = lexer.token + private fun Lexer.parseInputObjectTypeDefinition(): DefinitionNode.TypeSystemDefinitionNode.TypeDefinitionNode.InputObjectTypeDefinitionNode { + val start = token val description = parseDescription() expectKeyword("input") val name = parseName() @@ -881,9 +878,9 @@ internal class Parser { /** * InputFieldsDefinition : { InputValueDefinition+ } */ - private fun parseInputFieldsDefinition() = optionalMany( + private fun Lexer.parseInputFieldsDefinition() = optionalMany( BRACE_L, - ::parseInputValueDef, + { parseInputValueDef() }, BRACE_R ) @@ -891,8 +888,8 @@ internal class Parser { * DirectiveDefinition : * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations */ - private fun parseDirectiveDefinition(): DefinitionNode.TypeSystemDefinitionNode.DirectiveDefinitionNode { - val start = lexer.token + private fun Lexer.parseDirectiveDefinition(): DefinitionNode.TypeSystemDefinitionNode.DirectiveDefinitionNode { + val start = token val description = parseDescription() expectKeyword("directive") expectToken(AT) @@ -917,7 +914,7 @@ internal class Parser { * - `|`? DirectiveLocation * - DirectiveLocations | DirectiveLocation */ - private fun parseDirectiveLocations(): MutableList { + private fun Lexer.parseDirectiveLocations(): MutableList { // Optional leading pipe expectOptionalToken(PIPE) val locations = mutableListOf() @@ -955,8 +952,8 @@ internal class Parser { * `INPUT_OBJECT` * `INPUT_FIELD_DEFINITION` */ - private fun parseDirectiveLocation(): NameNode { - val start = lexer.token + private fun Lexer.parseDirectiveLocation(): NameNode { + val start = token val name = parseName() if (DirectiveLocation.from(name.value) != null) { return name @@ -968,9 +965,9 @@ internal class Parser { * Returns a location object, used to identify the place in * the source that created a given parsed object. */ - private fun loc(startToken: Token): Location? { - if (options.noLocation != true) { - return Location(startToken, lexer.lastToken, lexer.source) + private fun Lexer.loc(startToken: Token): Location? { + if (this@Parser.options.noLocation != true) { + return Location(startToken, lastToken, source) } return null } @@ -978,21 +975,21 @@ internal class Parser { /** * Determines if the next token is of a given kind */ - private fun peek(kind: TokenKindEnum) = lexer.token.kind == kind + private fun Lexer.peek(kind: TokenKindEnum) = token.kind == kind /** * If the next token is of the given kind, return that token after advancing * the lexer. Otherwise, do not change the parser state and throw an error. */ - internal fun expectToken(kind: TokenKindEnum): Token { - val token = lexer.token + internal fun Lexer.expectToken(kind: TokenKindEnum): Token { + val token = token if (token.kind == kind) { - lexer.advance() + advance() return token } throw syntaxError( - lexer.source, + source, token.start, "Expected ${getTokenKindDesc(kind)}, found ${ getTokenDesc( @@ -1006,10 +1003,10 @@ internal class Parser { * If the next token is of the given kind, return that token after advancing * the lexer. Otherwise, do not change the parser state and return undefined. */ - private fun expectOptionalToken(kind: TokenKindEnum): Token? { - val token = lexer.token + private fun Lexer.expectOptionalToken(kind: TokenKindEnum): Token? { + val token = token if (token.kind == kind) { - lexer.advance() + advance() return token } return null @@ -1019,13 +1016,13 @@ internal class Parser { * If the next token is a given keyword, advance the lexer. * Otherwise, do not change the parser state and throw an error. */ - private fun expectKeyword(value: String) { - val token = lexer.token + private fun Lexer.expectKeyword(value: String) { + val token = token if (token.kind == NAME && token.value == value) { - lexer.advance() + advance() } else { throw syntaxError( - lexer.source, + source, token.start, "Expected \"${value}\", found ${getTokenDesc(token)}." ) @@ -1036,10 +1033,10 @@ internal class Parser { * If the next token is a given keyword, return "true" after advancing * the lexer. Otherwise, do not change the parser state and return "false". */ - private fun expectOptionalKeyword(value: String): Boolean { - val token = lexer.token + private fun Lexer.expectOptionalKeyword(value: String): Boolean { + val token = token if (token.kind == NAME && token.value == value) { - lexer.advance() + advance() return true } return false @@ -1049,8 +1046,8 @@ internal class Parser { * Helper function for creating an error when an unexpected lexed token * is encountered. */ - private fun unexpected(token: Token = lexer.token) = syntaxError( - lexer.source, + private fun Lexer.unexpected(token: Token = this.token) = syntaxError( + source, token.start, "Unexpected ${getTokenDesc(token)}." ) @@ -1061,7 +1058,7 @@ internal class Parser { * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. */ - private fun any( + private fun Lexer.any( openKind: TokenKindEnum, parseFn: () -> T, closeKind: TokenKindEnum @@ -1081,7 +1078,7 @@ internal class Parser { * with a lex token of closeKind. Advances the parser to the next lex token * after the closing token. */ - private fun optionalMany( + private fun Lexer.optionalMany( openKind: TokenKindEnum, parseFn: () -> T, closeKind: TokenKindEnum @@ -1102,7 +1099,7 @@ internal class Parser { * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. */ - private fun many( + private fun Lexer.many( openKind: TokenKindEnum, parseFn: () -> T, closeKind: TokenKindEnum diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt index 7b28141e..1fee9106 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt @@ -3,7 +3,7 @@ package com.apurebase.kgraphql.schema import com.apurebase.kgraphql.Context import com.apurebase.kgraphql.ValidationException import com.apurebase.kgraphql.configuration.SchemaConfiguration -import com.apurebase.kgraphql.function.memoized +import com.apurebase.kgraphql.function.memoize import com.apurebase.kgraphql.request.Introspection import com.apurebase.kgraphql.request.Parser import com.apurebase.kgraphql.request.VariablesJson @@ -17,6 +17,7 @@ import com.apurebase.kgraphql.schema.execution.RequestExecutor import com.apurebase.kgraphql.schema.introspection.__Schema import com.apurebase.kgraphql.schema.model.ast.DocumentNode import com.apurebase.kgraphql.schema.model.ast.NameNode +import com.apurebase.kgraphql.schema.model.ast.Source import com.apurebase.kgraphql.schema.structure.LookupSchema import com.apurebase.kgraphql.schema.structure.RequestInterpreter import com.apurebase.kgraphql.schema.structure.SchemaModel @@ -43,16 +44,13 @@ class DefaultSchema( } private val requestInterpreter: RequestInterpreter = RequestInterpreter(model) + private val parser = Parser() - private val parseRequest: (suspend (String) -> DocumentNode) = - if (configuration.useCachingDocumentParser) { - ::parseDocument.memoized( - this, - configuration.documentParserCacheMaximumSize - ) - } else { - ::parseDocument - } + private val parse: suspend (String) -> DocumentNode = if (configuration.useCachingDocumentParser) { + memoize(this, configuration.documentParserCacheMaximumSize) { parser.parseDocument(Source(it)) } + } else { + { parser.parseDocument(Source(it)) } + } override suspend fun execute( request: String, @@ -69,7 +67,7 @@ class DefaultSchema( ?.let { VariablesJson.Defined(configuration.objectMapper, variables) } ?: VariablesJson.Empty() - val document = parseRequest(request) + val document = parse(request) val executor = options.executor?.let(this@DefaultSchema::getExecutor) ?: defaultRequestExecutor @@ -88,8 +86,5 @@ class DefaultSchema( override fun findTypeByName(name: String): Type? = model.allTypesByName[name] - @Suppress("RedundantSuspendModifier") - private suspend fun parseDocument(input: String): DocumentNode = Parser(input).parseDocument() - override val coroutineContext: CoroutineContext = configuration.coroutineDispatcher } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt index 5192b303..32531721 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt @@ -13,22 +13,21 @@ class MemoizeTest { @Test fun `calls function on invocation`() = runTest { - assertEquals(2, ::slowPlusOne.memoized(this,1).invoke(1)) + assertEquals(2, memoize(this, 1, ::slowPlusOne)(1)) assertEquals(slowFunctionDuration.inWholeMilliseconds, testScheduler.currentTime) } @Test fun `calls function once on multiple invocations with same input`() = runTest { - val memoized = ::slowPlusOne.memoized(this, 2) - + val memoized = memoize(this, 1, ::slowPlusOne) repeat(2) { assertEquals(2, memoized(1)) } assertEquals(slowFunctionDuration.inWholeMilliseconds, testScheduler.currentTime) } @Test fun `different memoized instances do not share their memory`() = runTest { - val one = ::slowPlusOne.memoized(this, 2) - val two = ::slowPlusOne.memoized(this, 2) + val one = memoize(this, 2, ::slowPlusOne) + val two = memoize(this, 2, ::slowPlusOne) one(1) two(2) diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt index f2b7e5f4..e98b275e 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt @@ -28,23 +28,25 @@ import org.junit.jupiter.api.Test class ParserTest { - private fun parse(source: String, options: Parser.Options? = null) = Parser(source, options).parseDocument() + private fun parse(source: String, options: Parser.Options? = null) = Parser(options).parseDocument(Source(source)) - private fun parse(source: Source) = Parser(source).parseDocument() + private fun parse(source: Source) = Parser().parseDocument(source) private fun parseValue(source: String): ValueNode { - val parser = Parser(source) - parser.expectToken(SOF) - val value = parser.parseValueLiteral(false) - parser.expectToken(EOF) + val parser = Parser() + val lexer = Lexer(Source(source)) + with(parser) { lexer.expectToken(SOF) } + val value = with(parser) { lexer.parseValueLiteral(false) } + with(parser) { lexer.expectToken(EOF) } return value } private fun parseType(source: String): TypeNode { - val parser = Parser(source) - parser.expectToken(SOF) - val type = parser.parseTypeReference() - parser.expectToken(EOF) + val parser = Parser() + val lexer = Lexer(Source(source)) + with(parser) { lexer.expectToken(SOF) } + val type = with(parser) { lexer.parseTypeReference() } + with(parser) { lexer.expectToken(EOF) } return type }