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..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 { @@ -34,7 +40,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/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 new file mode 100644 index 00000000..4e458907 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/function/Memoize.kt @@ -0,0 +1,14 @@ +package com.apurebase.kgraphql.function + +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.asLoadingCache +import kotlinx.coroutines.CoroutineScope + +internal inline fun memoize(scope: CoroutineScope, memorySize: Long, crossinline f: suspend (X) -> Y): suspend (X) -> Y { + val cache = Caffeine + .newBuilder() + .maximumSize(memorySize) + .asLoadingCache(scope) { f(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 deleted file mode 100644 index 6ed7fdcb..00000000 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/CachingDocumentParser.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.apurebase.kgraphql.request - -import com.apurebase.kgraphql.schema.model.ast.DocumentNode -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() - - 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) - } - } - - when (result) { - is Result.Success -> return result.document - is Result.Exception -> throw result.exception - else -> { - cache.invalidateAll() - error("Internal error of CachingDocumentParser") - } - } - } -} 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..d50f957c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt @@ -45,26 +45,17 @@ 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 - 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) - - constructor(lexer: Lexer, options: Options? = null) { - this.options = options ?: Options() - this.lexer = lexer } /** * 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!!, @@ -77,16 +68,17 @@ open 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) ) } @@ -100,9 +92,9 @@ open 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() @@ -121,8 +113,8 @@ open 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, @@ -151,7 +143,7 @@ open 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 @@ -164,17 +156,17 @@ open 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() }, @@ -191,8 +183,8 @@ open 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(), @@ -203,8 +195,8 @@ open 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, @@ -221,7 +213,7 @@ open 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) } @@ -230,8 +222,8 @@ open 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? @@ -261,16 +253,16 @@ open 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) @@ -281,8 +273,8 @@ open 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) }, @@ -297,8 +289,8 @@ open 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") @@ -328,8 +320,8 @@ open 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(), @@ -343,8 +335,8 @@ open 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() @@ -368,14 +360,14 @@ open 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) @@ -383,7 +375,7 @@ open class Parser { } FLOAT -> { - lexer.advance() + advance() ValueNode.DoubleValueNode( value = token.value!!, loc = loc(token) @@ -393,16 +385,16 @@ open 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) @@ -415,9 +407,9 @@ open 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, @@ -430,8 +422,8 @@ open 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( @@ -445,8 +437,8 @@ open 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), @@ -457,8 +449,8 @@ open 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) @@ -472,7 +464,7 @@ open 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)) @@ -483,8 +475,8 @@ open 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(), @@ -499,8 +491,8 @@ open 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() @@ -525,8 +517,8 @@ open 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) @@ -547,12 +539,12 @@ open 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) { @@ -571,12 +563,12 @@ open 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 @@ -585,13 +577,13 @@ open 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( @@ -604,8 +596,8 @@ open 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() @@ -619,8 +611,8 @@ open 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() @@ -638,8 +630,8 @@ open 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() @@ -661,7 +653,7 @@ open class Parser { * - implements `&`? NamedType * - ImplementsInterfaces & NamedType */ - private fun parseImplementsInterfaces(): MutableList { + private fun Lexer.parseImplementsInterfaces(): MutableList { val types = mutableListOf() if (expectOptionalKeyword("implements")) { // Optional leading ampersand @@ -671,7 +663,7 @@ open class Parser { } while ( expectOptionalToken(AMP) != null || // Legacy support for the SDL? - (options.allowLegacySDLImplementsInterfaces && peek(NAME)) + (this@Parser.options.allowLegacySDLImplementsInterfaces && peek(NAME)) ) } return types @@ -680,20 +672,20 @@ open 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 ) } @@ -702,8 +694,8 @@ open 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() @@ -724,9 +716,9 @@ open class Parser { /** * ArgumentsDefinition : ( InputValueDefinition+ ) */ - private fun parseArgumentDefs() = optionalMany( + private fun Lexer.parseArgumentDefs() = optionalMany( PAREN_L, - ::parseInputValueDef, + { parseInputValueDef() }, PAREN_R ) @@ -734,8 +726,8 @@ open 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() @@ -759,8 +751,8 @@ open 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() @@ -780,8 +772,8 @@ open 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() @@ -802,7 +794,7 @@ open class Parser { * - = `|`? NamedType * - UnionMemberTypes | NamedType */ - private fun parseUnionMemberTypes(): MutableList { + private fun Lexer.parseUnionMemberTypes(): MutableList { val types = mutableListOf() if (expectOptionalToken(EQUALS) != null) { // Optional leading pipe @@ -818,8 +810,8 @@ open 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() @@ -837,9 +829,9 @@ open class Parser { /** * EnumValuesDefinition : { EnumValueDefinition+ } */ - private fun parseEnumValuesDefinition() = optionalMany( + private fun Lexer.parseEnumValuesDefinition() = optionalMany( BRACE_L, - ::parseEnumValueDefinition, + { parseEnumValueDefinition() }, BRACE_R ) @@ -848,8 +840,8 @@ open 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) @@ -866,8 +858,8 @@ open 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() @@ -886,9 +878,9 @@ open class Parser { /** * InputFieldsDefinition : { InputValueDefinition+ } */ - private fun parseInputFieldsDefinition() = optionalMany( + private fun Lexer.parseInputFieldsDefinition() = optionalMany( BRACE_L, - ::parseInputValueDef, + { parseInputValueDef() }, BRACE_R ) @@ -896,8 +888,8 @@ open 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) @@ -922,7 +914,7 @@ open class Parser { * - `|`? DirectiveLocation * - DirectiveLocations | DirectiveLocation */ - private fun parseDirectiveLocations(): MutableList { + private fun Lexer.parseDirectiveLocations(): MutableList { // Optional leading pipe expectOptionalToken(PIPE) val locations = mutableListOf() @@ -960,8 +952,8 @@ open 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 @@ -973,9 +965,9 @@ open 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 } @@ -983,21 +975,21 @@ open 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( @@ -1011,10 +1003,10 @@ open 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 @@ -1024,13 +1016,13 @@ open 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)}." ) @@ -1041,10 +1033,10 @@ open 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 @@ -1054,8 +1046,8 @@ open 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)}." ) @@ -1066,7 +1058,7 @@ open 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 @@ -1086,7 +1078,7 @@ open 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 @@ -1107,7 +1099,7 @@ open 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 caf99b98..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,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.memoize import com.apurebase.kgraphql.request.Introspection import com.apurebase.kgraphql.request.Parser import com.apurebase.kgraphql.request.VariablesJson @@ -14,18 +15,22 @@ 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.model.ast.Source 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) @@ -39,6 +44,13 @@ class DefaultSchema( } private val requestInterpreter: RequestInterpreter = RequestInterpreter(model) + private val parser = Parser() + + 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, @@ -55,7 +67,7 @@ class DefaultSchema( ?.let { VariablesJson.Defined(configuration.objectMapper, variables) } ?: VariablesJson.Empty() - val document = Parser(request).parseDocument() + val document = parse(request) val executor = options.executor?.let(this@DefaultSchema::getExecutor) ?: defaultRequestExecutor @@ -73,4 +85,6 @@ class DefaultSchema( override fun inputTypeByKClass(kClass: KClass<*>): Type? = model.inputTypes[kClass] override fun findTypeByName(name: String): Type? = model.allTypesByName[name] + + 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 new file mode 100644 index 00000000..32531721 --- /dev/null +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/function/MemoizeTest.kt @@ -0,0 +1,42 @@ +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, memoize(this, 1, ::slowPlusOne)(1)) + assertEquals(slowFunctionDuration.inWholeMilliseconds, testScheduler.currentTime) + } + + @Test + fun `calls function once on multiple invocations with same input`() = runTest { + 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 = memoize(this, 2, ::slowPlusOne) + val two = memoize(this, 2, ::slowPlusOne) + + one(1) + two(2) + + assertEquals((slowFunctionDuration * 2).inWholeMilliseconds, testScheduler.currentTime) + } + + private suspend fun slowPlusOne(x: Int): Int { + delay(slowFunctionDuration) + return x + 1 + } +} 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 }