0.4.3-SNAPSHOT: disallow unintentionally overriding command implementations

This commit is contained in:
Sergey Chernov 2023-07-21 11:59:23 +01:00
parent 40fe234070
commit 8aa4b6bc3d
6 changed files with 48 additions and 12 deletions

View File

@ -10,7 +10,7 @@ plugins {
} }
group = "net.sergeych" group = "net.sergeych"
version = "0.4.3-SNAPSHOT" version = "0.4.4-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()
@ -53,7 +53,7 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") 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:unikrypto:1.2.2-SNAPSHOT")
api("net.sergeych:mp_stools:1.3.2-SNAPSHOT") api("net.sergeych:mp_stools:1.3.2-SNAPSHOT")

View File

@ -25,10 +25,13 @@ class AdapterBuilder<S : WithAdapter, H : CommandHost<S>>(
} }
/** /**
* 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 <A, R> on(ca: CommandDescriptor<A, R>, block: suspend S.(A) -> R) { fun <A, R> on(ca: CommandDescriptor<A, R>,overwrite: Boolean=false, block: suspend S.(A) -> R) {
api.on(ca, block) api.on(ca, overwrite, block)
} }
val onCancelHandlers = mutableListOf<suspend () -> Unit>() val onCancelHandlers = mutableListOf<suspend () -> Unit>()

View File

@ -13,8 +13,8 @@ class CommandDescriptor<A, R>(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend operator fun invoke(adapter: Adapter<*>): R = adapter.invokeCommand(this,Unit as A) suspend operator fun invoke(adapter: Adapter<*>): R = adapter.invokeCommand(this,Unit as A)
operator fun <I: WithAdapter>invoke(commandHost: CommandHost<I>, block: suspend I.(A)->R) { operator fun <I: WithAdapter>invoke(commandHost: CommandHost<I>, overwrite: Boolean = false,block: suspend I.(A)->R) {
commandHost.on(this, block) commandHost.on(this, overwrite, block)
} }
} }

View File

@ -34,10 +34,16 @@ open class CommandHost<T: WithAdapter> {
private val handlers = mutableMapOf<String, suspend T.(ByteArray) -> ByteArray>() private val handlers = mutableMapOf<String, suspend T.(ByteArray) -> ByteArray>()
/** /**
* Provide implementation for a specific command in type-safe compile-time checked manner. the command * Provide implementation for a specific command in a type-safe compile-time checked manner.
* should be declared with [command] invocation. * 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 <A, R> on(ca: CommandDescriptor<A, R>, block: suspend T.(A) -> R) { fun <A, R> on(ca: CommandDescriptor<A, R>, 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 -> handlers[ca.name] = {args ->
val decodedArgs = BossDecoder.decodeFrom<A>(ca.ass, args) val decodedArgs = BossDecoder.decodeFrom<A>(ca.ass, args)
BossEncoder.encode(ca.rss, block(decodedArgs)) BossEncoder.encode(ca.rss, block(decodedArgs))

View File

@ -16,4 +16,11 @@ class InvalidFrameException(text: String): ParsecException(ExceptionsRegistry.in
class UnknownCodeException(code: String,message: String?): ParsecException(code, message) class UnknownCodeException(code: String,message: String?): ParsecException(code, message)
class UnknownException(message: String?): ParsecException(ExceptionsRegistry.unknownErrorCode, message) 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)

View File

@ -58,6 +58,7 @@ internal class AdapterTest {
val foo by command<String, String>() val foo by command<String, String>()
val ex by command<String,Unit>() val ex by command<String,Unit>()
val ex2 by command<String,Unit>() val ex2 by command<String,Unit>()
val nullableTest by command<Int,String?>()
} }
class ApiS2<T: WithAdapter> : CommandHost<T>() { class ApiS2<T: WithAdapter> : CommandHost<T>() {
// create command `foo` that takes a string argument and // create command `foo` that takes a string argument and
@ -88,6 +89,9 @@ internal class AdapterTest {
on(ApiS1.ex2) { on(ApiS1.ex2) {
throw IllegalArgumentException() throw IllegalArgumentException()
} }
on(ApiS1.nullableTest) {
if( it == 1 ) "one" else null
}
onCancel { onCancel {
b1Cancelled = true b1Cancelled = true
} }
@ -120,6 +124,9 @@ internal class AdapterTest {
assertEquals("%% loop-42foo %%", a1.invokeCommand(ApiS2<WithAdapter>().loopCall, "123")) assertEquals("%% loop-42foo %%", a1.invokeCommand(ApiS2<WithAdapter>().loopCall, "123"))
assertEquals("32142foo", a2.invokeCommand(ApiS1.foo, "321")) 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, "---")) assertEquals("---42foo", ApiS1.foo.invoke(a2, "---"))
val x = assertThrows<IllegalArgumentException> { ApiS1.ex.invoke(a2, "foobar") } val x = assertThrows<IllegalArgumentException> { ApiS1.ex.invoke(a2, "foobar") }
assertEquals("foobar", x.message) assertEquals("foobar", x.message)
@ -133,5 +140,18 @@ internal class AdapterTest {
assertTrue { b1Cancelled } assertTrue { b1Cancelled }
assertTrue { b2Cancelled } assertTrue { b2Cancelled }
} }
@Test
fun builderDupeErrorTest() = runTest {
assertThrows<DuplicateCommandDefinition> {
AdapterBuilder(ApiS1) {
newSession { TestSession("42") }
on(api.foo) {
it + buzz + "foo"
}
on(api.foo) {
it + buzz + "foo"
}
}
}
}
} }