simplified adapter/command logic, adapeter builder introduced
This commit is contained in:
parent
32280ffc61
commit
0cfe95d4f4
@ -4,6 +4,7 @@ val kotlin_version: String by project
|
||||
plugins {
|
||||
kotlin("multiplatform") version "1.7.10"
|
||||
kotlin("plugin.serialization") version "1.7.10"
|
||||
id("org.jetbrains.dokka") version "1.7.10"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package channel
|
||||
package net.sergeych.parsec3
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
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_mp.BossEncoder
|
||||
import net.sergeych.boss_serialization_mp.decodeBoss
|
||||
import net.sergeych.cloudoc.api.Package
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_logger.warning
|
||||
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
|
||||
@ -82,7 +78,6 @@ open class Adapter<T>(
|
||||
private var lastId = 1
|
||||
private val access = Mutex()
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -93,7 +88,7 @@ open class Adapter<T>(
|
||||
* @return value from remote partm any serializable type.
|
||||
*/
|
||||
@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
|
||||
return CompletableDeferred<ByteArray>().also { dr ->
|
||||
sendPackage(
|
||||
|
40
src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt
Normal file
40
src/commonMain/kotlin/net.sergeych.parsec3/AdapterBuilder.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.sergeych.parsec3
|
||||
|
||||
import channel.CommandDescriptor
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KType
|
||||
|
||||
@ -13,7 +12,7 @@ class AdapterDelegate<I, A, R>(
|
||||
val ass: 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(
|
||||
overrideName ?: property.name,
|
||||
ass, rss
|
||||
|
@ -1,19 +1,19 @@
|
||||
package channel
|
||||
package net.sergeych.parsec3
|
||||
|
||||
import kotlin.reflect.KType
|
||||
|
||||
class CommandDescriptor<I, A, R>(
|
||||
class CommandDescriptor<A, R>(
|
||||
val name: String,
|
||||
val ass: 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)
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
package channel
|
||||
package net.sergeych.parsec3
|
||||
|
||||
import net.sergeych.boss_serialization.BossDecoder
|
||||
import net.sergeych.boss_serialization_mp.BossEncoder
|
||||
import net.sergeych.parsec3.AdapterDelegate
|
||||
import net.sergeych.parsec3.CommandNotFoundException
|
||||
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
|
||||
* 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 ->
|
||||
val decodedArgs = BossDecoder.decodeFrom<A>(ca.ass, args)
|
||||
BossEncoder.encode(ca.rss, block(decodedArgs))
|
||||
|
@ -1,13 +1,32 @@
|
||||
package net.sergeych.cloudoc.api
|
||||
package net.sergeych.parsec3
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The parsec3 package transmit requests and responses over the parsec3 channel.
|
||||
*/
|
||||
@Serializable
|
||||
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
|
||||
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
|
||||
data class Response(
|
||||
val toId: Int,
|
||||
|
@ -1,32 +1,34 @@
|
||||
package parsec3
|
||||
|
||||
import channel.Adapter
|
||||
import channel.CommandHost
|
||||
import assertThrows
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.parsec3.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class AdapterTest {
|
||||
|
||||
class Api1: CommandHost<Unit>() {
|
||||
object Api1 : CommandHost<Unit>() {
|
||||
// create command `foo` that takes a string argument and
|
||||
// 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
|
||||
fun interconnect() = runTest {
|
||||
val ch12 = Channel<ByteArray>()
|
||||
val ch21 = Channel<ByteArray>()
|
||||
val api1 = Api1()
|
||||
val api2 = Api2()
|
||||
val api1 = Api1
|
||||
val api2 = Api2
|
||||
api1.on(api1.foo) {
|
||||
it + "foo"
|
||||
}
|
||||
@ -34,10 +36,10 @@ internal class AdapterTest {
|
||||
it + "bar"
|
||||
}
|
||||
|
||||
val a1 = Adapter(Unit,api1) { ch12.send(it) }
|
||||
val a2 = Adapter(Unit,api2) { ch21.send(it) }
|
||||
launch { for( b in ch12) a2.receiveFrame(b) }
|
||||
launch { for( b in ch21) a1.receiveFrame(b) }
|
||||
val a1 = Adapter(Unit, api1) { ch12.send(it) }
|
||||
val a2 = Adapter(Unit, api2) { ch21.send(it) }
|
||||
launch { for (b in ch12) a2.receiveFrame(b) }
|
||||
launch { for (b in ch21) a1.receiveFrame(b) }
|
||||
|
||||
assertEquals("123bar", a1.invokeCommand(api2.bar, "123"))
|
||||
assertEquals("321foo", a2.invokeCommand(api1.foo, "321"))
|
||||
@ -46,4 +48,58 @@ internal class AdapterTest {
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
21
src/commonTest/kotlin/parsec3/assertThrows.kt
Normal file
21
src/commonTest/kotlin/parsec3/assertThrows.kt
Normal 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}")
|
||||
//}
|
Loading…
Reference in New Issue
Block a user