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 {
|
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`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
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
|
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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
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