simplified adapter/command logic, adapeter builder introduced

This commit is contained in:
Sergey Chernov 2022-09-07 20:23:33 +03:00
parent 32280ffc61
commit 0cfe95d4f4
9 changed files with 161 additions and 32 deletions

View File

@ -4,6 +4,7 @@ val kotlin_version: String by project
plugins { plugins {
kotlin("multiplatform") version "1.7.10" kotlin("multiplatform") version "1.7.10"
kotlin("plugin.serialization") version "1.7.10" kotlin("plugin.serialization") version "1.7.10"
id("org.jetbrains.dokka") version "1.7.10"
`maven-publish` `maven-publish`
} }

View File

@ -1,4 +1,4 @@
package channel package net.sergeych.parsec3
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -7,15 +7,11 @@ import kotlinx.serialization.json.Json
import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization.BossDecoder
import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.boss_serialization_mp.decodeBoss import net.sergeych.boss_serialization_mp.decodeBoss
import net.sergeych.cloudoc.api.Package
import net.sergeych.mp_logger.LogTag import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.debug import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.warning import net.sergeych.mp_logger.warning
import net.sergeych.mptools.toDump import net.sergeych.mptools.toDump
import net.sergeych.parsec3.ExceptionsRegistry
import net.sergeych.parsec3.InvalidFrameException
import net.sergeych.parsec3.ParsecException
/** /**
* Create adapter, an interface to provide local API commands and invoke remote API commands * Create adapter, an interface to provide local API commands and invoke remote API commands
@ -82,7 +78,6 @@ open class Adapter<T>(
private var lastId = 1 private var lastId = 1
private val access = Mutex() private val access = Mutex()
/** /**
* Call the remote party for a type command. See [CommandHost] on how to declare and implement * Call the remote party for a type command. See [CommandHost] on how to declare and implement
* such commands in parsec3. Suspends until receiving answer from a remote party. * such commands in parsec3. Suspends until receiving answer from a remote party.
@ -93,7 +88,7 @@ open class Adapter<T>(
* @return value from remote partm any serializable type. * @return value from remote partm any serializable type.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend fun <A, R> invokeCommand(ca: CommandDescriptor<T, A, R>, args: A = Unit as A): R { suspend fun <A, R> invokeCommand(ca: CommandDescriptor<A, R>, args: A = Unit as A): R {
var myId = -1 var myId = -1
return CompletableDeferred<ByteArray>().also { dr -> return CompletableDeferred<ByteArray>().also { dr ->
sendPackage( sendPackage(

View File

@ -0,0 +1,40 @@
package net.sergeych.parsec3
import kotlinx.coroutines.flow.Flow
import net.sergeych.mp_tools.globalLaunch
class AdapterBuilder<S, H : CommandHost<S>>(
val api: H,
private val exceptionRegistry: ExceptionsRegistry = ExceptionsRegistry(),
f: AdapterBuilder<S, H>.() -> Unit,
) {
internal var sessionProducer: (suspend () -> S)? = null
private set
fun newSession(f: suspend () -> S) {
sessionProducer = f
}
/**
* Register command implementation
*/
fun <A, R> on(ca: CommandDescriptor<A, R>, block: suspend S.(A) -> R) {
api.on(ca, block)
}
fun <T : Throwable> addError(code: String, handler: (String?) -> T) {
exceptionRegistry.register(code, handler)
}
suspend fun createWith(input: Flow<ByteArray>, f: suspend (ByteArray)->Unit ): Adapter<S> {
return Adapter<S>(sessionProducer!!(), api, exceptionRegistry) { f(it) }
.also { a-> globalLaunch { input.collect { a.receiveFrame(it)} } }
}
init {
f(this)
}
}

View File

@ -1,6 +1,5 @@
package net.sergeych.parsec3 package net.sergeych.parsec3
import channel.CommandDescriptor
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
@ -13,7 +12,7 @@ class AdapterDelegate<I, A, R>(
val ass: KType, val ass: KType,
val rss: KType, val rss: KType,
) { ) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): CommandDescriptor<I, A, R> { operator fun getValue(thisRef: Any?, property: KProperty<*>): CommandDescriptor<A, R> {
return CommandDescriptor( return CommandDescriptor(
overrideName ?: property.name, overrideName ?: property.name,
ass, rss ass, rss

View File

@ -1,19 +1,19 @@
package channel package net.sergeych.parsec3
import kotlin.reflect.KType import kotlin.reflect.KType
class CommandDescriptor<I, A, R>( class CommandDescriptor<A, R>(
val name: String, val name: String,
val ass: KType, val ass: KType,
val rss: KType, val rss: KType,
) { ) {
suspend operator fun invoke(adapter: Adapter<I>, args: A): R = suspend operator fun invoke(adapter: Adapter<*>, args: A): R =
adapter.invokeCommand(this, args) adapter.invokeCommand(this, args)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend operator fun invoke(adapter: Adapter<I>): R = adapter.invokeCommand(this,Unit as A) suspend operator fun invoke(adapter: Adapter<*>): R = adapter.invokeCommand(this,Unit as A)
operator fun invoke(commandHost: CommandHost<I>, block: suspend I.(A)->R) { operator fun <I>invoke(commandHost: CommandHost<I>, block: suspend I.(A)->R) {
commandHost.on(this, block) commandHost.on(this, block)
} }
} }

View File

@ -1,9 +1,7 @@
package channel package net.sergeych.parsec3
import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization.BossDecoder
import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.parsec3.AdapterDelegate
import net.sergeych.parsec3.CommandNotFoundException
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/** /**
@ -39,7 +37,7 @@ open class CommandHost<T> {
* Provide implementation for a specific command in type-safe compile-time checked manner. the command * Provide implementation for a specific command in type-safe compile-time checked manner. the command
* should be declared with [command] invocation. * should be declared with [command] invocation.
*/ */
fun <A, R> on(ca: CommandDescriptor<T, A, R>, block: suspend T.(A) -> R) { fun <A, R> on(ca: CommandDescriptor<A, R>, block: suspend T.(A) -> R) {
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

@ -1,13 +1,32 @@
package net.sergeych.cloudoc.api package net.sergeych.parsec3
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* The parsec3 package transmit requests and responses over the parsec3 channel.
*/
@Serializable @Serializable
sealed class Package { sealed class Package {
/**
* Invoke a remote command.
* @param id command is a monotonously growing number, that could reset to 0 after getting close
* to `Int.MAX_VALUE`, for example. It uniquely identifies a command that is waiting for an answer.
* @param name command's name
* @param args whatever arguments the command accepts serialized with BOSS.
*/
@Serializable @Serializable
data class Command(val id: Int, val name: String, val args: ByteArray) : Package() data class Command(val id: Int, val name: String, val args: ByteArray) : Package()
/**
* Response to a previously issued command. See [ExceptionsRegistry] and [ParsecException] for more information
* on passing errors.
*
* @param tiId if of the command this response is for
* @param result packed result. If null, it means the command has thrown an exception and [errorCode] must
* not be null
* @param errorCode exception code, if not null then result must be ignored (and assumed to be null).
*/
@Serializable @Serializable
data class Response( data class Response(
val toId: Int, val toId: Int,

View File

@ -1,32 +1,34 @@
package parsec3 package parsec3
import channel.Adapter import assertThrows
import channel.CommandHost
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.parsec3.*
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
internal class AdapterTest { internal class AdapterTest {
class Api1: CommandHost<Unit>() { object Api1 : CommandHost<Unit>() {
// create command `foo` that takes a string argument and // create command `foo` that takes a string argument and
// returns a string: // returns a string:
val foo by command<String,String>() val foo by command<String, String>()
} }
class Api2: CommandHost<Unit>() {
val bar by command<String,String>() object Api2 : CommandHost<Unit>() {
val bar by command<String, String>()
} }
@Test @Test
fun interconnect() = runTest { fun interconnect() = runTest {
val ch12 = Channel<ByteArray>() val ch12 = Channel<ByteArray>()
val ch21 = Channel<ByteArray>() val ch21 = Channel<ByteArray>()
val api1 = Api1() val api1 = Api1
val api2 = Api2() val api2 = Api2
api1.on(api1.foo) { api1.on(api1.foo) {
it + "foo" it + "foo"
} }
@ -34,10 +36,10 @@ internal class AdapterTest {
it + "bar" it + "bar"
} }
val a1 = Adapter(Unit,api1) { ch12.send(it) } val a1 = Adapter(Unit, api1) { ch12.send(it) }
val a2 = Adapter(Unit,api2) { ch21.send(it) } val a2 = Adapter(Unit, api2) { ch21.send(it) }
launch { for( b in ch12) a2.receiveFrame(b) } launch { for (b in ch12) a2.receiveFrame(b) }
launch { for( b in ch21) a1.receiveFrame(b) } launch { for (b in ch21) a1.receiveFrame(b) }
assertEquals("123bar", a1.invokeCommand(api2.bar, "123")) assertEquals("123bar", a1.invokeCommand(api2.bar, "123"))
assertEquals("321foo", a2.invokeCommand(api1.foo, "321")) assertEquals("321foo", a2.invokeCommand(api1.foo, "321"))
@ -46,4 +48,58 @@ internal class AdapterTest {
ch21.cancel() ch21.cancel()
} }
data class TestSession(var buzz: String)
object ApiS1 : CommandHost<TestSession>() {
// create command `foo` that takes a string argument and
// returns a string:
val foo by command<String, String>()
val ex by command<String,Unit>()
}
class ApiS2<T> : CommandHost<T>() {
// create command `foo` that takes a string argument and
// returns a string:
val bar by command<String, String>()
}
@Test
fun builderTest() = runTest {
val ch12 = Channel<ByteArray>()
val ch21 = Channel<ByteArray>()
val er = ExceptionsRegistry().also {
it.register("foo_x") { IllegalArgumentException("foo_x") }
}
val b1 = AdapterBuilder(ApiS1, er) {
newSession { TestSession("42") }
on(api.foo) {
it + buzz + "foo"
}
on(ApiS1.ex) {
throw ParsecException("foo_x")
}
}
val b2 = AdapterBuilder(ApiS2<Unit>(), er) {
newSession { }
on(api.bar) {
it + "bar"
}
}
val a1 = b1.createWith(ch21.receiveAsFlow()) { ch12.send(it) }
val a2 = b2.createWith(ch12.receiveAsFlow()) { ch21.send(it) }
assertEquals("123bar", a1.invokeCommand(ApiS2<Unit>().bar, "123"))
assertEquals("32142foo", a2.invokeCommand(ApiS1.foo, "321"))
assertEquals("---42foo", ApiS1.foo.invoke(a2, "---"))
assertThrows<IllegalArgumentException> { ApiS1.ex.invoke(a2, "foobar") }
ch12.cancel()
ch21.cancel()
}
} }

View File

@ -0,0 +1,21 @@
import kotlin.test.fail
inline fun <reified T : Throwable> assertThrows(f: () -> Unit): T {
try {
f()
fail("expected to throw ${T::class.simpleName} but threw nothing")
} catch (e: Throwable) {
if (e !is T) {
println("unexpected error class: ${e::class.simpleName}")
e.printStackTrace()
fail("expected to throw ${T::class.simpleName} instead ${e::class.simpleName} was thrown: $e")
}
return e
}
}
//fun assertThrowsCode(code: String, f: () -> Unit): Unit {
// val err = assertThrows<ServiceError> { f() }
// if( err.code != code )
// Assert.fail("expected error code '$code' got ${err.code}")
//}