From 8aa4b6bc3d0bd7140881de0e78af6909d089d6ca Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 21 Jul 2023 11:59:23 +0100 Subject: [PATCH] 0.4.3-SNAPSHOT: disallow unintentionally overriding command implementations --- build.gradle.kts | 4 ++-- .../net.sergeych.parsec3/AdapterBuilder.kt | 9 +++++--- .../net.sergeych.parsec3/CommandDescriptor.kt | 4 ++-- .../net.sergeych.parsec3/CommandHost.kt | 12 +++++++--- .../net.sergeych.parsec3/ParsecException.kt | 9 +++++++- src/commonTest/kotlin/parsec3/AdapterTest.kt | 22 ++++++++++++++++++- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2e8829e..cc24cea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "net.sergeych" -version = "0.4.3-SNAPSHOT" +version = "0.4.4-SNAPSHOT" repositories { mavenCentral() @@ -53,7 +53,7 @@ kotlin { api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") - api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT") + api("net.sergeych:boss-serialization-mp:0.2.7-SNAPSHOT") api("net.sergeych:unikrypto:1.2.2-SNAPSHOT") api("net.sergeych:mp_stools:1.3.2-SNAPSHOT") diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt b/src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt index 945eb27..104abde 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt @@ -25,10 +25,13 @@ class AdapterBuilder>( } /** - * Register command implementation + * Register command implementation. + * @param ca command to implement + * @param overwrite allow replacing existing command implementation with a new block + * @param block command handler that implements the command */ - fun on(ca: CommandDescriptor, block: suspend S.(A) -> R) { - api.on(ca, block) + fun on(ca: CommandDescriptor,overwrite: Boolean=false, block: suspend S.(A) -> R) { + api.on(ca, overwrite, block) } val onCancelHandlers = mutableListOf Unit>() diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt b/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt index d3c02fc..465a900 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/CommandDescriptor.kt @@ -13,8 +13,8 @@ class CommandDescriptor( @Suppress("UNCHECKED_CAST") suspend operator fun invoke(adapter: Adapter<*>): R = adapter.invokeCommand(this,Unit as A) - operator fun invoke(commandHost: CommandHost, block: suspend I.(A)->R) { - commandHost.on(this, block) + operator fun invoke(commandHost: CommandHost, overwrite: Boolean = false,block: suspend I.(A)->R) { + commandHost.on(this, overwrite, block) } } diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt index 87c7e4e..cb81c72 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt @@ -34,10 +34,16 @@ open class CommandHost { private val handlers = mutableMapOf ByteArray>() /** - * Provide implementation for a specific command in type-safe compile-time checked manner. the command - * should be declared with [command] invocation. + * Provide implementation for a specific command in a type-safe compile-time checked manner. + * The command should be declared with [command] invocation. + * + * Normally, existing command can't be replaced by new handlers and attempt to do so will throw + * [DuplicateCommandDefinition] at runtime. If it is done intentionally, set [overwrite] to + * true. */ - fun on(ca: CommandDescriptor, block: suspend T.(A) -> R) { + fun on(ca: CommandDescriptor, overwrite: Boolean = false, block: suspend T.(A) -> R) { + if( ca.name in handlers && !overwrite ) + throw DuplicateCommandDefinition("command already defined: ${ca.name}") handlers[ca.name] = {args -> val decodedArgs = BossDecoder.decodeFrom(ca.ass, args) BossEncoder.encode(ca.rss, block(decodedArgs)) diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt b/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt index ec68f37..0a28c0b 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt @@ -16,4 +16,11 @@ class InvalidFrameException(text: String): ParsecException(ExceptionsRegistry.in class UnknownCodeException(code: String,message: String?): ParsecException(code, message) -class UnknownException(message: String?): ParsecException(ExceptionsRegistry.unknownErrorCode, message) \ No newline at end of file +class UnknownException(message: String?): ParsecException(ExceptionsRegistry.unknownErrorCode, message) + +/** + * This exception is server-side, not intended to arise while processing commands, so it is not + * a parsec exception. It means some adapter is trying to redefine existing command, which is + * generally a fault. + */ +class DuplicateCommandDefinition(message: String) : RuntimeException(message) diff --git a/src/commonTest/kotlin/parsec3/AdapterTest.kt b/src/commonTest/kotlin/parsec3/AdapterTest.kt index 23c6d63..bc0c916 100644 --- a/src/commonTest/kotlin/parsec3/AdapterTest.kt +++ b/src/commonTest/kotlin/parsec3/AdapterTest.kt @@ -58,6 +58,7 @@ internal class AdapterTest { val foo by command() val ex by command() val ex2 by command() + val nullableTest by command() } class ApiS2 : CommandHost() { // create command `foo` that takes a string argument and @@ -88,6 +89,9 @@ internal class AdapterTest { on(ApiS1.ex2) { throw IllegalArgumentException() } + on(ApiS1.nullableTest) { + if( it == 1 ) "one" else null + } onCancel { b1Cancelled = true } @@ -120,6 +124,9 @@ internal class AdapterTest { assertEquals("%% loop-42foo %%", a1.invokeCommand(ApiS2().loopCall, "123")) assertEquals("32142foo", a2.invokeCommand(ApiS1.foo, "321")) + assertEquals("one", a2.invokeCommand(ApiS1.nullableTest,1)) + assertEquals(null, a2.invokeCommand(ApiS1.nullableTest,2)) + assertEquals("---42foo", ApiS1.foo.invoke(a2, "---")) val x = assertThrows { ApiS1.ex.invoke(a2, "foobar") } assertEquals("foobar", x.message) @@ -133,5 +140,18 @@ internal class AdapterTest { assertTrue { b1Cancelled } assertTrue { b2Cancelled } } - + @Test + fun builderDupeErrorTest() = runTest { + assertThrows { + AdapterBuilder(ApiS1) { + newSession { TestSession("42") } + on(api.foo) { + it + buzz + "foo" + } + on(api.foo) { + it + buzz + "foo" + } + } + } + } } \ No newline at end of file