diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index e3aa4aba3..0b43e1c03 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -31,7 +31,6 @@ dependencies { tasks { val examples = listOf( - "ZBytes", "ZDelete", "ZGet", "ZPub", diff --git a/examples/src/main/kotlin/io.zenoh/ZBytes.kt b/examples/src/main/kotlin/io.zenoh/ZBytes.kt deleted file mode 100644 index b5c844e17..000000000 --- a/examples/src/main/kotlin/io.zenoh/ZBytes.kt +++ /dev/null @@ -1,237 +0,0 @@ -package io.zenoh - -import io.zenoh.protocol.* -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.reflect.typeOf - -fun main() { - - /*********************************************** - * Standard serialization and deserialization. * - ***********************************************/ - - /** Numeric: byte, short, int, float, double */ - val intInput = 1234 - var payload = ZBytes.from(intInput) - var intOutput = payload.deserialize().getOrThrow() - check(intInput == intOutput) - - // Alternatively you can serialize into the type. - payload = ZBytes.serialize(intInput).getOrThrow() - intOutput = payload.deserialize().getOrThrow() - check(intInput == intOutput) - - // Alternatively, `Numeric.into()`: ZBytes can be used - payload = intInput.into() - intOutput = payload.deserialize().getOrThrow() - check(intInput == intOutput) - - // Another example with float - val floatInput = 3.1415f - payload = ZBytes.from(floatInput) - val floatOutput = payload.deserialize().getOrThrow() - check(floatInput == floatOutput) - - /** String serialization and deserialization. */ - val stringInput = "example" - payload = ZBytes.from(stringInput) - // Alternatively, you can also call `String.into()` to convert - // a string into a ZBytes object: - // payload = stringInput.into() - var stringOutput = payload.deserialize().getOrThrow() - check(stringInput == stringOutput) - - // For the case of strings, ZBytes::toString() is equivalent: - stringOutput = payload.toString() - check(stringInput == stringOutput) - - /** ByteArray serialization and deserialization. */ - val byteArrayInput = "example".toByteArray() - payload = ZBytes.from(byteArrayInput) // Equivalent to `byteArrayInput.into()` - var byteArrayOutput = payload.deserialize().getOrThrow() - check(byteArrayInput.contentEquals(byteArrayOutput)) - // Alternatively, we can directly access the bytes of property of ZBytes: - byteArrayOutput = payload.toByteArray() - check(byteArrayInput.contentEquals(byteArrayOutput)) - - /** List serialization and deserialization. - * - * Supported types: String, ByteArray, ZBytes, Byte, Short, Int, Long, Float and Double. - */ - val inputList = listOf("sample1", "sample2", "sample3") - payload = ZBytes.serialize(inputList).getOrThrow() - val outputList = payload.deserialize>().getOrThrow() - check(inputList == outputList) - - val inputListZBytes = inputList.map { value -> value.into() } - payload = ZBytes.serialize(inputListZBytes).getOrThrow() - val outputListZBytes = payload.deserialize>().getOrThrow() - check(inputListZBytes == outputListZBytes) - - val inputListByteArray = inputList.map { value -> value.toByteArray() } - payload = ZBytes.serialize(inputListByteArray).getOrThrow() - val outputListByteArray = payload.deserialize>().getOrThrow() - check(compareByteArrayLists(inputListByteArray, outputListByteArray)) - - /** - * Map serialization and deserialization. - * - * Maps with the following Type combinations are supported: String, ByteArray, ZBytes, Byte, Short, Int, Long, Float and Double. - */ - val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") - payload = ZBytes.serialize(inputMap).getOrThrow() - val outputMap = payload.deserialize>().getOrThrow() - check(inputMap == outputMap) - - val combinedInputMap = mapOf("key1" to ZBytes.from("zbytes1"), "key2" to ZBytes.from("zbytes2")) - payload = ZBytes.serialize(combinedInputMap).getOrThrow() - val combinedOutputMap = payload.deserialize>().getOrThrow() - check(combinedInputMap == combinedOutputMap) - - /********************************************* - * Custom serialization and deserialization. * - *********************************************/ - - /** - * The examples below use [MyZBytes], an example class consisting that implements the [Serializable] interface. - * - * In order for the serialization and deserialization to be successful on a custom class, - * the class itself must override the `into(): ZBytes` function, but also the companion - * object must implement the [Deserializable.From] interface. - * - * @see MyZBytes - */ - val inputMyZBytes = MyZBytes("example") - payload = ZBytes.serialize(inputMyZBytes).getOrThrow() - val outputMyZBytes = payload.deserialize().getOrThrow() - check(inputMyZBytes == outputMyZBytes) - - /** List of MyZBytes. */ - val inputListMyZBytes = inputList.map { value -> MyZBytes(value) } - payload = ZBytes.serialize>(inputListMyZBytes).getOrThrow() - val outputListMyZBytes = payload.deserialize>().getOrThrow() - check(inputListMyZBytes == outputListMyZBytes) - - /** Map of MyZBytes. */ - val inputMapMyZBytes = inputMap.map { (k, v) -> MyZBytes(k) to MyZBytes(v)}.toMap() - payload = ZBytes.serialize>(inputMapMyZBytes).getOrThrow() - val outputMapMyZBytes = payload.deserialize>().getOrThrow() - check(inputMapMyZBytes == outputMapMyZBytes) - - val combinedMap = mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2) - payload = ZBytes.serialize>(combinedMap).getOrThrow() - val combinedOutput = payload.deserialize>().getOrThrow() - check(combinedMap == combinedOutput) - - /** - * Providing a map of deserializers. - * - * Alternatively, [ZBytes.deserialize] also accepts a deserializers parameter of type - * `Map>`. That is, a map of types that is associated - * to a function receiving a ByteArray, that returns Any. This way, you can provide a series - * of deserializer functions that extend the deserialization mechanisms we provide by default. - * - * For example, let's say we have a custom map serializer, with its own deserializer: - */ - val fooMap = mapOf(Foo("foo1") to Foo("bar1"), Foo("foo2") to Foo("bar2")) - val fooMapSerialized = ZBytes.from(serializeFooMap(fooMap)) - val deserializersMap = mapOf(typeOf>() to ::deserializeFooMap) - val deserializedFooMap = fooMapSerialized.deserialize>(deserializersMap).getOrThrow() - check(fooMap == deserializedFooMap) -} - -class MyZBytes(val content: String) : Serializable, Deserializable { - - override fun into(): ZBytes = content.into() - - companion object : Deserializable.From { - override fun from(zbytes: ZBytes): MyZBytes { - return MyZBytes(zbytes.toString()) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MyZBytes - - return content == other.content - } - - override fun hashCode(): Int { - return content.hashCode() - } -} - -/** Example class for the deserialization map examples. */ -class Foo(val content: String) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Foo - - return content == other.content - } - - override fun hashCode(): Int { - return content.hashCode() - } -} - -/** Example serializer and deserializer. */ -private fun serializeFooMap(testMap: Map): ByteArray { - return testMap.map { - val key = it.key.content.toByteArray() - val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() - val value = it.value.content.toByteArray() - val valueLength = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() - keyLength + key + valueLength + value - }.reduce { acc, bytes -> acc + bytes } -} - -private fun deserializeFooMap(serializedMap: ZBytes): Map { - var idx = 0 - var sliceSize: Int - val bytes = serializedMap.toByteArray() - val decodedMap = mutableMapOf() - while (idx < bytes.size) { - sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) - .order(ByteOrder.LITTLE_ENDIAN).int - idx += Int.SIZE_BYTES - - val key = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( - ByteOrder.LITTLE_ENDIAN - ).int - idx += Int.SIZE_BYTES - - val value = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - decodedMap[Foo(key.decodeToString())] = Foo(value.decodeToString()) - } - return decodedMap -} - -/** Utils for this example. */ - -private fun compareByteArrayLists(list1: List, list2: List): Boolean { - if (list1.size != list2.size) { - return false - } - - for (i in list1.indices) { - if (!list1[i].contentEquals(list2[i])) { - return false - } - } - - return true -} diff --git a/examples/src/main/kotlin/io.zenoh/ZDelete.kt b/examples/src/main/kotlin/io.zenoh/ZDelete.kt index 384820b39..6d4f1712d 100644 --- a/examples/src/main/kotlin/io.zenoh/ZDelete.kt +++ b/examples/src/main/kotlin/io.zenoh/ZDelete.kt @@ -28,8 +28,10 @@ class ZDelete(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - println("Deleting resources matching '$keyExpr'...") - session.delete(keyExpr) + keyExpr.use { + println("Deleting resources matching '$keyExpr'...") + session.delete(keyExpr).res() + } } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZGet.kt b/examples/src/main/kotlin/io.zenoh/ZGet.kt index 91c26f4f1..6fce767f8 100644 --- a/examples/src/main/kotlin/io.zenoh/ZGet.kt +++ b/examples/src/main/kotlin/io.zenoh/ZGet.kt @@ -17,12 +17,10 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.long -import io.zenoh.protocol.into +import io.zenoh.query.ConsolidationMode import io.zenoh.query.QueryTarget import io.zenoh.query.Reply import io.zenoh.selector.intoSelector -import io.zenoh.value.Value -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import java.time.Duration @@ -31,28 +29,38 @@ class ZGet(private val emptyArgs: Boolean) : CliktCommand( ) { override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) Session.open(config).onSuccess { session -> session.use { selector.intoSelector().onSuccess { selector -> - session.get(selector, - channel = Channel(), - value = payload?.let { Value(it) }, - target = target?.let { QueryTarget.valueOf(it.uppercase()) } ?: QueryTarget.BEST_MATCHING, - attachment = attachment?.into(), - timeout = Duration.ofMillis(timeout)) - .onSuccess { channelReceiver -> - runBlocking { - for (reply in channelReceiver) { - when (reply) { - is Reply.Success -> println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')") - is Reply.Error -> println("Received (ERROR: '${reply.error}')") - is Reply.Delete -> println("Received (DELETE '${reply.keyExpr}')") - } + selector.use { + session.get(selector) + .timeout(Duration.ofMillis(timeout)) + .apply { + target?.let { + target(QueryTarget.valueOf(it.uppercase())) + } + attachment?.let { + withAttachment(it.toByteArray()) + } + value?.let { + withValue(it) } } + .res() + .onSuccess { receiver -> + runBlocking { + for (reply in receiver!!) { + when (reply) { + is Reply.Success -> println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')") + is Reply.Error -> println("Received (ERROR: '${reply.error}')") + is Reply.Delete -> println("Received (DELETE '${reply.keyExpr}')") + } + } + } } + } } } } @@ -64,8 +72,8 @@ class ZGet(private val emptyArgs: Boolean) : CliktCommand( help = "The selection of resources to query [default: demo/example/**]", metavar = "selector" ).default("demo/example/**") - private val payload by option( - "-p", "--payload", help = "An optional payload to put in the query.", metavar = "payload" + private val value by option( + "-v", "--value", help = "An optional value to put in the query.", metavar = "value" ) private val target by option( "-t", diff --git a/examples/src/main/kotlin/io.zenoh/ZPub.kt b/examples/src/main/kotlin/io.zenoh/ZPub.kt index ba62d6611..ac8becd62 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPub.kt @@ -17,7 +17,6 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.protocol.into class ZPub(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Pub example" @@ -29,23 +28,27 @@ class ZPub(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - println("Declaring publisher on '$keyExpr'...") - session.declarePublisher(keyExpr).onSuccess { pub -> - println("Press CTRL-C to quit...") - val attachment = attachment?.toByteArray() - var idx = 0 - while (true) { - Thread.sleep(1000) - val payload = "[${ - idx.toString().padStart(4, ' ') - }] $value" - println( - "Putting Data ('$keyExpr': '$payload')..." - ) - attachment?.let { - pub.put(payload, attachment = it.into()) - } ?: let { pub.put(payload) } - idx++ + keyExpr.use { + println("Declaring publisher on '$keyExpr'...") + session.declarePublisher(keyExpr).res().onSuccess { pub -> + pub.use { + println("Press CTRL-C to quit...") + val attachment = attachment?.toByteArray() + var idx = 0 + while (true) { + Thread.sleep(1000) + val payload = "[${ + idx.toString().padStart(4, ' ') + }] $value" + println( + "Putting Data ('$keyExpr': '$payload')..." + ) + attachment?.let { + pub.put(payload).withAttachment(attachment).res() + } ?: let { pub.put(payload).res() } + idx++ + } + } } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt index 81ef45956..9db3be0bf 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt @@ -25,7 +25,6 @@ import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.CongestionControl import io.zenoh.prelude.Encoding import io.zenoh.prelude.Priority -import io.zenoh.prelude.QoS import io.zenoh.value.Value class ZPubThr(private val emptyArgs: Boolean) : CliktCommand( @@ -39,35 +38,35 @@ class ZPubThr(private val emptyArgs: Boolean) : CliktCommand( } val value = Value(data, Encoding(Encoding.ID.ZENOH_BYTES)) - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) - - val qos = QoS( - congestionControl = CongestionControl.BLOCK, - priority = priorityInput?.let { Priority.entries[it] } ?: Priority.default(), - ) + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) Session.open(config).onSuccess { it.use { session -> - session.declarePublisher("test/thr".intoKeyExpr().getOrThrow(), qos = qos).onSuccess { pub -> - println("Publisher declared on test/thr.") - var count: Long = 0 - var start = System.currentTimeMillis() - val number = number.toLong() - println("Press CTRL-C to quit...") - while (true) { - pub.put(value).getOrThrow() - if (statsPrint) { - if (count < number) { - count++ - } else { - val throughput = count * 1000 / (System.currentTimeMillis() - start) - println("$throughput msgs/s") - count = 0 - start = System.currentTimeMillis() + session.declarePublisher("test/thr".intoKeyExpr().getOrThrow()) + .congestionControl(CongestionControl.BLOCK).apply { + priorityInput?.let { priority(Priority.entries[it]) } + }.res().onSuccess { pub -> + pub.use { + println("Publisher declared on test/thr.") + var count: Long = 0 + var start = System.currentTimeMillis() + val number = number.toLong() + println("Press CTRL-C to quit...") + while (true) { + pub.put(value).res().getOrThrow() + if (statsPrint) { + if (count < number) { + count++ + } else { + val throughput = count * 1000 / (System.currentTimeMillis() - start) + println("$throughput msgs/s") + count = 0 + start = System.currentTimeMillis() + } + } } } } - } } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZPut.kt b/examples/src/main/kotlin/io.zenoh/ZPut.kt index ae5492bf8..60841792d 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPut.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPut.kt @@ -17,21 +17,31 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.protocol.into +import io.zenoh.prelude.SampleKind +import io.zenoh.prelude.CongestionControl +import io.zenoh.prelude.Priority class ZPut(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Put example" ) { override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) println("Opening Session...") Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> keyExpr.use { - session.put(keyExpr, value, attachment = attachment?.into()) + session.put(keyExpr, value) + .congestionControl(CongestionControl.BLOCK) + .priority(Priority.REALTIME) + .apply { + attachment?.let { + withAttachment(it.toByteArray()) + } + } + .res() .onSuccess { println("Putting Data ('$keyExpr': '$value')...") } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZQueryable.kt b/examples/src/main/kotlin/io.zenoh/ZQueryable.kt index aabd85192..995015f1e 100644 --- a/examples/src/main/kotlin/io.zenoh/ZQueryable.kt +++ b/examples/src/main/kotlin/io.zenoh/ZQueryable.kt @@ -16,8 +16,10 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* +import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.value.Value +import io.zenoh.prelude.SampleKind +import io.zenoh.queryable.Query import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import org.apache.commons.net.ntp.TimeStamp @@ -32,17 +34,16 @@ class ZQueryable(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - println("Declaring Queryable on $key...") - session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> - runBlocking { - for (query in queryable.receiver) { - val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" - println(">> [Queryable] Received Query '${query.selector}' $valueInfo") - query.replySuccess( - keyExpr, - value = Value(value), - timestamp = TimeStamp.getCurrentTime() - ).onFailure { println(">> [Queryable ] Error sending reply: $it") } + keyExpr.use { + println("Declaring Queryable on $key...") + session.declareQueryable(keyExpr).res().onSuccess { queryable -> + queryable.use { + println("Press CTRL-C to quit...") + queryable.receiver?.let { receiverChannel -> // The default receiver is a Channel we can process on a coroutine. + runBlocking { + handleRequests(receiverChannel, keyExpr) + } + } } } } @@ -51,6 +52,17 @@ class ZQueryable(private val emptyArgs: Boolean) : CliktCommand( } } + private suspend fun handleRequests( + receiverChannel: Channel, keyExpr: KeyExpr + ) { + for (query in receiverChannel) { + val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" + println(">> [Queryable] Received Query '${query.selector}' $valueInfo") + query.reply(keyExpr).success(value).timestamp(TimeStamp.getCurrentTime()) + .res().onFailure { println(">> [Queryable ] Error sending reply: $it") } + } + } + private val key by option( "-k", "--key", diff --git a/examples/src/main/kotlin/io.zenoh/ZSub.kt b/examples/src/main/kotlin/io.zenoh/ZSub.kt index 5a7fd15d7..d9b20f1ad 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSub.kt @@ -17,7 +17,6 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking class ZSub(private val emptyArgs: Boolean) : CliktCommand( @@ -25,7 +24,7 @@ class ZSub(private val emptyArgs: Boolean) : CliktCommand( ) { override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) println("Opening session...") Session.open(config).onSuccess { session -> @@ -33,15 +32,17 @@ class ZSub(private val emptyArgs: Boolean) : CliktCommand( key.intoKeyExpr().onSuccess { keyExpr -> keyExpr.use { println("Declaring Subscriber on '$keyExpr'...") - - session.declareSubscriber(keyExpr, Channel()).onSuccess { subscriber -> - runBlocking { - for (sample in subscriber.receiver) { - println(">> [Subscriber] Received ${sample.kind} ('${sample.keyExpr}': '${sample.value}'" + "${ - sample.attachment?.let { - ", with attachment: $it" - } ?: "" - })") + session.declareSubscriber(keyExpr).bestEffort().res().onSuccess { subscriber -> + subscriber.use { + println("Press CTRL-C to quit...") + runBlocking { + for (sample in subscriber.receiver!!) { + println(">> [Subscriber] Received ${sample.kind} ('${sample.keyExpr}': '${sample.value}'" + "${ + sample.attachment?.let { + ", with attachment: " + it.decodeToString() + } ?: "" + })") + } } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt index f413d7333..f192e0010 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt @@ -18,7 +18,6 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.ulong import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.subscriber.Reliability import io.zenoh.subscriber.Subscriber import kotlin.system.exitProcess @@ -80,11 +79,7 @@ class ZSubThr(private val emptyArgs: Boolean) : CliktCommand( session.use { println("Press CTRL-C to quit...") subscriber = - session.declareSubscriber( - keyExpr, - callback = { listener(number) }, - reliability = Reliability.RELIABLE - ).getOrThrow() + session.declareSubscriber(keyExpr).reliable().with { listener(number) }.res().getOrThrow() while (subscriber.isValid()) {/* Keep alive the subscriber until the test is done. */ Thread.sleep(1000) } diff --git a/zenoh-jni/src/errors.rs b/zenoh-jni/src/errors.rs index a254c67f5..edca4ab89 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -28,7 +28,7 @@ macro_rules! throw_exception { #[macro_export] macro_rules! jni_error { ($arg:expr) => { - $crate::errors::Error::Jni($arg.to_string()) + Error::Jni($arg.to_string()) }; ($fmt:expr, $($arg:tt)*) => { Error::Jni(format!($fmt, $($arg)*)) diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index a982babe4..edfba47a4 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -21,7 +21,6 @@ mod queryable; mod session; mod subscriber; mod utils; -mod zbytes; // Test should be runned with `cargo test --no-default-features` #[test] diff --git a/zenoh-jni/src/logger.rs b/zenoh-jni/src/logger.rs index 85362ac47..780e693e9 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -17,7 +17,10 @@ use jni::{ JNIEnv, }; -use crate::{errors::Result, jni_error, throw_exception}; +use crate::{ + errors::{Error, Result}, + jni_error, throw_exception, +}; /// Redirects the Rust logs either to logcat for Android systems or to the standard output (for non-Android systems). /// diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 4246cb6a3..2391261c5 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -587,7 +587,8 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> R ) })?; - let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { + let (with_value, payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() + { let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. let encoding_id = encoding.id() as jint; let encoding_schema = encoding @@ -598,9 +599,10 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> R ) .map(|value| env.auto_local(value))?; let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; - (byte_array, encoding_id, encoding_schema) + (true, byte_array, encoding_id, encoding_schema) } else { ( + false, env.auto_local(JByteArray::default()), 0, env.auto_local(JString::default()), @@ -632,10 +634,11 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> R .call_method( callback_global_ref, "run", - "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJ)V", + "(Ljava/lang/String;Ljava/lang/String;Z[BILjava/lang/String;[BJ)V", &[ JValue::from(&key_expr_str), JValue::from(&selector_params_jstr), + JValue::from(with_value), JValue::from(&payload), JValue::from(encoding_id), JValue::from(&encoding_schema), @@ -760,7 +763,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( /// of using a non declared key expression, in which case the `key_expr_str` parameter will be used instead. /// - `key_expr_str`: String representation of the key expression to be used to declare the query. It is not /// considered if a `key_expr_ptr` is provided. -/// - `selector_params`: Optional parameters of the selector. +/// - `selector_params`: Parameters of the selector. /// - `session_ptr`: A raw pointer to the Zenoh [Session]. /// - `callback`: A Java/Kotlin callback to be called upon receiving a reply. /// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when no more replies will be received. @@ -768,9 +771,11 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( /// - `target`: The query target as the ordinal of the enum. /// - `consolidation`: The consolidation mode as the ordinal of the enum. /// - `attachment`: An optional attachment encoded into a byte array. -/// - `payload`: Optional payload for the query. -/// - `encoding_id`: The encoding of the payload. -/// - `encoding_schema`: The encoding schema of the payload, may be null. +/// - `with_value`: Boolean value to tell if a value must be included in the get operation. If true, +/// then the next params are valid. +/// - `payload`: The payload of the value. +/// - `encoding_id`: The encoding of the value payload. +/// - `encoding_schema`: The encoding schema of the value payload, may be null. /// /// Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. @@ -789,7 +794,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( _class: JClass, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - selector_params: /*nullable*/ JString, + selector_params: JString, session_ptr: *const Session, callback: JObject, on_close: JObject, @@ -797,6 +802,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( target: jint, consolidation: jint, attachment: /*nullable*/ JByteArray, + with_value: jboolean, payload: /*nullable*/ JByteArray, encoding_id: jint, encoding_schema: /*nullable*/ JString, @@ -809,14 +815,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; let query_target = decode_query_target(target)?; let consolidation = decode_consolidation(consolidation)?; + let selector_params = decode_string(&mut env, &selector_params)?; let timeout = Duration::from_millis(timeout_ms as u64); let on_close = load_on_close(&java_vm, on_close_global_ref); - let selector_params = if selector_params.is_null() { - String::new() - } else { - decode_string(&mut env, &selector_params)? - }; - let selector = Selector::owned(&key_expr, selector_params); + let selector = Selector::owned(&key_expr, &*selector_params); let mut get_builder = session .get(selector) .callback(move |reply| { @@ -848,7 +850,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( .timeout(timeout) .consolidation(consolidation); - if !payload.is_null() { + if with_value != 0 { let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; get_builder = get_builder.encoding(encoding); get_builder = get_builder.payload(decode_byte_array(&env, payload)?); diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs deleted file mode 100644 index da067f499..000000000 --- a/zenoh-jni/src/zbytes.rs +++ /dev/null @@ -1,197 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use std::collections::HashMap; - -use jni::{ - objects::{JByteArray, JClass, JList, JMap, JObject}, - sys::{jbyteArray, jobject}, - JNIEnv, -}; -use zenoh::bytes::ZBytes; - -use crate::{errors::Result, jni_error, session_error, utils::bytes_to_java_array}; -use crate::{throw_exception, utils::decode_byte_array}; - -/// -/// Map serialization and deserialization -/// - -/// Serializes a Map, returning the resulting ByteArray. -/// -/// # Parameters -/// - `env``: the JNI environment. -/// - `_class`: The java class. -/// - `map`: A Java bytearray map. -/// -/// # Returns: -/// - Returns the serialized map as a byte array. -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeIntoMapViaJNI( - mut env: JNIEnv, - _class: JClass, - map: JObject, -) -> jbyteArray { - || -> Result { - let zbytes = java_map_to_zbytes(&mut env, &map).map_err(|err| jni_error!(err))?; - let byte_array = bytes_to_java_array(&env, &zbytes)?; - Ok(byte_array.as_raw()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::null().as_raw() - }) -} - -fn java_map_to_zbytes(env: &mut JNIEnv, map: &JObject) -> jni::errors::Result { - let jmap = JMap::from_env(env, map)?; - let mut iterator = jmap.iter(env)?; - let mut rust_map: HashMap, Vec> = HashMap::new(); - while let Some((key, value)) = iterator.next(env)? { - let key_bytes = env.convert_byte_array(env.auto_local(JByteArray::from(key)))?; - let value_bytes = env.convert_byte_array(env.auto_local(JByteArray::from(value)))?; - rust_map.insert(key_bytes, value_bytes); - } - Ok(ZBytes::serialize(rust_map)) -} - -/// Deserializes a serialized bytearray map, returning the original map. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The Java class. -/// - `serialized_map`: The byte array resulting of the serialization of a bytearray map. -/// -/// # Returns -/// - The original byte array map before serialization. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeIntoMapViaJNI( - mut env: JNIEnv, - _class: JClass, - serialized_map: JByteArray, -) -> jobject { - || -> Result { - let payload = decode_byte_array(&env, serialized_map)?; - let zbytes = ZBytes::new(payload); - let deserialization: HashMap, Vec> = zbytes - .deserialize::, Vec>>() - .map_err(|err| session_error!(err))?; - hashmap_to_java_map(&mut env, &deserialization).map_err(|err| jni_error!(err)) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::null().as_raw() - }) -} - -fn hashmap_to_java_map( - env: &mut JNIEnv, - hashmap: &HashMap, Vec>, -) -> jni::errors::Result { - let map = env.new_object("java/util/HashMap", "()V", &[])?; - let jmap = JMap::from_env(env, &map)?; - - for (k, v) in hashmap.iter() { - let key = env.byte_array_from_slice(k.as_slice())?; - let value = env.byte_array_from_slice(v.as_slice())?; - jmap.put(env, &key, &value)?; - } - Ok(map.as_raw()) -} - -/// -/// List serialization and deserialization -/// - -/// Serializes a list of byte arrays, returning a byte array. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The Java class. -/// - `list`: The Java list of byte arrays to serialize. -/// -/// # Returns: -/// - The serialized list as a ByteArray. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeIntoListViaJNI( - mut env: JNIEnv, - _class: JClass, - list: JObject, -) -> jbyteArray { - || -> Result { - let zbytes = java_list_to_zbytes(&mut env, &list).map_err(|err| jni_error!(err))?; - let byte_array = bytes_to_java_array(&env, &zbytes)?; - Ok(byte_array.as_raw()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::null().as_raw() - }) -} - -fn java_list_to_zbytes(env: &mut JNIEnv, list: &JObject) -> jni::errors::Result { - let jmap = JList::from_env(env, list)?; - let mut iterator = jmap.iter(env)?; - let mut rust_vec: Vec> = Vec::new(); - while let Some(value) = iterator.next(env)? { - let value_bytes = env.auto_local(JByteArray::from(value)); - let value_bytes = env.convert_byte_array(value_bytes)?; - rust_vec.push(value_bytes); - } - let zbytes = ZBytes::from_iter(rust_vec); - Ok(zbytes) -} - -/// Deserializes a serialized list of byte arrrays, returning the original list. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The Java class. -/// - `serialized_list`: The byte array resulting of the serialization of a bytearray list. -/// -/// # Returns: -/// - The original list of byte arrays prior to serialization. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeIntoListViaJNI( - mut env: JNIEnv, - _class: JClass, - serialized_list: JByteArray, -) -> jobject { - || -> Result { - let payload = decode_byte_array(&env, serialized_list)?; - let zbytes = ZBytes::new(payload); - zbytes_to_java_list(&mut env, &zbytes).map_err(|err| jni_error!(err)) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::null().as_raw() - }) -} - -fn zbytes_to_java_list(env: &mut JNIEnv, zbytes: &ZBytes) -> jni::errors::Result { - let array_list = env.new_object("java/util/ArrayList", "()V", &[])?; - let jlist = JList::from_env(env, &array_list)?; - for value in zbytes.iter::>() { - let value = &mut env.byte_array_from_slice(value.unwrap().as_slice())?; //The unwrap is unfallible. - jlist.add(env, value)?; - } - Ok(array_list.as_raw()) -} diff --git a/zenoh-kotlin/build.gradle.kts b/zenoh-kotlin/build.gradle.kts index 40eaaef6e..16c49998c 100644 --- a/zenoh-kotlin/build.gradle.kts +++ b/zenoh-kotlin/build.gradle.kts @@ -60,15 +60,11 @@ kotlin { implementation("commons-net:commons-net:3.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - implementation("org.jetbrains.kotlin:kotlin-reflect") } } val commonTest by getting { dependencies { implementation(kotlin("test")) - implementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") - implementation("org.junit.jupiter:junit-jupiter-params:5.10.0") } } if (androidEnabled) { @@ -109,13 +105,13 @@ kotlin { } } } + tasks.withType { doFirst { // The line below is added for the Android Unit tests which are equivalent to the JVM tests. // For them to work we need to specify the path to the native library as a system property and not as a jvmArg. systemProperty("java.library.path", "../zenoh-jni/target/$buildMode") } - useJUnitPlatform() } tasks.whenObjectAdded { diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt new file mode 100644 index 000000000..decb9ae39 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt @@ -0,0 +1,23 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh + +/** + * A resolvable function interface meant to be used by Zenoh builders. + */ +fun interface Resolvable { + + fun res(): Result +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt index 4b89afaf0..4da978e57 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt @@ -16,12 +16,9 @@ package io.zenoh import io.zenoh.exceptions.SessionException import io.zenoh.handlers.Callback -import io.zenoh.handlers.ChannelHandler -import io.zenoh.handlers.Handler import io.zenoh.jni.JNISession import io.zenoh.keyexpr.KeyExpr import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes import io.zenoh.publication.Delete import io.zenoh.publication.Publisher import io.zenoh.publication.Put @@ -107,314 +104,137 @@ class Session private constructor(private val config: Config) : AutoCloseable { * Session.open().onSuccess { * it.use { session -> * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * session.declarePublisher(keyExpr).onSuccess { pub -> - * pub.use { - * println("Publisher declared on $keyExpr.") - * var i = 0 - * while (true) { - * val payload = "Hello for the ${i}th time!" - * println(payload) - * pub.put(payload) - * Thread.sleep(1000) - * i++ + * session.declarePublisher(keyExpr) + * .priority(Priority.REALTIME) + * .congestionControl(CongestionControl.DROP) + * .res().onSuccess { pub -> + * pub.use { + * println("Publisher declared on $keyExpr.") + * var i = 0 + * while (true) { + * val payload = "Hello for the ${i}th time!" + * println(payload) + * pub.put(payload).res() + * Thread.sleep(1000) + * i++ + * } * } * } - * } * } * } * } * ``` * * @param keyExpr The [KeyExpr] the publisher will be associated to. - * @param qos The [QoS] configuration of the publisher. - * @return The result of the declaration, returning the publisher in case of success. + * @return A resolvable [Publisher.Builder] */ - fun declarePublisher(keyExpr: KeyExpr, qos: QoS = QoS.default()): Result { - return resolvePublisher(keyExpr, qos) - } + fun declarePublisher(keyExpr: KeyExpr): Publisher.Builder = Publisher.Builder(this, keyExpr) /** - * Declare a [Subscriber] on the session, specifying a callback to handle incoming samples. - * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> - * session.declareSubscriber(keyExpr, callback = { sample -> println(sample) }).onSuccess { - * println("Declared subscriber on $keyExpr.") - * } - * } - * } - * } - * ``` + * Declare a [Subscriber] on the session. * - * @param keyExpr The [KeyExpr] the subscriber will be associated to. - * @param callback Callback to handle the received samples. - * @param onClose Callback function to be called when the subscriber is closed. - * @param reliability The reliability the subscriber wishes to obtain from the network. - * @return A result with the [Subscriber] in case of success. - */ - fun declareSubscriber( - keyExpr: KeyExpr, - callback: Callback, - onClose: (() -> Unit)? = null, - reliability: Reliability = Reliability.BEST_EFFORT - ): Result> { - val resolvedOnClose = fun() { - onClose?.invoke() - } - return resolveSubscriber(keyExpr, callback, resolvedOnClose, Unit, reliability) - } - - /** - * Declare a [Subscriber] on the session, specifying a handler to handle incoming samples. + * The default receiver is a [Channel], but can be changed with the [Subscriber.Builder.with] functions. * * Example: - * ```kotlin - * - * class ExampleHandler: Handler { - * override fun handle(t: Sample) = println(t) - * - * override fun receiver() = Unit - * - * override fun onClose() = println("Closing handler") - * } * - * Session.open().onSuccess { session -> - * session.use { - * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> - * session.declareSubscriber(keyExpr, handler = ExampleHandler()) - * .onSuccess { - * println("Declared subscriber on $keyExpr.") - * } - * } - * } - * } - * ``` - * - * @param keyExpr The [KeyExpr] the subscriber will be associated to. - * @param handler [Handler] implementation to handle the received samples. [Handler.onClose] will be called - * upon closing the session. - * @param onClose Callback function to be called when the subscriber is closed. - * @param reliability The reliability the subscriber wishes to obtain from the network. - * @return A result with the [Subscriber] in case of success. - */ - fun declareSubscriber( - keyExpr: KeyExpr, - handler: Handler, - onClose: (() -> Unit)? = null, - reliability: Reliability = Reliability.BEST_EFFORT - ): Result> { - val resolvedOnClose = fun() { - handler.onClose() - onClose?.invoke() - } - val callback = Callback { t: Sample -> handler.handle(t) } - return resolveSubscriber(keyExpr, callback, resolvedOnClose, handler.receiver(), reliability) - } - - /** - * Declare a [Subscriber] on the session, specifying a channel pipe the received samples. - * - * Example: * ```kotlin - * * Session.open().onSuccess { session -> * session.use { * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> - * val samplesChannel = Channel() - * session.declareSubscriber(keyExpr, channel = samplesChannel) - * .onSuccess { - * println("Declared subscriber on $keyExpr.") + * session.declareSubscriber(keyExpr) + * .bestEffort() + * .res() + * .onSuccess { subscriber -> + * subscriber.use { + * println("Declared subscriber on $keyExpr.") + * runBlocking { + * val receiver = subscriber.receiver!! + * val iterator = receiver.iterator() + * while (iterator.hasNext()) { + * val sample = iterator.next() + * println(sample) + * } + * } * } - * } - * // ... + * } * } * } + * } * ``` * * @param keyExpr The [KeyExpr] the subscriber will be associated to. - * @param channel [Channel] instance through which the received samples will be piped. Once the subscriber is - * closed, the channel is closed as well. - * @param onClose Callback function to be called when the subscriber is closed. [Handler.onClose] will be called - * upon closing the session. - * @param reliability The reliability the subscriber wishes to obtain from the network. - * @return A result with the [Subscriber] in case of success. + * @return A [Subscriber.Builder] with a [Channel] receiver. */ - fun declareSubscriber( - keyExpr: KeyExpr, - channel: Channel, - onClose: (() -> Unit)? = null, - reliability: Reliability = Reliability.BEST_EFFORT - ): Result>> { - val channelHandler = ChannelHandler(channel) - val resolvedOnClose = fun() { - channelHandler.onClose() - onClose?.invoke() - } - val callback = Callback { t: Sample -> channelHandler.handle(t) } - return resolveSubscriber(keyExpr, callback, resolvedOnClose, channelHandler.receiver(), reliability) - } + fun declareSubscriber(keyExpr: KeyExpr): Subscriber.Builder> = Subscriber.newBuilder(this, keyExpr) /** - * Declare a [Queryable] on the session with a callback. + * Declare a [Queryable] on the session. * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * println("Declaring Queryable") - * val queryable = session.declareQueryable(keyExpr, callback = { query -> - * query.replySuccess(keyExpr, value = Value("Hello!")) - * .onSuccess { println("Replied hello.") } - * .onFailure { println(it) } - * }).getOrThrow() - * } - * }} - * ``` + * The default receiver is a [Channel], but can be changed with the [Queryable.Builder.with] functions. * - * @param keyExpr The [KeyExpr] the queryable will be associated to. - * @param callback The callback to handle the received queries. - * @param onClose Callback to be run upon closing the queryable. - * @param complete The queryable completeness. - * @return A result with the queryable. - * @see Query - */ - fun declareQueryable( - keyExpr: KeyExpr, - callback: Callback, - onClose: (() -> Unit)? = null, - complete: Boolean = false - ): Result> { - return resolveQueryable(keyExpr, callback, fun() { onClose?.invoke() }, Unit, complete) - } - - /** - * Declare a [Queryable] on the session with a [Handler]. - * - * Example: we create a class `ExampleHandler` that implements the [Handler] interface to reply - * to the incoming queries: - * - * ```kotlin - * class ExampleHandler: Handler { - * override fun handle(t: Query) = query.replySuccess(query.keyExpr, value = Value("Hello!")) - * - * override fun receiver() = Unit - * - * override fun onClose() = println("Closing handler") - * } - * ``` - * - * Then we'd use it as follows: + * Example: * ```kotlin * Session.open().onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * val exampleHandler = ExampleHandler() - * val queryable = session.declareQueryable(keyExpr, handler = exampleHandler).getOrThrow() - * // ... - * } - * }} - * ``` - * - * @param keyExpr The [KeyExpr] the queryable will be associated to. - * @param handler The [Handler] to handle the incoming queries. [Handler.onClose] will be called upon - * closing the queryable. - * @param onClose Callback to be run upon closing the queryable. - * @param complete The completeness of the queryable. - * @return A result with the queryable. - */ - fun declareQueryable( - keyExpr: KeyExpr, - handler: Handler, - onClose: (() -> Unit)? = null, - complete: Boolean = false - ): Result> { - return resolveQueryable(keyExpr, { t: Query -> handler.handle(t) }, fun() { - handler.onClose() - onClose?.invoke() - }, handler.receiver(), complete) - } - - /** - * Declare a [Queryable] with a [Channel] to pipe the incoming queries. - * - * Example: - * ```kotlin - * Session.open(config).onSuccess { session -> - * session.use { - * key.intoKeyExpr().onSuccess { keyExpr -> - * println("Declaring Queryable on $key...") - * session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> - * runBlocking { - * for (query in queryable.receiver) { - * val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" - * println(">> [Queryable] Received Query '${query.selector}' $valueInfo") - * query.replySuccess(keyExpr, value = Value("Example reply")) - * .onSuccess { println(">> [Queryable ] Performed reply...") } - * .onFailure { println(">> [Queryable ] Error sending reply: $it") } + * session.declareQueryable(keyExpr).res().onSuccess { queryable -> + * queryable.use { + * it.receiver?.let { receiverChannel -> + * runBlocking { + * val iterator = receiverChannel.iterator() + * while (iterator.hasNext()) { + * iterator.next().use { query -> + * println("Received query at ${query.keyExpr}") + * query.reply(keyExpr) + * .success("Hello!") + * .withKind(SampleKind.PUT) + * .withTimeStamp(TimeStamp.getCurrentTime()) + * .res() + * .onSuccess { println("Replied hello.") } + * .onFailure { println(it) } + * } + * } * } * } * } * } * } - * } + * }} * ``` * + * * @param keyExpr The [KeyExpr] the queryable will be associated to. - * @param channel The [Channel] to receive the incoming queries. It will be closed upon closing the queryable. - * @param onClose Callback to be run upon closing the queryable. - * @param complete The completeness of the queryable. - * @return A result with the queryable, where the [Queryable.receiver] is the provided [Channel]. + * @return A [Queryable.Builder] with a [Channel] receiver. */ - fun declareQueryable( - keyExpr: KeyExpr, - channel: Channel, - onClose: (() -> Unit)? = null, - complete: Boolean = false - ): Result>> { - val handler = ChannelHandler(channel) - return resolveQueryable(keyExpr, { t: Query -> handler.handle(t) }, fun() { - handler.onClose() - onClose?.invoke() - }, handler.receiver(), complete) - } + fun declareQueryable(keyExpr: KeyExpr): Queryable.Builder> = Queryable.newBuilder(this, keyExpr) /** * Declare a [KeyExpr]. * * Informs Zenoh that you intend to use the provided Key Expression repeatedly. - * Also, declared key expression provide additional optimizations by associating - * it with a native key expression representation, minimizing the amount of operations - * performed between the JVM and the Rust layer of this library. * - * A declared key expression is associated to the session from which it was declared. - * It can be undeclared with the function [undeclare], or alternatively when closing - * the session it will be automatically undeclared. Undeclaring a key expression causes - * it to be downgraded to a regular key expression without optimizations, this means - * that operations can still be performed with it. - * - * When declaring a subscriber, a queryable, or a publisher, it is not necessary - * to declare the key expression beforehand, since Zenoh is already informed of your - * intent to use their key expressions repeatedly. It can be handy when doing instead - * many repeated puts or reply operations. + * It is generally not needed to declare key expressions, as declaring a subscriber, + * a queryable, or a publisher will also inform Zenoh of your intent to use their + * key expressions repeatedly. * * Example: * ```kotlin * Session.open().onSuccess { session -> session.use { - * val keyExpr = session.declareKeyExpr("demo/kotlin/example").getOrThrow() - * for (i in 0..999) { - * put(keyExpr, "Put number $i!") + * session.declareKeyExpr("demo/kotlin/example").res().onSuccess { keyExpr -> + * keyExpr.use { + * session.declarePublisher(it).res().onSuccess { publisher -> + * // ... + * } + * } * } * }} * ``` * - * @param keyExpr The intended key expression. - * @return A result with the declared key expression. + * @param keyExpr The intended Key expression. + * @return A resolvable returning an optimized representation of the passed `keyExpr`. */ - fun declareKeyExpr(keyExpr: String): Result { - return jniSession?.run { + fun declareKeyExpr(keyExpr: String): Resolvable = Resolvable { + return@Resolvable jniSession?.run { declareKeyExpr(keyExpr).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } @@ -426,421 +246,155 @@ class Session private constructor(private val config: Config) : AutoCloseable { * otherwise the operation will result in a failure. * * @param keyExpr The key expression to undeclare. - * @return A result with the status of the undeclare operation. + * @return A resolvable returning the status of the undeclare operation. */ - fun undeclare(keyExpr: KeyExpr): Result { - return jniSession?.run { + fun undeclare(keyExpr: KeyExpr): Resolvable = Resolvable { + return@Resolvable jniSession?.run { undeclareKeyExpr(keyExpr) } ?: Result.failure(sessionClosedException) } /** - * Performs a Get query on the [selector], handling the replies with a callback. + * Declare a [Get] with a [Channel] receiver. * - * A callback must be provided to handle the incoming replies. A basic query can be achieved - * as follows: * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * session.get(selector, callback = { reply -> println(reply) }) - * } - * } - * } - * ``` - * - * Additionally, other optional parameters to the query can be specified, and the result - * of the operation can be checked as well: - * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * session.get( - * selector, - * callback = { reply -> println(reply) }, - * value = Value("Example value"), - * target = QueryTarget.BEST_MATCHING, - * attachment = ZBytes.from("Example attachment"), - * timeout = Duration.ofMillis(1000), - * onClose = { println("Query terminated.") } - * ).onSuccess { - * println("Get query launched...") - * }.onFailure { - * println("Error: $it") + * val timeout = Duration.ofMillis(10000) + * println("Opening Session") + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> + * session.get(keyExpr) + * .consolidation(ConsolidationMode.NONE) + * .target(QueryTarget.BEST_MATCHING) + * .withValue("Get value example") + * .with { reply -> println("Received reply $reply") } + * .timeout(timeout) + * .res() + * .onSuccess { + * // Leaving the session alive the same duration as the timeout for the sake of this example. + * Thread.sleep(timeout.toMillis()) * } * } * } * } * ``` - * - * @param selector The [Selector] on top of which the get query will be performed. - * @param callback [Callback] to handle the replies. - * @param value Optional [Value] for the query. - * @param attachment Optional attachment. - * @param target The [QueryTarget] of the query. - * @param consolidation The [ConsolidationMode] configuration. - * @param onClose Callback to be executed when the query is terminated. - * @return A [Result] with the status of the query. When [Result.success] is returned, that means - * the query was properly launched and not that it has received all the possible replies (this - * can't be known from the perspective of the query). + * @param selector The [KeyExpr] to be used for the get operation. + * @return a resolvable [Get.Builder] with a [Channel] receiver. */ - fun get( - selector: Selector, - callback: Callback, - value: Value? = null, - attachment: ZBytes? = null, - timeout: Duration = Duration.ofMillis(10000), - target: QueryTarget = QueryTarget.BEST_MATCHING, - consolidation: ConsolidationMode = ConsolidationMode.NONE, - onClose: (() -> Unit)? = null - ) : Result { - return resolveGet ( - selector = selector, - callback = callback, - onClose = fun() { onClose?.invoke() }, - receiver = Unit, - timeout = timeout, - target = target, - consolidation = consolidation, - value = value, - attachment = attachment - ) - } + fun get(selector: Selector): Get.Builder> = Get.newBuilder(this, selector) /** - * Performs a Get query on the [selector], handling the replies with a [Handler]. + * Declare a [Get] with a [Channel] receiver. * - * A handler must be provided to handle the incoming replies. For instance, imagine we implement - * a `QueueHandler`: * ```kotlin - * class QueueHandler : Handler> { - * private val queue: ArrayDeque = ArrayDeque() - * - * override fun handle(t: T) { - * queue.add(t) - * } - * - * override fun receiver(): ArrayDeque { - * return queue - * } - * - * override fun onClose() { - * println("Received in total ${queue.size} elements.") - * } - * } - * ``` - * - * then we could use it as follows: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * val handler = QueueHandler() - * val receiver = session.get(selector, handler).getOrThrow() - * // ... - * for (reply in receiver) { - * println(reply) - * } - * } - * } - * } - * ``` - * - * Additionally, other optional parameters to the query can be specified, and the result - * of the operation can be checked as well: - * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * val handler = QueueHandler() - * session.get( - * selector, - * handler, - * value = Value("Example value"), - * target = QueryTarget.BEST_MATCHING, - * attachment = ZBytes.from("Example attachment"), - * timeout = Duration.ofMillis(1000), - * onClose = { println("Query terminated.") } - * ).onSuccess { receiver -> - * println("Get query launched...") - * // ... - * for (reply in receiver) { - * println(reply) - * } - * }.onFailure { - * println("Error: $it") + * val timeout = Duration.ofMillis(10000) + * println("Opening Session") + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> + * session.get(keyExpr) + * .consolidation(ConsolidationMode.NONE) + * .target(QueryTarget.BEST_MATCHING) + * .withValue("Get value example") + * .with { reply -> println("Received reply $reply") } + * .timeout(timeout) + * .res() + * .onSuccess { + * // Leaving the session alive the same duration as the timeout for the sake of this example. + * Thread.sleep(timeout.toMillis()) * } * } * } * } * ``` - * - * @param selector The [Selector] on top of which the get query will be performed. - * @param handler [Handler] to handle the replies. - * @param value Optional [Value] for the query. - * @param attachment Optional attachment. - * @param target The [QueryTarget] of the query. - * @param consolidation The [ConsolidationMode] configuration. - * @param onClose Callback to be executed when the query is terminated. - * @return A [Result] with the [handler]'s receiver of type [R]. When [Result.success] is returned, that means - * the query was properly launched and not that it has received all the possible replies (this - * can't be known from the perspective of the query). + * @param keyExpr The [KeyExpr] to be used for the get operation. + * @return a resolvable [Get.Builder] with a [Channel] receiver. */ - fun get( - selector: Selector, - handler: Handler, - value: Value? = null, - attachment: ZBytes? = null, - timeout: Duration = Duration.ofMillis(10000), - target: QueryTarget = QueryTarget.BEST_MATCHING, - consolidation: ConsolidationMode = ConsolidationMode.NONE, - onClose: (() -> Unit)? = null - ) : Result { - return resolveGet( - selector = selector, - callback = { r: Reply -> handler.handle(r) }, - onClose = fun() { - handler.onClose() - onClose?.invoke() - }, - receiver = handler.receiver(), - timeout = timeout, - target = target, - consolidation = consolidation, - value = value, - attachment = attachment - ) - } - - /** - * Performs a Get query on the [selector], handling the replies with a blocking [Channel]. - * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * session.get(selector, channel = Channel()).onSuccess { channel -> - * runBlocking { - * for (reply in channel) { - * println("Received $reply") - * } - * } - * }.onFailure { - * println("Error: $it") - * } - * } - * } - * } - * ``` - * - * Additionally, other optional parameters to the query can be specified, and the result - * of the operation can be checked as well: - * - * Example: - * - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoSelector().onSuccess { selector -> - * session.get(selector, - * channel = Channel(), - * value = Value("Example value"), - * target = QueryTarget.BEST_MATCHING, - * attachment = ZBytes.from("Example attachment"), - * timeout = Duration.ofMillis(1000), - * onClose = { println("Query terminated.") } - * ).onSuccess { channel -> - * runBlocking { - * for (reply in channel) { - * println("Received $reply") - * } - * } - * }.onFailure { - * println("Error: $it") - * } - * } - * } - * } - * ``` - * - * @param selector The [Selector] on top of which the get query will be performed. - * @param channel Blocking [Channel] to handle the replies. - * @param value Optional [Value] for the query. - * @param attachment Optional attachment. - * @param target The [QueryTarget] of the query. - * @param consolidation The [ConsolidationMode] configuration. - * @param onClose Callback to be executed when the query is terminated. - * @return A [Result] with the [channel] on success. When [Result.success] is returned, that means - * the query was properly launched and not that it has received all the possible replies (this - * can't be known from the perspective of the query). - */ - fun get( - selector: Selector, - channel: Channel, - value: Value? = null, - attachment: ZBytes? = null, - timeout: Duration = Duration.ofMillis(10000), - target: QueryTarget = QueryTarget.BEST_MATCHING, - consolidation: ConsolidationMode = ConsolidationMode.NONE, - onClose: (() -> Unit)? = null - ) : Result> { - val channelHandler = ChannelHandler(channel) - return resolveGet( - selector = selector, - callback = { r: Reply -> channelHandler.handle(r) }, - onClose = fun() { - channelHandler.onClose() - onClose?.invoke() - }, - receiver = channelHandler.receiver(), - timeout = timeout, - target = target, - consolidation = consolidation, - value = value, - attachment = attachment - ) - } + fun get(keyExpr: KeyExpr): Get.Builder> = Get.newBuilder(this, Selector(keyExpr)) /** * Declare a [Put] with the provided value on the specified key expression. * * Example: * ```kotlin - * Session.open(config).onSuccess { session -> - * session.use { - * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> - * session.put(keyExpr, value = Value("Example value")).getOrThrow() - * } - * // ... - * } - * } - * ``` - * - * Additionally, a [QoS] configuration can be specified as well as an attachment, for instance: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> - * val exampleQoS = QoS( - * congestionControl = CongestionControl.DROP, - * express = true, - * priority = Priority.DATA_HIGH) - * val exampleAttachment = "exampleAttachment".into() - * session.put( - * keyExpr, - * value = Value("Example value"), - * qos = exampleQoS, - * attachment = exampleAttachment).getOrThrow() - * } - * // ... - * } + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> + * session.put(keyExpr, Value("Hello")) + * .congestionControl(CongestionControl.BLOCK) + * .priority(Priority.REALTIME) + * .res() + * .onSuccess { println("Put 'Hello' on $keyExpr.") } + * }} * } * ``` * * @param keyExpr The [KeyExpr] to be used for the put operation. * @param value The [Value] to be put. - * @param qos The [QoS] configuration. - * @param attachment Optional attachment. - * @return A [Result] with the status of the put operation. + * @return A resolvable [Put.Builder]. */ - fun put(keyExpr: KeyExpr, value: Value, qos: QoS = QoS.default(), attachment: ZBytes? = null) : Result { - val put = Put(keyExpr, value, qos, attachment) - return resolvePut(keyExpr, put) - } + fun put(keyExpr: KeyExpr, value: Value): Put.Builder = Put.newBuilder(this, keyExpr, value) /** - * Declare a [Put] with the provided message on the specified key expression. + * Declare a [Put] with the provided value on the specified key expression. * * Example: * ```kotlin - * Session.open(config).onSuccess { session -> - * session.use { - * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> - * session.put(keyExpr, "Example message").getOrThrow() - * } - * // ... - * } - * } - * ``` - * - * Additionally, a [QoS] configuration can be specified as well as an attachment, for instance: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> - * val exampleQoS = QoS( - * congestionControl = CongestionControl.DROP, - * express = true, - * priority = Priority.DATA_HIGH) - * val exampleAttachment = "exampleAttachment".into() - * session.put( - * keyExpr, - * message = "Example message", - * qos = exampleQoS, - * attachment = exampleAttachment).getOrThrow() - * } - * // ... - * } + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> + * session.put(keyExpr, "Hello") + * .congestionControl(CongestionControl.BLOCK) + * .priority(Priority.REALTIME) + * .res() + * .onSuccess { println("Put 'Hello' on $keyExpr.") } + * }} * } * ``` * * @param keyExpr The [KeyExpr] to be used for the put operation. - * @param message The [String] message to put. - * @param qos The [QoS] configuration. - * @param attachment Optional attachment. - * @return A [Result] with the status of the put operation. + * @param message The message to be put. + * @return A resolvable [Put.Builder]. */ - fun put(keyExpr: KeyExpr, message: String, qos: QoS = QoS.default(), attachment: ZBytes? = null) : Result { - val put = Put(keyExpr, Value(message), qos, attachment) - return resolvePut(keyExpr, put) - } + fun put(keyExpr: KeyExpr, message: String): Put.Builder = Put.newBuilder(this, keyExpr, Value(message)) /** - * Perform a delete operation. + * Declare a [Delete]. * * Example: + * * ```kotlin - * Session.open(config).onSuccess { session -> + * println("Opening Session") + * Session.open().onSuccess { session -> * session.use { - * key.intoKeyExpr().onSuccess { keyExpr -> - * println("Deleting resources matching '$keyExpr'...") - * session.delete(keyExpr) + * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> + * session.delete(keyExpr) + * .res() + * .onSuccess { + * println("Performed a delete on $keyExpr.") + * } * } * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the delete operation. - * @param qos The [QoS] configuration. - * @param attachment Optional [ZBytes] attachment. - * @return a [Result] with the status of the operation. + * @return a resolvable [Delete.Builder]. */ - fun delete(keyExpr: KeyExpr, qos: QoS = QoS.default(), attachment: ZBytes? = null): Result { - val delete = Delete(keyExpr, qos, attachment) - return resolveDelete(keyExpr, delete) - } + fun delete(keyExpr: KeyExpr): Delete.Builder = Delete.newBuilder(this, keyExpr) /** Returns if session is open or has been closed. */ fun isOpen(): Boolean { return jniSession != null } - private fun resolvePublisher(keyExpr: KeyExpr, qos: QoS): Result { + internal fun resolvePublisher(keyExpr: KeyExpr, qos: QoS): Result { return jniSession?.run { declarePublisher(keyExpr, qos).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } - private fun resolveSubscriber( + internal fun resolveSubscriber( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R, + receiver: R?, reliability: Reliability ): Result> { return jniSession?.run { @@ -848,11 +402,11 @@ class Session private constructor(private val config: Config) : AutoCloseable { } ?: Result.failure(sessionClosedException) } - private fun resolveQueryable( + internal fun resolveQueryable( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R, + receiver: R?, complete: Boolean ): Result> { return jniSession?.run { @@ -860,38 +414,27 @@ class Session private constructor(private val config: Config) : AutoCloseable { } ?: Result.failure(sessionClosedException) } - private fun resolveGet( + internal fun resolveGet( selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R, + receiver: R?, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, value: Value?, - attachment: ZBytes?, - ): Result { + attachment: ByteArray?, + ): Result { return jniSession?.run { - performGet( - selector, - callback, - onClose, - receiver, - timeout, - target, - consolidation, - value?.payload, - value?.encoding, - attachment - ) + performGet(selector, callback, onClose, receiver, timeout, target, consolidation, value, attachment) } ?: Result.failure(sessionClosedException) } - private fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { + internal fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { jniSession?.run { performPut(keyExpr, put) } } - private fun resolveDelete(keyExpr: KeyExpr, delete: Delete): Result = runCatching { + internal fun resolveDelete(keyExpr:KeyExpr, delete: Delete): Result = runCatching { jniSession?.run { performDelete(keyExpr, delete) } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt index 16d6c91f5..06097dcfa 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt @@ -21,13 +21,15 @@ import kotlinx.coroutines.runBlocking /** * Channel handler * - * Implementation of a [Handler] with a [Channel] receiver. + * Implementation of a [Handler] with a [Channel] receiver. This handler is intended to be used + * as the default handler by the [io.zenoh.queryable.Queryable], [io.zenoh.subscriber.Subscriber] and [io.zenoh.query.Get], + * allowing us to send the incoming elements through a [Channel] within the context of a Kotlin coroutine. * * @param T * @property channel * @constructor Create empty Channel handler */ -internal class ChannelHandler(private val channel: Channel) : Handler> { +class ChannelHandler(private val channel: Channel) : Handler> { override fun handle(t: T) { runBlocking { channel.send(t) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt index 18da152a9..774c3ce29 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt @@ -23,6 +23,7 @@ import io.zenoh.ZenohType * **Example**: * ```kotlin * class QueueHandler : Handler> { + * * private val queue: ArrayDeque = ArrayDeque() * * override fun handle(t: T) { @@ -42,7 +43,11 @@ import io.zenoh.ZenohType * * That `QueueHandler` could then be used as follows, for instance for a subscriber: * ```kotlin - * val subscriber = session.declareSubscriber(keyExpr, handler = QueueHandler()).getOrThrow() + * val handler = QueueHandler() + * val receiver = session.declareSubscriber(keyExpr) + * .with(handler) + * .res() + * .onSuccess { ... } * ``` * * @param T A receiving [ZenohType], either a [io.zenoh.sample.Sample], a [io.zenoh.query.Reply] or a [io.zenoh.queryable.Query]. @@ -67,7 +72,8 @@ interface Handler { * * For instances of [io.zenoh.queryable.Queryable] and [io.zenoh.subscriber.Subscriber], * Zenoh triggers this callback when they are closed or undeclared. In the case of a Get query - * it is invoked when no more elements of type [T] are expected to be received. + * (see [io.zenoh.query.Get]), it is invoked when no more elements of type [T] are expected + * to be received. */ fun onClose() } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt index f1b57f971..93dbffa7e 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -14,7 +14,6 @@ package io.zenoh.jni -import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** @@ -30,8 +29,8 @@ internal class JNIPublisher(private val ptr: Long) { * @param value The [Value] to be put. * @param attachment Optional attachment. */ - fun put(value: Value, attachment: ZBytes?): Result = runCatching { - putViaJNI(value.payload.bytes, value.encoding.id.ordinal, value.encoding.schema, attachment?.bytes, ptr) + fun put(value: Value, attachment: ByteArray?): Result = runCatching { + putViaJNI(value.payload, value.encoding.id.ordinal, value.encoding.schema, attachment, ptr) } /** @@ -39,8 +38,8 @@ internal class JNIPublisher(private val ptr: Long) { * * @param attachment Optional attachment. */ - fun delete(attachment: ZBytes?): Result = runCatching { - deleteViaJNI(attachment?.bytes, ptr) + fun delete(attachment: ByteArray?): Result = runCatching { + deleteViaJNI(attachment, ptr) } /** diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt index f881fbc1e..fd5eae3c3 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -16,7 +16,6 @@ package io.zenoh.jni import io.zenoh.keyexpr.KeyExpr import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes import io.zenoh.sample.Sample import io.zenoh.value.Value import org.apache.commons.net.ntp.TimeStamp @@ -36,12 +35,12 @@ internal class JNIQuery(private val ptr: Long) { ptr, sample.keyExpr.jniKeyExpr?.ptr ?: 0, sample.keyExpr.keyExpr, - sample.value.payload.bytes, + sample.value.payload, sample.value.encoding.id.ordinal, sample.value.encoding.schema, timestampEnabled, if (timestampEnabled) sample.timestamp!!.ntpValue() else 0, - sample.attachment?.bytes, + sample.attachment, sample.qos.express, sample.qos.priority.value, sample.qos.congestionControl.value @@ -49,10 +48,10 @@ internal class JNIQuery(private val ptr: Long) { } fun replyError(errorValue: Value): Result = runCatching { - replyErrorViaJNI(ptr, errorValue.payload.bytes, errorValue.encoding.id.ordinal, errorValue.encoding.schema) + replyErrorViaJNI(ptr, errorValue.payload, errorValue.encoding.id.ordinal, errorValue.encoding.schema) } - fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: ZBytes?, qos: QoS): Result = + fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: ByteArray?, qos: QoS): Result = runCatching { val timestampEnabled = timestamp != null replyDeleteViaJNI( @@ -61,7 +60,7 @@ internal class JNIQuery(private val ptr: Long) { keyExpr.keyExpr, timestampEnabled, if (timestampEnabled) timestamp!!.ntpValue() else 0, - attachment?.bytes, + attachment, qos.express, qos.priority.value, qos.congestionControl.value diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index bd10d4f56..ceaba4678 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -25,9 +25,7 @@ import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr import io.zenoh.prelude.* -import io.zenoh.protocol.ZBytes import io.zenoh.protocol.ZenohID -import io.zenoh.protocol.into import io.zenoh.publication.Delete import io.zenoh.publication.Publisher import io.zenoh.publication.Put @@ -84,7 +82,7 @@ internal class JNISession { } fun declareSubscriber( - keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R, reliability: Reliability + keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R?, reliability: Reliability ): Result> = runCatching { val subCallback = JNISubscriberCallback { keyExpr, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> @@ -94,8 +92,8 @@ internal class JNISession { Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), SampleKind.fromInt(kind), timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() + QoS(express, congestionControl, priority), + attachmentBytes ) callback.run(sample) } @@ -106,19 +104,16 @@ internal class JNISession { } fun declareQueryable( - keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R, complete: Boolean + keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R?, complete: Boolean ): Result> = runCatching { val queryCallback = - JNIQueryableCallback { keyExpr: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long -> + JNIQueryableCallback { keyExpr: String, selectorParams: String, withValue: Boolean, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long -> val jniQuery = JNIQuery(queryPtr) val keyExpr2 = KeyExpr(keyExpr, null) - val selector = if (selectorParams.isEmpty()) { - Selector(keyExpr2) - } else { - Selector(keyExpr2, selectorParams) - } - val value = payload?.let { Value(it, Encoding(ID.fromId(encodingId)!!, encodingSchema)) } - val query = Query(keyExpr2, selector, value, attachmentBytes?.into(), jniQuery) + val selector = Selector(keyExpr2, selectorParams) + val value: Value? = + if (withValue) Value(payload!!, Encoding(ID.fromId(encodingId)!!, encodingSchema)) else null + val query = Query(keyExpr2, selector, value, attachmentBytes, jniQuery) callback.run(query) } val queryableRawPtr = declareQueryableViaJNI( @@ -131,14 +126,13 @@ internal class JNISession { selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R, + receiver: R?, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, - payload: ZBytes?, - encoding: Encoding?, - attachment: ZBytes? - ): Result = runCatching { + value: Value?, + attachment: ByteArray? + ): Result = runCatching { val getCallback = JNIGetCallback { replierId: String?, success: Boolean, @@ -164,8 +158,8 @@ internal class JNISession { Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), SampleKind.fromInt(kind), timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() + QoS(express, congestionControl, priority), + attachmentBytes ) reply = Reply.Success(replierId?.let { ZenohID(it) }, sample) } @@ -175,16 +169,13 @@ internal class JNISession { replierId?.let { ZenohID(it) }, KeyExpr(keyExpr!!, null), timestamp, - attachmentBytes?.into(), - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express) + attachmentBytes, + QoS(express, congestionControl, priority) ) } } } else { - reply = Reply.Error( - replierId?.let { ZenohID(it) }, - Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)) - ) + reply = Reply.Error(replierId?.let { ZenohID(it) }, Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema))) } callback.run(reply) } @@ -199,10 +190,11 @@ internal class JNISession { timeout.toMillis(), target.ordinal, consolidation.ordinal, - attachment?.bytes, - payload?.bytes, - encoding?.id?.ordinal ?: ID.default().id, - encoding?.schema + attachment, + value != null, + value?.payload, + value?.encoding?.id?.ordinal ?: 0, + value?.encoding?.schema ) receiver } @@ -228,13 +220,13 @@ internal class JNISession { keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr.get(), - put.value.payload.bytes, + put.value.payload, put.value.encoding.id.ordinal, put.value.encoding.schema, put.qos.congestionControl.value, put.qos.priority.value, put.qos.express, - put.attachment?.bytes + put.attachment ) } @@ -250,7 +242,7 @@ internal class JNISession { delete.qos.congestionControl.value, delete.qos.priority.value, delete.qos.express, - delete.attachment?.bytes + delete.attachment ) } @@ -303,7 +295,7 @@ internal class JNISession { private external fun getViaJNI( keyExprPtr: Long, keyExprString: String, - selectorParams: String?, + selectorParams: String, sessionPtr: Long, callback: JNIGetCallback, onClose: JNIOnCloseCallback, @@ -311,6 +303,7 @@ internal class JNISession { target: Int, consolidation: Int, attachmentBytes: ByteArray?, + withValue: Boolean, payload: ByteArray?, encodingId: Int, encodingSchema: String?, diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt deleted file mode 100644 index 804cd07d5..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.zenoh.jni - -import io.zenoh.ZenohLoad -import io.zenoh.protocol.ZBytes -import io.zenoh.protocol.into - -object JNIZBytes { - - init { - ZenohLoad - } - - fun serializeIntoList(list: List): ZBytes { - return serializeIntoListViaJNI(list.map { it.bytes }).into() - } - - fun deserializeIntoList(zbytes: ZBytes): List { - return deserializeIntoListViaJNI(zbytes.bytes).map { it.into() }.toList() - } - - fun serializeIntoMap(map: Map): ZBytes { - return serializeIntoMapViaJNI(map.map { (k, v) -> k.bytes to v.bytes }.toMap()).into() - } - - fun deserializeIntoMap(bytes: ZBytes): Map { - return deserializeIntoMapViaJNI(bytes.bytes).map { (k, v) -> k.into() to v.into() }.toMap() - } - - private external fun serializeIntoMapViaJNI(map: Map): ByteArray - - private external fun serializeIntoListViaJNI(list: List): ByteArray - - private external fun deserializeIntoMapViaJNI(payload: ByteArray): Map - - private external fun deserializeIntoListViaJNI(payload: ByteArray): List -} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt index 31f5885f8..84ac7ca2f 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt @@ -17,6 +17,7 @@ package io.zenoh.jni.callbacks internal fun interface JNIQueryableCallback { fun run(keyExpr: String, selectorParams: String, + withValue: Boolean, payload: ByteArray?, encodingId: Int, encodingSchema: String?, diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index e6654f088..41c7f6e11 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -14,10 +14,10 @@ package io.zenoh.keyexpr +import io.zenoh.Resolvable import io.zenoh.Session import io.zenoh.SessionDeclaration import io.zenoh.jni.JNIKeyExpr -import io.zenoh.selector.Selector /** * # Address space @@ -115,9 +115,9 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * Undeclare the key expression if it was previously declared on the specified [session]. * * @param session The session from which the key expression was previously declared. - * @return A [Result] with the operation status. + * @return An empty [Resolvable]. */ - fun undeclare(session: Session): Result { + fun undeclare(session: Session): Resolvable { return session.undeclare(this) } @@ -128,10 +128,6 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn return jniKeyExpr != null } - fun intoSelector(): Selector { - return Selector(this) - } - override fun toString(): String { return keyExpr } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt index 4fa635b70..22e3fdc5f 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt @@ -26,7 +26,23 @@ package io.zenoh.prelude * This is particularly useful in helping Zenoh to perform additional network optimizations. * */ -data class Encoding(val id: ID, val schema: String? = null) { +class Encoding(val id: ID, val schema: String? = null) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Encoding + + if (id != other.id) return false + return schema == other.schema + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + schema.hashCode() + return result + } /** * The ID of the encoding. @@ -95,8 +111,7 @@ data class Encoding(val id: ID, val schema: String? = null) { companion object { private val idToEnum = entries.associateBy(ID::id) - internal fun fromId(id: Int): ID? = idToEnum[id] - internal fun default() = ZENOH_BYTES + fun fromId(id: Int): ID? = idToEnum[id] } } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt index c8f0420ea..1984006a3 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt @@ -17,19 +17,52 @@ package io.zenoh.prelude /** * Quality of service settings used to send zenoh message. * + * @property express If true, the message is not batched in order to reduce the latency. * @property congestionControl [CongestionControl] policy used for the message. * @property priority [Priority] policy used for the message. - * @property express If true, the message is not batched in order to reduce the latency. */ -data class QoS ( - val congestionControl: CongestionControl = CongestionControl.DROP, - val priority: Priority = Priority.DATA, - val express: Boolean = false +class QoS internal constructor( + internal val express: Boolean, + internal val congestionControl: CongestionControl, + internal val priority: Priority ) { + internal constructor(express: Boolean, congestionControl: Int, priority: Int) : this( + express, CongestionControl.fromInt(congestionControl), Priority.fromInt(priority) + ) + + /** + * Returns priority of the message. + */ + fun priority(): Priority = priority + + /** + * Returns congestion control setting of the message. + */ + fun congestionControl(): CongestionControl = congestionControl + + /** + * Returns express flag. If it is true, the message is not batched to reduce the latency. + */ + fun isExpress(): Boolean = express + companion object { - private val defaultQoS = QoS() + fun default() = QoS(false, CongestionControl.default(), Priority.default()) + } + + internal class Builder( + private var express: Boolean = false, + private var congestionControl: CongestionControl = CongestionControl.default(), + private var priority: Priority = Priority.default(), + ) { + + fun express(value: Boolean) = apply { this.express = value } + + fun priority(priority: Priority) = apply { this.priority = priority } + + fun congestionControl(congestionControl: CongestionControl) = + apply { this.congestionControl = congestionControl } - fun default() = defaultQoS + fun build() = QoS(express, congestionControl, priority) } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt deleted file mode 100644 index 9ad142452..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.zenoh.protocol - -/** - * Deserializable interface. - * - * Classes implementing these two nested interfaces can be deserialized into a ZBytes object. - * - * The class must be declared as [Deserializable], but it's also necessary to make the companion - * object of the class implement the [Deserializable.From], as shown in the example below: - * - * ```kotlin - * class Foo(val content: String) : Deserializable { - * - * companion object: Deserializable.From { - * override fun from(zbytes: ZBytes): Foo { - * return Foo(zbytes.toString()) - * } - * } - * } - * ``` - */ -interface Deserializable { - interface From { - fun from(zbytes: ZBytes): Serializable - } -} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt deleted file mode 100644 index 0c6bd8182..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.zenoh.protocol - -/** - * Serializable interface. - * - * Classes implementing this interface can be serialized into a ZBytes object. - * - * Example: - * ```kotlin - * class Foo(val content: String) : Serializable { - * - * override fun into(): ZBytes = content.into() - * } - * ``` - */ -interface Serializable { - fun into(): ZBytes -} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt deleted file mode 100644 index 1cb2dc75e..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt +++ /dev/null @@ -1,552 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.protocol - -import io.zenoh.jni.JNIZBytes -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.reflect.KClass -import kotlin.reflect.KFunction1 -import kotlin.reflect.KType -import kotlin.reflect.full.* -import kotlin.reflect.jvm.jvmErasure -import kotlin.reflect.typeOf - -/** - * The ZBytes class (Zenoh bytes) represents the bytes received through the Zenoh network. - * - * It provides many utilities to serialize an object into a ZBytes, as well as to deserialize from a ZBytes instance. - * - * # Serialization - * - * Supported types: - * - * ## Raw types - * - * * Numeric: Byte, Short, Int, Long, Float and Double.** - * * String - * * ByteArray - * - * For the raw types, there are basically three ways to serialize them into a ZBytes, for instance let's suppose - * we want to serialize an `Int`, we could achieve it by:: - * * using the `into()` syntax: - * ```kotlin - * val exampleInt: Int = 256 - * val zbytes: ZBytes = exampleInt.into() - * ``` - * - * * using the `from()` syntax: - * ```kotlin - * val exampleInt: Int = 256 - * val zbytes: ZBytes = ZBytes.from(exampleInt) - * ``` - * - * * using the serialize syntax: - * ```kotlin - * val exampleInt: Int = 256 - * val zbytes: ZBytes = ZBytes.serialize(exampleInt).getOrThrow() - * ``` - * This approach works as well for the other mentioned types. - * - * ## Lists - * - * Lists are supported, but they must be either: - * - List of [Number] (Byte, Short, Int, Long, Float or Double) - * - List of [String] - * - List of [ByteArray] - * - List of [Serializable] - * - * The serialize syntax must be used: - * ```kotlin - * val myList = listOf(1, 2, 5, 8, 13, 21) - * val zbytes = ZBytes.serialize>(myList).getOrThrow() - * ``` - * - * ## Maps - * - * Maps are supported as well, with the restriction that their inner types must be either: - * - [Number] - * - [String] - * - [ByteArray] - * - [Serializable] - * - * ```kotlin - * val myMap: Map = mapOf("foo" to 1, "bar" to 2) - * val zbytes = ZBytes.serialize>(myMap).getOrThrow() - * ``` - * - * # Deserialization - * - * ## Raw types - * - * * Numeric: Byte, Short, Int, Long, Float and Double - * * String - * * ByteArray - * - * Example: - * - * For these raw types, you can use the functions `to`, that is - * - [toByte] - * - [toShort] - * - [toInt] - * - [toLong] - * - [toDouble] - * - [toString] - * - [toByteArray] - * - * For instance, for an Int: - * ```kotlin - * val example: Int = 256 - * val zbytes: ZBytes = exampleInt.into() - * val deserializedInt = zbytes.toInt() - * ``` - * - * Alternatively, the deserialize syntax can be used as well: - * ```kotlin - * val exampleInt: Int = 256 - * val zbytes: ZBytes = exampleInt.into() - * val deserializedInt = zbytes.deserialize().getOrThrow() - * ``` - * - * ## Lists - * - * Lists are supported, but they must be deserialized either into a: - * - List of [Number] (Byte, Short, Int, Long, Float or Double) - * - List of [String] - * - List of [ByteArray] - * - List of [Deserializable] - * - * To deserialize into a list, we need to use the deserialize syntax as follows: - * ```kotlin - * val inputList = listOf("sample1", "sample2", "sample3") - * payload = ZBytes.serialize(inputList).getOrThrow() - * val outputList = payload.deserialize>().getOrThrow() - * ``` - * - * ## Maps - * - * Maps are supported as well, with the restriction that their inner types must be either: - * - [Number] - * - [String] - * - [ByteArray] - * - [Deserializable] - * - * ```kotlin - * val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") - * payload = ZBytes.serialize(inputMap).getOrThrow() - * val outputMap = payload.deserialize>().getOrThrow() - * check(inputMap == outputMap) - * ``` - * - * # Custom serialization and deserialization - * - * ## Serialization - * - * For custom serialization, classes to be serialized need to implement the [Serializable] interface. - * For instance: - * - * ```kotlin - * class Foo(val content: String) : Serializable { - * - * /*Inherits: Serializable*/ - * override fun into(): ZBytes = content.into() - * } - * ``` - * - * This way, we can do: - * ```kotlin - * val foo = Foo("bar") - * val serialization = ZBytes.serialize(foo).getOrThrow() - * ``` - * - * Implementing the [Serializable] interface on a class enables the possibility of serializing lists and maps - * of that type, for instance: - * ```kotlin - * val list = listOf(Foo("bar"), Foo("buz"), Foo("fizz")) - * val zbytes = ZBytes.serialize>(list) - * ``` - * - * ## Deserialization - * - * For custom deserialization, classes to be serialized need to implement the [Deserializable] interface, and - * their companion object need to implement the [Deserializable.From] interface, for instance, let's make the - * `Foo` class (defined in the previous section) implement these interfaces: - * - * ```kotlin - * class Foo(val content: String) : Serializable, Deserializable { - * - * /*Inherits: Serializable*/ - * override fun into(): ZBytes = content.into() - * - * companion object: Deserializable.From { - * override fun from(zbytes: ZBytes): Foo { - * return Foo(zbytes.toString()) - * } - * } - * } - * ``` - * - * With this implementation, then the deserialization works as follows with the deserialization syntax: - * ```kotlin - * val foo = Foo("bar") - * val zbytes = ZBytes.serialize(foo).getOrThrow() - * val deserialization = zbytes.deserialize().getOrThrow() - * ``` - * - * Analogous to the serialization, we can deserialize into lists and maps of the type implementing - * the [Deserializable] interface: - * - * ```kotlin - * val list = listOf(Foo("bar"), Foo("buz"), Foo("fizz")) - * val zbytes = ZBytes.serialize>(list) - * val deserializedList = zbytes.deserialize>().getOrThrow() - * ``` - * - * ### Deserialization functions: - * - * The [deserialize] function admits an argument which by default is an emptyMap, consisting - * of a `Map>` map. This allows to specify types in a map, associating - * functions for deserialization for each of the types in the map. - * - * For instance, let's stick to the previous implementation of our example Foo class, when it - * only implemented the [Serializable] class: - * ```kotlin - * class Foo(val content: String) : Serializable { - * - * /*Inherits: Serializable*/ - * override fun into(): ZBytes = content.into() - * } - * ``` - * - * Instead of making it implement the [Deserializable] interface as explained previously, - * we could provide directly the deserialization function as follows: - * - * ```kotlin - * fun deserializeFoo(zbytes: ZBytes): Foo { - * return Foo(zbytes.toString()) - * } - * - * val foo = Foo("bar") - * val zbytes = ZBytes.serialize(foo) - * val deserialization = zbytes.deserialize(mapOf(typeOf() to ::deserializeFoo)).getOrThrow() - * ``` - */ -class ZBytes internal constructor(internal val bytes: ByteArray) : Serializable { - - companion object { - fun from(serializable: Serializable) = serializable.into() - fun from(string: String) = ZBytes(string.toByteArray()) - fun from(byteArray: ByteArray) = ZBytes(byteArray) - fun from(number: Number): ZBytes { - val byteArray = when (number) { - is Byte -> byteArrayOf(number) - is Short -> ByteBuffer.allocate(Short.SIZE_BYTES).apply { - order(ByteOrder.LITTLE_ENDIAN) - putShort(number) - }.array() - - is Int -> ByteBuffer.allocate(Int.SIZE_BYTES).apply { - order(ByteOrder.LITTLE_ENDIAN) - putInt(number) - }.array() - - is Long -> ByteBuffer.allocate(Long.SIZE_BYTES).apply { - order(ByteOrder.LITTLE_ENDIAN) - putLong(number) - }.array() - - is Float -> ByteBuffer.allocate(Float.SIZE_BYTES).apply { - order(ByteOrder.LITTLE_ENDIAN) - putFloat(number) - }.array() - - is Double -> ByteBuffer.allocate(Double.SIZE_BYTES).apply { - order(ByteOrder.LITTLE_ENDIAN) - putDouble(number) - }.array() - - else -> throw IllegalArgumentException("Unsupported number type") - } - return ZBytes(byteArray) - } - - /** - * Serialize an element of type [T] into a [ZBytes]. - * - * Supported types: - * - [Number]: Byte, Short, Int, Long, Float, Double - * - [String] - * - [ByteArray] - * - [Serializable] - * - Lists and Maps of the above-mentioned types. - * - * @see ZBytes - * @return a [Result] with the serialized [ZBytes]. - */ - inline fun serialize(t: T): Result = runCatching { - return serialize(t, T::class) - } - - fun serialize(t: T, clazz: KClass): Result = runCatching { - val type: KType = when (clazz) { - List::class -> typeOf>() - Map::class -> typeOf>() - else -> clazz.createType() - } - when { - typeOf>().isSupertypeOf(type) -> { - val list = t as List<*> - val zbytesList = list.map { it.into() } - return Result.success(JNIZBytes.serializeIntoList(zbytesList)) - } - - typeOf>().isSupertypeOf(type) -> { - val map = t as Map<*, *> - val zbytesMap = map.map { (k, v) -> k.into() to v.into() }.toMap() - return Result.success(JNIZBytes.serializeIntoMap(zbytesMap)) - } - - typeOf().isSupertypeOf(type) -> { - return Result.success((t as Any).into()) - } - - else -> throw IllegalArgumentException("Unsupported type '$type' for serialization.") - } - } - } - - /** - * Deserialize the [ZBytes] instance into an element of type [T]. - * - * Supported types: - * - [Number]: Byte, Short, Int, Long, Float, Double - * - [String] - * - [ByteArray] - * - [Deserializable] - * - Lists and Maps of the above-mentioned types. - * - * - * A map of types and functions for deserialization can also be provided. - * - * For instance: - * ```kotlin - * fun deserializeFoo(zbytes: ZBytes): Foo { - * return Foo(zbytes.toString()) - * } - * - * val foo = Foo("bar") - * val zbytes = ZBytes.serialize(foo) - * val deserialization = zbytes.deserialize(mapOf(typeOf() to ::deserializeFoo)).getOrThrow() - * ``` - * - * In case the provided type isn't associated with any of the functions provided in the [deserializers] map - * (if provided), the deserialization will carry on with the default behavior. - * - * @see ZBytes - * @see Deserializable - * @return a [Result] with the deserialization. - */ - inline fun deserialize( - deserializers: Map> = emptyMap() - ): Result { - val type = typeOf() - val deserializer = deserializers[type] - if (deserializer != null) { - return Result.success(deserializer(this) as T) - } - when { - typeOf>().isSupertypeOf(type) -> { - val itemsClass = type.arguments.firstOrNull()?.type?.jvmErasure - return deserialize(T::class, arg1clazz = itemsClass) - } - typeOf>().isSupertypeOf(type) -> { - val keyClass = type.arguments.getOrNull(0)?.type?.jvmErasure - val valueClass = type.arguments.getOrNull(1)?.type?.jvmErasure - return deserialize(T::class, arg1clazz = keyClass, arg2clazz = valueClass) - } - typeOf().isSupertypeOf(type) -> { - return deserialize(T::class) - } - } - throw IllegalArgumentException("Unsupported type for deserialization: '$type'.") - } - - /** - * Deserialize the [ZBytes] into an element of class [clazz]. - * - * It's generally preferable to use the [ZBytes.deserialize] function with reification, however - * this function is exposed for cases when reification needs to be avoided. - * - * Example: - * ```kotlin - * val list = listOf("value1", "value2", "value3") - * val zbytes = ZBytes.serialize(list).getOrThrow() - * val deserializedList = zbytes.deserialize(clazz = List::class, arg1clazz = String::class).getOrThrow() - * check(list == deserializedList) - * ``` - * - * Supported types: - * - [Number]: Byte, Short, Int, Long, Float, Double - * - [String] - * - [ByteArray] - * - [Deserializable] - * - Lists and Maps of the above-mentioned types. - * - * @see [ZBytes.deserialize] - * - * - * @param clazz: the [KClass] of the type to be serialized. - * @param arg1clazz Optional first nested parameter of the provided clazz, for instance when trying to deserialize - * into a `List`, arg1clazz should be set to `String::class`, when trying to deserialize into a - * `Map`, arg1clazz should be set to `Int::class`. Can be null if providing a basic type. - * @param arg2clazz Optional second nested parameter of the provided clazz, to be used for the cases of maps. - * For instance, when trying to deserialize into a `Map`, arg2clazz should be set to `String::class`. - * Can be null if providing a basic type. - */ - @Suppress("UNCHECKED_CAST") - fun deserialize( - clazz: KClass, - arg1clazz: KClass<*>? = null, - arg2clazz: KClass<*>? = null, - ): Result { - val type: KType = when (clazz) { - List::class -> typeOf>() - Map::class -> typeOf>() - else -> clazz.createType() - } - return when { - typeOf>().isSupertypeOf(type) -> { - val typeElement = arg1clazz?.createType() - if (typeElement != null) { - Result.success(JNIZBytes.deserializeIntoList(this).map { it.intoAny(typeElement) } as T) - } else { - Result.failure(IllegalArgumentException("Unsupported list type for deserialization: $type")) - } - } - - typeOf>().isSupertypeOf(type) -> { - val keyType = arg1clazz?.createType() - val valueType = arg2clazz?.createType() - if (keyType != null && valueType != null) { - Result.success(JNIZBytes.deserializeIntoMap(this) - .map { (k, v) -> k.intoAny(keyType) to v.intoAny(valueType) }.toMap() as T - ) - } else { - Result.failure(IllegalArgumentException("Unsupported map type for deserialization: $type")) - } - } - - typeOf().isSupertypeOf(type) -> { - Result.success(this.intoAny(type) as T) - } - - else -> Result.failure(IllegalArgumentException("Unsupported type for deserialization: $type")) - } - } - - - fun toByteArray() = bytes - - fun toByte(): Byte { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).get() - } - - fun toShort(): Short { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).short - } - - fun toInt(): Int { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).int - } - - fun toLong(): Long { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).long - } - - fun toFloat(): Float { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).float - } - - fun toDouble(): Double { - return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).double - } - - override fun toString() = bytes.decodeToString() - - override fun into(): ZBytes = this - - override fun equals(other: Any?) = other is ZBytes && bytes.contentEquals(other.bytes) - - override fun hashCode() = bytes.contentHashCode() -} - -fun Number.into(): ZBytes { - return ZBytes.from(this) -} - -fun String.into(): ZBytes { - return ZBytes.from(this) -} - -fun ByteArray.into(): ZBytes { - return ZBytes(this) -} - -@Throws(Exception::class) -internal fun Any?.into(): ZBytes { - return when (this) { - is String -> this.into() - is Number -> this.into() - is ByteArray -> this.into() - is Serializable -> this.into() - else -> throw IllegalArgumentException("Unsupported serializable type") - } -} - -@Throws(Exception::class) -internal fun ZBytes.intoAny(type: KType): Any { - return when (type) { - typeOf() -> this.toString() - typeOf() -> this.toByte() - typeOf() -> this.toShort() - typeOf() -> this.toInt() - typeOf() -> this.toLong() - typeOf() -> this.toFloat() - typeOf() -> this.toDouble() - typeOf() -> this.toByteArray() - typeOf() -> this - else -> { - when { - typeOf().isSupertypeOf(type) -> { - val companion = type.jvmErasure.companionObject - val function = companion?.declaredMemberFunctions?.find { it.name == "from" } - if (function != null) { - val result = function.call(type.jvmErasure.companionObjectInstance, this) - if (result != null) { - return result - } else { - throw Exception("The 'from' method returned null for the type '$type'.") - } - } else { - throw Exception("Implementation of 'from' method from the ${Deserializable.From::class} interface not found on element of type '$type'.") - } - } - - else -> throw IllegalArgumentException("Unsupported type '$type' for deserialization.") - } - - } - } -} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt index 5406429e4..ea7785b8e 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt @@ -14,17 +14,90 @@ package io.zenoh.publication +import io.zenoh.Resolvable +import io.zenoh.prelude.CongestionControl +import io.zenoh.prelude.Priority +import io.zenoh.Session import io.zenoh.keyexpr.KeyExpr import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes /** - * Delete operation. + * Delete operation to perform on Zenoh on a key expression. * - * @property keyExpr The [KeyExpr] for the delete operation. - * @property qos The [QoS] configuration. - * @property attachment Optional attachment. + * Example: + * ```kotlin + * Session.open().onSuccess { session -> + * session.use { + * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> + * session.delete(keyExpr) + * .res() + * .onSuccess { + * println("Performed a delete on $keyExpr") + * } + * } + * } + * } + * ``` + * + * A delete operation is a special case of a Put operation, it is analogous to perform a Put with an empty value and + * specifying the sample kind to be `DELETE`. */ -internal class Delete ( - val keyExpr: KeyExpr, val qos: QoS, val attachment: ZBytes? -) +class Delete private constructor( + val keyExpr: KeyExpr, val qos: QoS, val attachment: ByteArray? +) { + + companion object { + /** + * Creates a new [Builder] associated with the specified [session] and [keyExpr]. + * + * @param session The [Session] from which the Delete will be performed. + * @param keyExpr The [KeyExpr] upon which the Delete will be performed. + * @return A [Delete] operation [Builder]. + */ + fun newBuilder(session: Session, keyExpr: KeyExpr): Builder { + return Builder(session, keyExpr) + } + } + + /** + * Builder to construct a [Delete] operation. + * + * @property session The [Session] from which the Delete will be performed + * @property keyExpr The [KeyExpr] from which the Delete will be performed + * @constructor Create a [Delete] builder. + */ + class Builder internal constructor( + val session: Session, + val keyExpr: KeyExpr, + ) : Resolvable { + + private var qosBuilder: QoS.Builder = QoS.Builder() + private var attachment: ByteArray? = null + + /** Change the [CongestionControl] to apply when routing the data. */ + fun congestionControl(congestionControl: CongestionControl) = + apply { this.qosBuilder.congestionControl(congestionControl) } + + /** Change the [Priority] of the written data. */ + fun priority(priority: Priority) = apply { this.qosBuilder.priority(priority) } + + /** + * Sets the express flag. If true, the reply won't be batched in order to reduce the latency. + */ + fun express(isExpress: Boolean) = apply { this.qosBuilder.express(isExpress) } + + /** Set an attachment to the put operation. */ + fun withAttachment(attachment: ByteArray) = apply { this.attachment = attachment } + + /** + * Performs a DELETE operation on the specified [keyExpr]. + * + * A successful [Result] only states the Delete request was properly sent through the network, it doesn't mean it + * was properly executed remotely. + */ + override fun res(): Result = runCatching { + val delete = Delete(this.keyExpr, qosBuilder.build(), attachment) + session.resolveDelete(keyExpr, delete) + } + } +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt index c786d547a..2aee4315d 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt @@ -21,7 +21,6 @@ import io.zenoh.keyexpr.KeyExpr import io.zenoh.prelude.Priority import io.zenoh.prelude.CongestionControl import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** @@ -36,18 +35,23 @@ import io.zenoh.value.Value * which we can specify the [Priority], and the [CongestionControl]. * * Example: - * ```kotlin + * ``` * val keyExpr = "demo/kotlin/greeting" * Session.open().onSuccess { * it.use { session -> * session * .declarePublisher(keyExpr) + * .priority(Priority.REALTIME) + * .congestionControl(CongestionControl.DROP) + * .res() * .onSuccess { pub -> - * var i = 0 - * while (true) { - * pub.put("Hello for the ${i}th time!") - * Thread.sleep(1000) - * i++ + * pub.use { + * var i = 0 + * while (true) { + * pub.put("Hello for the ${i}th time!").res() + * Thread.sleep(1000) + * i++ + * } * } * } * } @@ -69,7 +73,7 @@ import io.zenoh.value.Value */ class Publisher internal constructor( val keyExpr: KeyExpr, - val qos: QoS, + private var qos: QoS, private var jniPublisher: JNIPublisher?, ) : SessionDeclaration, AutoCloseable { @@ -77,21 +81,28 @@ class Publisher internal constructor( private val InvalidPublisherResult = Result.failure(SessionException("Publisher is not valid.")) } - val congestionControl = qos.congestionControl - val priority = qos.priority - val express = qos.express - /** Performs a PUT operation on the specified [keyExpr] with the specified [value]. */ - fun put(value: Value, attachment: ZBytes? = null) = jniPublisher?.put(value, attachment) ?: InvalidPublisherResult - + fun put(value: Value) = Put(jniPublisher, value) /** Performs a PUT operation on the specified [keyExpr] with the specified string [value]. */ - fun put(value: String, attachment: ZBytes? = null) = jniPublisher?.put(Value(value), attachment) ?: InvalidPublisherResult + fun put(value: String) = Put(jniPublisher, Value(value)) /** * Performs a DELETE operation on the specified [keyExpr] + * + * @return A [Resolvable] operation. */ - fun delete(attachment: ZBytes? = null) = jniPublisher?.delete(attachment) ?: InvalidPublisherResult + fun delete() = Delete(jniPublisher) + + /** Get congestion control policy. */ + fun getCongestionControl(): CongestionControl { + return qos.congestionControl() + } + + /** Get priority policy. */ + fun getPriority(): Priority { + return qos.priority() + } fun isValid(): Boolean { return jniPublisher != null @@ -105,5 +116,60 @@ class Publisher internal constructor( jniPublisher?.close() jniPublisher = null } + + class Put internal constructor( + private var jniPublisher: JNIPublisher?, + val value: Value, + var attachment: ByteArray? = null + ) : Resolvable { + + fun withAttachment(attachment: ByteArray) = apply { this.attachment = attachment } + + override fun res(): Result = run { + jniPublisher?.put(value, attachment) ?: InvalidPublisherResult + } + } + + class Delete internal constructor( + private var jniPublisher: JNIPublisher?, + var attachment: ByteArray? = null + ) : Resolvable { + + fun withAttachment(attachment: ByteArray) = apply { this.attachment = attachment } + + override fun res(): Result = run { + jniPublisher?.delete(attachment) ?: InvalidPublisherResult + } + } + + /** + * Publisher Builder. + * + * @property session The [Session] from which the publisher is declared. + * @property keyExpr The key expression the publisher will be associated to. + * @constructor Create empty Builder. + */ + class Builder internal constructor( + internal val session: Session, + internal val keyExpr: KeyExpr, + ) { + private var qosBuilder: QoS.Builder = QoS.Builder() + + /** Change the [CongestionControl] to apply when routing the data. */ + fun congestionControl(congestionControl: CongestionControl) = + apply { this.qosBuilder.congestionControl(congestionControl) } + + /** Change the [Priority] of the written data. */ + fun priority(priority: Priority) = apply { this.qosBuilder.priority(priority) } + + /** + * Sets the express flag. If true, the reply won't be batched in order to reduce the latency. + */ + fun express(isExpress: Boolean) = apply { this.qosBuilder.express(isExpress) } + + fun res(): Result { + return session.run { resolvePublisher(keyExpr, qosBuilder.build()) } + } + } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt index 93c7340a7..a0339b909 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt @@ -14,22 +14,100 @@ package io.zenoh.publication +import io.zenoh.Resolvable +import io.zenoh.Session import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes +import io.zenoh.prelude.* import io.zenoh.value.Value /** * Put operation. * + * A put puts a [io.zenoh.sample.Sample] into the specified key expression. + * + * Example: + * ```kotlin + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> + * session.put(keyExpr, "Hello") + * .congestionControl(CongestionControl.BLOCK) + * .priority(Priority.REALTIME) + * .res() + * .onSuccess { println("Put 'Hello' on $keyExpr.") } + * }} + * } + * ``` + * + * This class is an open class for the sake of the [Delete] operation, which is a special case of [Put] operation. + * * @property keyExpr The [KeyExpr] to which the put operation will be performed. * @property value The [Value] to put. * @property qos The [QoS] configuration. * @property attachment An optional user attachment. */ -internal data class Put ( +class Put private constructor( val keyExpr: KeyExpr, val value: Value, val qos: QoS, - val attachment: ZBytes? -) + val attachment: ByteArray? +) { + + companion object { + + /** + * Creates a bew [Builder] associated to the specified [session] and [keyExpr]. + * + * @param session The [Session] from which the put will be performed. + * @param keyExpr The [KeyExpr] upon which the put will be performed. + * @param value The [Value] to put. + * @return A [Put] operation [Builder]. + */ + internal fun newBuilder(session: Session, keyExpr: KeyExpr, value: Value): Builder { + return Builder(session, keyExpr, value) + } + } + + /** + * Builder to construct a [Put] operation. + * + * @property session The [Session] from which the put operation will be performed. + * @property keyExpr The [KeyExpr] upon which the put operation will be performed. + * @property value The [Value] to put. + * @constructor Create a [Put] builder. + */ + class Builder internal constructor( + private val session: Session, + private val keyExpr: KeyExpr, + private var value: Value, + ): Resolvable { + + private var qosBuilder: QoS.Builder = QoS.Builder() + private var attachment: ByteArray? = null + + /** Change the [Encoding] of the written data. */ + fun encoding(encoding: Encoding) = apply { + this.value = Value(value.payload, encoding) + } + + /** Change the [CongestionControl] to apply when routing the data. */ + fun congestionControl(congestionControl: CongestionControl) = + apply { this.qosBuilder.congestionControl(congestionControl) } + + /** Change the [Priority] of the written data. */ + fun priority(priority: Priority) = apply { this.qosBuilder.priority(priority) } + + /** + * Sets the express flag. If true, the reply won't be batched in order to reduce the latency. + */ + fun express(isExpress: Boolean) = apply { this.qosBuilder.express(isExpress) } + + /** Set an attachment to the put operation. */ + fun withAttachment(attachment: ByteArray) = apply { this.attachment = attachment } + + /** Resolves the put operation, returning a [Result]. */ + override fun res(): Result = runCatching { + val put = Put(keyExpr, value, qosBuilder.build(), attachment) + session.run { resolvePut(keyExpr, put) } + } + } +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt new file mode 100644 index 000000000..c0eacb1d0 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt @@ -0,0 +1,195 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.query + +import io.zenoh.handlers.Callback +import io.zenoh.Session +import io.zenoh.handlers.ChannelHandler +import io.zenoh.handlers.Handler +import io.zenoh.selector.Selector +import io.zenoh.value.Value +import kotlinx.coroutines.channels.Channel +import java.time.Duration + +/** + * Get to query data from the matching queryables in the system. + * + * Example with a [Callback]: + * ``` + * println("Opening Session") + * Session.open().onSuccess { session -> session.use { + * "demo/kotlin/example".intoSelector().onSuccess { selector -> + * session.get(selector) + * .consolidation(ConsolidationMode.NONE) + * .target(QueryTarget.BEST_MATCHING) + * .withValue("Get value example") + * .with { reply -> println("Received reply $reply") } + * .timeout(Duration.ofMillis(1000)) + * .res() + * .onSuccess {...} + * } + * } + * } + * ``` + * + * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. + */ +class Get private constructor() { + + companion object { + /** + * Creates a bew [Builder] associated to the specified [session] and [keyExpr]. + * + * @param session The [Session] from which the query will be triggered. + * @param selector The [Selector] with which the query will be performed. + * @return A [Builder] with a default [ChannelHandler] to handle any incoming [Reply]. + */ + fun newBuilder(session: Session, selector: Selector): Builder> { + return Builder(session, selector, handler = ChannelHandler(Channel())) + } + } + + /** + * Builder to construct a [Get]. + * + * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, + * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the + * last one specified. + * + * @param R The receiver type of the [Handler] implementation, defaults to [Unit] when no handler is specified. + * @property session The [Session] from which the query will be performed. + * @property selector The [Selector] with which the get query will be performed. + * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this + * builder should be obtained through the [Session] after calling [Session.get]. + */ + class Builder internal constructor( + private val session: Session, + private val selector: Selector, + private var callback: Callback? = null, + private var handler: Handler? = null, + ) { + + private var timeout = Duration.ofMillis(10000) + private var target: QueryTarget = QueryTarget.BEST_MATCHING + private var consolidation: ConsolidationMode = ConsolidationMode.default() + private var value: Value? = null + private var attachment: ByteArray? = null + private var onClose: (() -> Unit)? = null + + private constructor(other: Builder<*>, handler: Handler?) : this(other.session, other.selector) { + this.handler = handler + copyParams(other) + } + + private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.selector) { + this.callback = callback + copyParams(other) + } + + private fun copyParams(other: Builder<*>) { + this.timeout = other.timeout + this.target = other.target + this.consolidation = other.consolidation + this.value = other.value + this.attachment = other.attachment + this.onClose = other.onClose + } + + /** Specify the [QueryTarget]. */ + fun target(target: QueryTarget): Builder { + this.target = target + return this + } + + /** Specify the [ConsolidationMode]. */ + fun consolidation(consolidation: ConsolidationMode): Builder { + this.consolidation = consolidation + return this + } + + /** Specify the timeout. */ + fun timeout(timeout: Duration): Builder { + this.timeout = timeout + return this + } + + /** + * Specify a string value. A [Value] is generated with the provided message, therefore + * this method is equivalent to calling `withValue(Value(message))`. + */ + fun withValue(message: String): Builder { + this.value = Value(message) + return this + } + + /** Specify a [Value]. */ + fun withValue(value: Value): Builder { + this.value = value + return this + } + + /** Specify an attachment. */ + fun withAttachment(attachment: ByteArray): Builder { + this.attachment = attachment + return this + } + + /** + * Specify an action to be invoked when the Get operation is over. + * + * Zenoh will trigger ths specified action once no more replies are to be expected. + */ + fun onClose(action: () -> Unit): Builder { + this.onClose = action + return this + } + + /** Specify a [Callback]. Overrides any previously specified callback or handler. */ + fun with(callback: Callback): Builder = Builder(this, callback) + + /** Specify a [Handler]. Overrides any previously specified callback or handler. */ + fun with(handler: Handler): Builder = Builder(this, handler) + + /** Specify a [Channel]. Overrides any previously specified callback or handler. */ + fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) + + /** + * Resolve the builder triggering the query. + * + * @return A [Result] with the receiver [R] from the specified [Handler] (if specified). + */ + fun res(): Result = runCatching { + require(callback != null || handler != null) { "Either a callback or a handler must be provided." } + val resolvedCallback = callback ?: Callback { t: Reply -> handler?.handle(t) } + val resolvedOnClose = fun() { + onClose?.invoke() + handler?.onClose() + } + return session.run { + resolveGet( + selector, + resolvedCallback, + resolvedOnClose, + handler?.receiver(), + timeout, + target, + consolidation, + value, + attachment + ) + } + } + } +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt index 00c39661d..e1c9f0c8f 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt @@ -14,51 +14,99 @@ package io.zenoh.query +import io.zenoh.Resolvable import io.zenoh.ZenohType import io.zenoh.sample.Sample +import io.zenoh.prelude.SampleKind import io.zenoh.value.Value import io.zenoh.keyexpr.KeyExpr +import io.zenoh.prelude.CongestionControl +import io.zenoh.prelude.Priority import io.zenoh.prelude.QoS -import io.zenoh.protocol.ZBytes import io.zenoh.protocol.ZenohID import io.zenoh.queryable.Query import org.apache.commons.net.ntp.TimeStamp /** - * Class to represent a Zenoh Reply to a get query and to a remote [Query]. + * Class to represent a Zenoh Reply to a [Get] operation and to a remote [Query]. * - * A reply can be either successful ([Success]), an error ([Error]) or a delete request ([Delete]), both having different - * information. - * For instance, the successful reply will contain a [Sample] while the error reply will only contain a [Value] - * with the error information. + * A reply can be either successful ([Success]) or an error ([Error]), both having different information. For instance, + * the successful reply will contain a [Sample] while the error reply will only contain a [Value] with the error information. + * + * Replies can either be automatically created when receiving a remote reply after performing a [Get] (in which case the + * [replierId] shows the id of the replier) or created through the builders while answering to a remote [Query] (in that + * case the replier ID is automatically added by Zenoh). + * + * Generating a reply only makes sense within the context of a [Query], therefore builders below are meant to only + * be accessible from [Query.reply]. * * Example: * ```kotlin - * Session.open(config).onSuccess { session -> - * session.use { - * key.intoKeyExpr().onSuccess { keyExpr -> - * session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> - * runBlocking { - * for (query in queryable.receiver) { - * val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" - * println(">> [Queryable] Received Query '${query.selector}' $valueInfo") - * query.replySuccess( - * keyExpr, - * value = Value("Example value"), - * timestamp = TimeStamp.getCurrentTime() - * ).getOrThrow() - * } - * } - * } - * } - * } - * } + * session.declareQueryable(keyExpr).with { query -> + * query.reply(keyExpr) + * .success(Value("Hello")) + * .withTimeStamp(TimeStamp(Date.from(Instant.now()))) + * .res() + * }.res() + * ... * ``` * - * @property replierId: unique ID identifying the replier, may be null in case the network cannot provide it - * (@see https://github.com/eclipse-zenoh/zenoh/issues/709#issuecomment-2202763630). + * **IMPORTANT: Error replies are not yet fully supported by Zenoh, but the code for the error replies below has been + * added for the sake of future compatibility.** + * + * @property replierId: unique ID identifying the replier. */ -sealed class Reply private constructor(open val replierId: ZenohID?) : ZenohType { +sealed class Reply private constructor(val replierId: ZenohID?) : ZenohType { + + /** + * Builder to construct a [Reply]. + * + * This builder allows you to construct [Success] and [Error] replies. + * + * @property query The received [Query] to reply to. + * @property keyExpr The [KeyExpr] from the queryable, which is at least an intersection of the query's key expression. + * @constructor Create empty Builder + */ + class Builder internal constructor(val query: Query, val keyExpr: KeyExpr) { + + /** + * Returns a [Success.Builder] with the provided [value]. + * + * @param value The [Value] of the reply. + */ + fun success(value: Value) = Success.Builder(query, keyExpr, value) + + /** + * Returns a [Success.Builder] with a [Value] containing the provided [message]. + * + * It is equivalent to calling `success(Value(message))`. + * + * @param message A string message for the reply. + */ + fun success(message: String) = success(Value(message)) + + /** + * Returns an [Error.Builder] with the provided [value]. + * + * @param value The [Value] of the error reply. + */ + fun error(value: Value) = Error.Builder(query, value) + + /** + * Returns an [Error.Builder] with a [Value] containing the provided [message]. + * + * It is equivalent to calling `error(Value(message))`. + * + * @param message A string message for the error reply. + */ + fun error(message: String) = error(Value(message)) + + /** + * Returns a [Delete.Builder]. + */ + fun delete() = Delete.Builder(query, keyExpr) + + } /** * A successful [Reply]. @@ -69,45 +117,175 @@ sealed class Reply private constructor(open val replierId: ZenohID?) : ZenohType * * @param replierId The replierId of the remotely generated reply. */ - data class Success internal constructor(override val replierId: ZenohID? = null, val sample: Sample) : Reply(replierId) { + class Success internal constructor(replierId: ZenohID?, val sample: Sample) : Reply(replierId) { + + /** + * Builder for the [Success] reply. + * + * @property query The [Query] to reply to. + * @property keyExpr The [KeyExpr] of the queryable. + * @property value The [Value] with the reply information. + */ + class Builder internal constructor(val query: Query, val keyExpr: KeyExpr, val value: Value) : + Resolvable { + + private val kind = SampleKind.PUT + private var timeStamp: TimeStamp? = null + private var attachment: ByteArray? = null + private var qosBuilder = QoS.Builder() + + /** + * Sets the [TimeStamp] of the replied [Sample]. + */ + fun timestamp(timeStamp: TimeStamp) = apply { this.timeStamp = timeStamp } + + /** + * Appends an attachment to the reply. + */ + fun attachment(attachment: ByteArray) = apply { this.attachment = attachment } + + /** + * Sets the express flag. If true, the reply won't be batched in order to reduce the latency. + */ + fun express(express: Boolean) = apply { qosBuilder.express(express) } + + /** + * Sets the [Priority] of the reply. + */ + fun priority(priority: Priority) = apply { qosBuilder.priority(priority) } + + /** + * Sets the [CongestionControl] of the reply. + * + * @param congestionControl + */ + fun congestionControl(congestionControl: CongestionControl) = + apply { qosBuilder.congestionControl(congestionControl) } + + /** + * Constructs the reply sample with the provided parameters and triggers the reply to the query. + */ + override fun res(): Result { + val sample = Sample(keyExpr, value, kind, timeStamp, qosBuilder.build(), attachment) + return query.reply(Success(null, sample)).res() + } + } override fun toString(): String { return "Success(sample=$sample)" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Success) return false + + return sample == other.sample + } + + override fun hashCode(): Int { + return sample.hashCode() + } } /** * An Error reply. * - * @property error: value with the error information.* + * @property error: value with the error information. + * @constructor The constructor is private since reply instances are created through JNI when receiving a reply to a query. + * * @param replierId: unique ID identifying the replier. */ - data class Error internal constructor(override val replierId: ZenohID? = null, val error: Value) : Reply(replierId) { + class Error internal constructor(replierId: ZenohID?, val error: Value) : Reply(replierId) { + + /** + * Builder for the [Error] reply. + * + * @property query The [Query] to reply to. + * @property value The [Value] with the reply information. + */ + class Builder internal constructor(val query: Query, val value: Value) : Resolvable { + + /** + * Triggers the error reply. + */ + override fun res(): Result { + return query.reply(Error(null, value)).res() + } + } override fun toString(): String { return "Error(error=$error)" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Error) return false + + return error == other.error + } + + override fun hashCode(): Int { + return error.hashCode() + } } /** * A Delete reply. * - * @property replierId Unique ID identifying the replier. - * @property keyExpr Key expression to reply to. This parameter must not be necessarily the same - * as the key expression from the Query, however it must intersect with the query key. - * @property attachment Optional attachment for the delete reply. - * @property qos QoS for the reply. + * @property keyExpr + * @constructor + * + * @param replierId */ - data class Delete internal constructor( - override val replierId: ZenohID? = null, + class Delete internal constructor( + replierId: ZenohID?, val keyExpr: KeyExpr, val timestamp: TimeStamp?, - val attachment: ZBytes?, + val attachment: ByteArray?, val qos: QoS ) : Reply(replierId) { - override fun toString(): String { - return "Delete(keyexpr=$keyExpr)" + class Builder internal constructor(val query: Query, val keyExpr: KeyExpr) : Resolvable { + + private val kind = SampleKind.DELETE + private var timeStamp: TimeStamp? = null + private var attachment: ByteArray? = null + private var qosBuilder = QoS.Builder() + + /** + * Sets the [TimeStamp] of the replied [Sample]. + */ + fun timestamp(timeStamp: TimeStamp) = apply { this.timeStamp = timeStamp } + + /** + * Appends an attachment to the reply. + */ + fun attachment(attachment: ByteArray) = apply { this.attachment = attachment } + + /** + * Sets the express flag. If true, the reply won't be batched in order to reduce the latency. + */ + fun express(express: Boolean) = apply { qosBuilder.express(express) } + + /** + * Sets the [Priority] of the reply. + */ + fun priority(priority: Priority) = apply { qosBuilder.priority(priority) } + + /** + * Sets the [CongestionControl] of the reply. + * + * @param congestionControl + */ + fun congestionControl(congestionControl: CongestionControl) = + apply { qosBuilder.congestionControl(congestionControl) } + + /** + * Triggers the delete reply. + */ + override fun res(): Result { + return query.reply(Delete(null, keyExpr, timeStamp, attachment, qosBuilder.build())).res() + } } } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt index 72322ee3f..01ae9b6fa 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt @@ -14,18 +14,14 @@ package io.zenoh.queryable +import io.zenoh.Resolvable import io.zenoh.ZenohType import io.zenoh.selector.Selector +import io.zenoh.value.Value import io.zenoh.exceptions.SessionException import io.zenoh.jni.JNIQuery import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.Encoding -import io.zenoh.prelude.QoS -import io.zenoh.prelude.SampleKind -import io.zenoh.protocol.ZBytes -import io.zenoh.sample.Sample -import io.zenoh.value.Value -import org.apache.commons.net.ntp.TimeStamp +import io.zenoh.query.Reply /** * Represents a Zenoh Query in Kotlin. @@ -44,85 +40,58 @@ class Query internal constructor( val keyExpr: KeyExpr, val selector: Selector, val value: Value?, - val attachment: ZBytes?, + val attachment: ByteArray?, private var jniQuery: JNIQuery? ) : AutoCloseable, ZenohType { /** Shortcut to the [selector]'s parameters. */ val parameters = selector.parameters - /** Payload of the query. */ - val payload: ZBytes? = value?.payload - - /** Encoding of the payload. */ - val encoding: Encoding? = value?.encoding - /** - * Reply success to the remote [Query]. - * - * A query can not be replied more than once. After the reply is performed, the query is considered - * to be no more valid and further attempts to reply to it will fail. + * Reply to the specified key expression. * * @param keyExpr Key expression to reply to. This parameter must not be necessarily the same * as the key expression from the Query, however it must intersect with the query key. - * @param value The [Value] with the reply information. - * @param qos The [QoS] for the reply. - * @param timestamp Optional timestamp for the reply. - * @param attachment Optional attachment for the reply. + * @return a [Reply.Builder] */ - fun replySuccess( - keyExpr: KeyExpr, - value: Value, - qos: QoS = QoS.default(), - timestamp: TimeStamp? = null, - attachment: ZBytes? = null - ): Result { - val sample = Sample(keyExpr, value, SampleKind.PUT, timestamp, qos, attachment) - return jniQuery?.replySuccess(sample) ?: Result.failure(SessionException("Query is invalid")) + fun reply(keyExpr: KeyExpr) = Reply.Builder(this, keyExpr) + + override fun close() { + jniQuery?.apply { + this.close() + jniQuery = null + } } - /** - * Reply error to the remote [Query]. - * - * A query can not be replied more than once. After the reply is performed, the query is considered - * to be no more valid and further attempts to reply to it will fail. - * - * @param error [Value] with the error information. - */ - fun replyError(error: Value): Result { - return jniQuery?.replyError(error) ?: Result.failure(SessionException("Query is invalid")) + protected fun finalize() { + close() } /** - * Perform a delete reply operation to the remote [Query]. + * Perform a reply operation to the remote [Query]. * * A query can not be replied more than once. After the reply is performed, the query is considered * to be no more valid and further attempts to reply to it will fail. * - * @param keyExpr Key expression to reply to. This parameter must not be necessarily the same - * as the key expression from the Query, however it must intersect with the query key. - * @param qos The [QoS] for the reply. - * @param timestamp Optional timestamp for the reply. - * @param attachment Optional attachment for the reply. + * @param reply The [Reply] to the Query. + * @return A [Resolvable] that returns a [Result] with the status of the reply operation. */ - fun replyDelete( - keyExpr: KeyExpr, - qos: QoS = QoS.default(), - timestamp: TimeStamp? = null, - attachment: ZBytes? = null - ): Result { - return jniQuery?.replyDelete(keyExpr, timestamp, attachment, qos) - ?: Result.failure(SessionException("Query is invalid")) - } - - override fun close() { + internal fun reply(reply: Reply): Resolvable = Resolvable { jniQuery?.apply { - this.close() + val result: Result = when (reply) { + is Reply.Success -> { + replySuccess(reply.sample) + } + is Reply.Error -> { + replyError(reply.error) + } + is Reply.Delete -> { + replyDelete(reply.keyExpr, reply.timestamp, reply.attachment, reply.qos) + } + } jniQuery = null + return@Resolvable result } - } - - protected fun finalize() { - close() + return@Resolvable Result.failure(SessionException("Query is invalid")) } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt index f0af904dd..5e07f0900 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt @@ -15,6 +15,8 @@ package io.zenoh.queryable import io.zenoh.* +import io.zenoh.handlers.Callback +import io.zenoh.handlers.ChannelHandler import io.zenoh.handlers.Handler import io.zenoh.jni.JNIQueryable import io.zenoh.keyexpr.KeyExpr @@ -32,7 +34,7 @@ import kotlinx.coroutines.channels.Channel * Session.open().onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * session.declareQueryable(keyExpr).wait().onSuccess { queryable -> + * session.declareQueryable(keyExpr).res().onSuccess { queryable -> * queryable.use { * it.receiver?.let { receiverChannel -> * runBlocking { @@ -44,7 +46,7 @@ import kotlinx.coroutines.channels.Channel * .success("Hello!") * .withKind(SampleKind.PUT) * .withTimeStamp(TimeStamp.getCurrentTime()) - * .wait() + * .res() * .onSuccess { println("Replied hello.") } * .onFailure { println(it) } * } @@ -66,10 +68,11 @@ import kotlinx.coroutines.channels.Channel * @property keyExpr The [KeyExpr] to which the subscriber is associated. * @property receiver Optional [R] that is provided when specifying a [Handler] for the subscriber. * @property jniQueryable Delegate object in charge of communicating with the underlying native code. - * @constructor Internal constructor. + * @constructor Internal constructor. Instances of Queryable must be created through the [Builder] obtained after + * calling [Session.declareQueryable] or alternatively through [newBuilder]. */ class Queryable internal constructor( - val keyExpr: KeyExpr, val receiver: R, private var jniQueryable: JNIQueryable? + val keyExpr: KeyExpr, val receiver: R?, private var jniQueryable: JNIQueryable? ) : AutoCloseable, SessionDeclaration { fun isValid(): Boolean { @@ -84,5 +87,97 @@ class Queryable internal constructor( override fun close() { undeclare() } + + companion object { + + /** + * Creates a new [Builder] associated to the specified [session] and [keyExpr]. + * + * @param session The [Session] from which the queryable will be declared. + * @param keyExpr The [KeyExpr] associated to the queryable. + * @return An empty [Builder] with a default [ChannelHandler] to handle the incoming samples. + */ + fun newBuilder(session: Session, keyExpr: KeyExpr): Builder> { + return Builder(session, keyExpr, handler = ChannelHandler(Channel())) + } + } + + /** + * Builder to construct a [Queryable]. + * + * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, + * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the + * last one specified. + * + * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. + * @property session [Session] to which the [Queryable] will be bound to. + * @property keyExpr The [KeyExpr] to which the queryable is associated. + * @property callback Optional callback that will be triggered upon receiving a [Query]. + * @property handler Optional handler to receive the incoming queries. + * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this + * builder should be obtained through the [Session] after calling [Session.declareQueryable]. + */ + class Builder internal constructor( + private val session: Session, + private val keyExpr: KeyExpr, + private var callback: Callback? = null, + private var handler: Handler? = null + ) : Resolvable> { + private var complete: Boolean = false + private var onClose: (() -> Unit)? = null + + private constructor(other: Builder<*>, handler: Handler?) : this(other.session, other.keyExpr) { + this.handler = handler + this.complete = other.complete + this.onClose = other.onClose + } + + private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.keyExpr) { + this.callback = callback + this.complete = other.complete + this.onClose = other.onClose + } + + /** Change queryable completeness. */ + fun complete(complete: Boolean) = apply { this.complete = complete } + + /** Specify an action to be invoked when the [Queryable] is undeclared. */ + fun onClose(action: () -> Unit): Builder { + this.onClose = action + return this + } + + /** Specify a [Callback]. Overrides any previously specified callback or handler. */ + fun with(callback: Callback): Builder = Builder(this, callback) + + /** Specify a [Handler]. Overrides any previously specified callback or handler. */ + fun with(handler: Handler): Builder = Builder(this, handler) + + /** Specify a [Channel]. Overrides any previously specified callback or handler. */ + fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) + + /** + * Resolve the builder, creating a [Queryable] with the provided parameters. + * + * @return A [Result] with the newly created [Queryable]. + */ + override fun res(): Result> = runCatching { + require(callback != null || handler != null) { "Either a callback or a handler must be provided." } + val resolvedCallback = callback ?: Callback { t: Query -> handler?.handle(t) } + val resolvedOnClose = fun() { + handler?.onClose() + onClose?.invoke() + } + return session.run { + resolveQueryable( + keyExpr, + resolvedCallback, + resolvedOnClose, + handler?.receiver(), + complete + ) + } + } + } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt index a69c098e3..e94206782 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt @@ -18,7 +18,6 @@ import io.zenoh.ZenohType import io.zenoh.prelude.SampleKind import io.zenoh.prelude.QoS import io.zenoh.keyexpr.KeyExpr -import io.zenoh.protocol.ZBytes import io.zenoh.value.Value import org.apache.commons.net.ntp.TimeStamp @@ -35,11 +34,35 @@ import org.apache.commons.net.ntp.TimeStamp * @property qos The Quality of Service settings used to deliver the sample. * @property attachment Optional attachment. */ -data class Sample( +class Sample( val keyExpr: KeyExpr, val value: Value, val kind: SampleKind, val timestamp: TimeStamp?, val qos: QoS, - val attachment: ZBytes? = null -): ZenohType + val attachment: ByteArray? = null +): ZenohType { + override fun toString(): String { + return if (kind == SampleKind.DELETE) "$kind($keyExpr)" else "$kind($keyExpr: $value)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sample + + if (keyExpr != other.keyExpr) return false + if (value != other.value) return false + if (kind != other.kind) return false + return timestamp == other.timestamp + } + + override fun hashCode(): Int { + var result = keyExpr.hashCode() + result = 31 * result + value.hashCode() + result = 31 * result + kind.hashCode() + result = 31 * result + (timestamp?.hashCode() ?: 0) + return result + } +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt index 78d71b11a..ff637cb73 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt @@ -28,28 +28,23 @@ import java.net.URLDecoder * @property keyExpr The [KeyExpr] of the selector. * @property parameters The parameters of the selector. */ -class Selector(val keyExpr: KeyExpr, val parameters: String? = null) : AutoCloseable { +class Selector(val keyExpr: KeyExpr, val parameters: String = "") : AutoCloseable { - /** - * If the [parameters] argument is defined, this function extracts its name-value pairs into a map, - * returning an error in case of duplicated parameters. - */ - fun parametersStringMap(): Result>? { - return parameters?.let { - it.split('&').fold>(mapOf()) { parametersMap, parameter -> - val keyValuePair = parameter.split('=') - val key = keyValuePair[0] - if (parametersMap.containsKey(key)) { - throw IllegalArgumentException("Duplicated parameter `$key` detected.") - } - val value = keyValuePair.getOrNull(1)?.let { URLDecoder.decode(it, Charsets.UTF_8.name()) } ?: "" - parametersMap + (key to value) - }.let { map -> Result.success(map) } + /** Extracts the selector [parameters]' name-value pairs into a map, returning an error in case of duplicated parameters. */ + fun parametersStringMap(): Result> = runCatching { + parameters.split('&').fold(mapOf()) { parametersMap, parameter -> + val keyValuePair = parameter.split('=') + val key = keyValuePair[0] + if (parametersMap.containsKey(key)) { + throw IllegalArgumentException("Duplicated parameter `$key` detected.") + } + val value = keyValuePair.getOrNull(1)?.let { URLDecoder.decode(it, Charsets.UTF_8.name()) } ?: "" + parametersMap + (key to value) } } override fun toString(): String { - return parameters?.let { "$keyExpr?$parameters" } ?: keyExpr.toString() + return if (parameters.isEmpty()) "$keyExpr" else "$keyExpr?$parameters" } /** Closes the selector's [KeyExpr]. */ diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt index d675758eb..bc47f36b7 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt @@ -32,7 +32,7 @@ enum class Reliability { * * Informs the network that this subscriber wishes for all publications to reliably reach it. * - * Note that if a publisher puts a sample with the [io.zenoh.prelude.CongestionControl.DROP] option, + * Note that if a publisher puts a sample with the [io.zenoh.publication.CongestionControl.DROP] option, * this reliability requirement may be infringed to prevent slow readers from blocking the network. */ RELIABLE, diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt index 997dc97b9..6670c38dc 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt @@ -15,9 +15,13 @@ package io.zenoh.subscriber import io.zenoh.* +import io.zenoh.handlers.Callback +import io.zenoh.handlers.ChannelHandler import io.zenoh.handlers.Handler +import io.zenoh.subscriber.Subscriber.Builder import io.zenoh.jni.JNISubscriber import io.zenoh.keyexpr.KeyExpr +import io.zenoh.sample.Sample import kotlinx.coroutines.channels.Channel /** @@ -30,7 +34,28 @@ import kotlinx.coroutines.channels.Channel * Example using the default [Channel] handler: * * ```kotlin - * TODO: add exmaple, update comment. + * Session.open().onSuccess { session -> + * session.use { + * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> + * session.declareSubscriber(keyExpr) + * .bestEffort() + * .res() + * .onSuccess { subscriber -> + * subscriber.use { + * println("Declared subscriber on $keyExpr.") + * runBlocking { + * val receiver = subscriber.receiver!! + * val iterator = receiver.iterator() + * while (iterator.hasNext()) { + * val sample = iterator.next() + * println(sample) + * } + * } + * } + * } + * } + * } + * } * ``` * * ## Lifespan @@ -47,7 +72,7 @@ import kotlinx.coroutines.channels.Channel * calling [Session.declareSubscriber] or alternatively through [newBuilder]. */ class Subscriber internal constructor( - val keyExpr: KeyExpr, val receiver: R, private var jniSubscriber: JNISubscriber? + val keyExpr: KeyExpr, val receiver: R?, private var jniSubscriber: JNISubscriber? ) : AutoCloseable, SessionDeclaration { fun isValid(): Boolean { @@ -62,4 +87,102 @@ class Subscriber internal constructor( override fun close() { undeclare() } + + companion object { + + /** + * Creates a new [Builder] associated to the specified [session] and [keyExpr]. + * + * @param session The [Session] from which the subscriber will be declared. + * @param keyExpr The [KeyExpr] associated to the subscriber. + * @return An empty [Builder] with a default [ChannelHandler] to handle the incoming samples. + */ + fun newBuilder(session: Session, keyExpr: KeyExpr): Builder> { + return Builder(session, keyExpr, handler = ChannelHandler(Channel())) + } + } + + /** + * Builder to construct a [Subscriber]. + * + * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, + * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the + * last one specified. + * + * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. + * @property session [Session] to which the [Subscriber] will be bound to. + * @property keyExpr The [KeyExpr] to which the subscriber is associated. + * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this + * builder should be obtained through the [Session] after calling [Session.declareSubscriber]. + */ + class Builder internal constructor( + private val session: Session, + private val keyExpr: KeyExpr, + private var callback: Callback? = null, + private var handler: Handler? = null + ): Resolvable> { + + private var reliability: Reliability = Reliability.BEST_EFFORT + private var onClose: (() -> Unit)? = null + + private constructor(other: Builder<*>, handler: Handler?): this(other.session, other.keyExpr) { + this.handler = handler + copyParams(other) + } + + private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.keyExpr) { + this.callback = callback + copyParams(other) + } + + private fun copyParams(other: Builder<*>) { + this.reliability = other.reliability + this.onClose = other.onClose + } + + /** Sets the [Reliability]. */ + fun reliability(reliability: Reliability): Builder = apply { + this.reliability = reliability + } + + /** Sets the reliability to [Reliability.RELIABLE]. */ + fun reliable(): Builder = apply { + this.reliability = Reliability.RELIABLE + } + + /** Sets the reliability to [Reliability.BEST_EFFORT]. */ + fun bestEffort(): Builder = apply { + this.reliability = Reliability.BEST_EFFORT + } + + /** Specify an action to be invoked when the [Subscriber] is undeclared. */ + fun onClose(action: () -> Unit): Builder { + this.onClose = action + return this + } + + /** Specify a [Callback]. Overrides any previously specified callback or handler. */ + fun with(callback: Callback): Builder = Builder(this, callback) + + /** Specify a [Handler]. Overrides any previously specified callback or handler. */ + fun with(handler: Handler): Builder = Builder(this, handler) + + /** Specify a [Channel]. Overrides any previously specified callback or handler. */ + fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) + + /** + * Resolve the builder, creating a [Subscriber] with the provided parameters. + * + * @return A [Result] with the newly created [Subscriber]. + */ + override fun res(): Result> = runCatching { + require(callback != null || handler != null) { "Either a callback or a handler must be provided." } + val resolvedCallback = callback ?: Callback { t: Sample -> handler?.handle(t) } + val resolvedOnClose = fun() { + handler?.onClose() + onClose?.invoke() + } + return session.run { resolveSubscriber(keyExpr, resolvedCallback, resolvedOnClose, handler?.receiver(), reliability) } + } + } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt index 0b2168dd1..ba787bcbc 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt @@ -15,9 +15,6 @@ package io.zenoh.value import io.zenoh.prelude.Encoding -import io.zenoh.protocol.Serializable -import io.zenoh.protocol.ZBytes -import io.zenoh.protocol.into /** * A Zenoh value. @@ -27,56 +24,17 @@ import io.zenoh.protocol.into * @property payload The payload of this Value. * @property encoding An encoding description indicating how the associated payload is encoded. */ -class Value(val payload: ZBytes, val encoding: Encoding) { +class Value(val payload: ByteArray, val encoding: Encoding) { /** * Constructs a value with the provided message, using [Encoding.ID.TEXT_PLAIN] for encoding. */ - constructor(message: String): this(message.toByteArray().into(), Encoding(Encoding.ID.TEXT_PLAIN)) + constructor(message: String): this(message.toByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)) /** * Constructs a value with the provided message and encoding. */ - constructor(message: String, encoding: Encoding): this(message.toByteArray().into(), encoding) - - /** - * Constructs a value with the provided payload and encoding. - */ - constructor(payload: ByteArray, encoding: Encoding): this(payload.into(), encoding) - - /** - * Constructs a value with the provided payload and encoding. - */ - constructor(payload: Serializable, encoding: Encoding): this(payload.into(), encoding) - - /** - * Constructs a value with the provided message - * - * @param message The message for the value. - * @param encoding The [Encoding.ID] - * @param schema Optional [Encoding.schema] - */ - constructor(message: String, encoding: Encoding.ID, schema: String? = null): this(message.toByteArray().into(), Encoding(encoding, schema)) - - - /** - * Constructs a value with the provided [payload] - * - * @param payload The payload of the value. - * @param encoding The [Encoding.ID] - * @param schema Optional [Encoding.schema] - */ - constructor(payload: ByteArray, encoding: Encoding.ID, schema: String? = null): this(payload.into(), Encoding(encoding, schema)) - - /** - * Constructs a value with the provided [payload] - * - * @param payload The payload of the value. - * @param encoding The [Encoding.ID] - * @param schema Optional [Encoding.schema] - */ - constructor(payload: Serializable, encoding: Encoding.ID, schema: String? = null): this(payload.into(), Encoding(encoding, schema)) - + constructor(message: String, encoding: Encoding): this(message.toByteArray(), encoding) companion object { @@ -86,7 +44,9 @@ class Value(val payload: ZBytes, val encoding: Encoding) { } } - override fun toString(): String = payload.toString() + override fun toString(): String { + return payload.decodeToString() + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -94,13 +54,12 @@ class Value(val payload: ZBytes, val encoding: Encoding) { other as Value - if (payload != other.payload) return false - + if (!payload.contentEquals(other.payload)) return false return encoding == other.encoding } override fun hashCode(): Int { - var result = payload.bytes.hashCode() + var result = payload.contentHashCode() result = 31 * result + encoding.hashCode() return result } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt index 884f64fd5..223008c71 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt @@ -28,8 +28,8 @@ class DeleteTest { val session = Session.open().getOrThrow() var receivedSample: Sample? = null val keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - session.delete(keyExpr) + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + session.delete(keyExpr).res() subscriber.close() session.close() assertNotNull(receivedSample) diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt index 7d3152cbb..cba59dc81 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt @@ -4,7 +4,6 @@ import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding import io.zenoh.query.Reply import io.zenoh.sample.Sample -import io.zenoh.selector.intoSelector import io.zenoh.value.Value import kotlin.test.* @@ -17,11 +16,11 @@ class EncodingTest { // Testing non null schema var receivedSample: Sample? = null - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample - }).getOrThrow() + }.res().getOrThrow() var value = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) - session.put(keyExpr, value) + session.put(keyExpr, value).res() Thread.sleep(200) assertNotNull(receivedSample) @@ -31,7 +30,7 @@ class EncodingTest { // Testing null schema receivedSample = null value = Value("test2", Encoding(Encoding.ID.ZENOH_STRING, null)) - session.put(keyExpr, value) + session.put(keyExpr, value).res() Thread.sleep(200) assertNotNull(receivedSample) @@ -46,25 +45,25 @@ class EncodingTest { fun encoding_replySuccessTest() { val session = Session.open().getOrThrow() val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() - val test1 = "example/testing/reply_success".intoSelector().getOrThrow() - val test2 = "example/testing/reply_success_with_schema".intoSelector().getOrThrow() + val test1 = "example/testing/reply_success".intoKeyExpr().getOrThrow() + val test2 = "example/testing/reply_success_with_schema".intoKeyExpr().getOrThrow() val testValueA = Value("test", Encoding(Encoding.ID.TEXT_CSV, null)) val testValueB = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) - val queryable = session.declareQueryable(keyExpr, callback = { query -> + val queryable = session.declareQueryable(keyExpr).with { query -> when (query.keyExpr) { - test1.keyExpr -> query.replySuccess(query.keyExpr, value = testValueA) - test2.keyExpr -> query.replySuccess(query.keyExpr, value = testValueB) + test1 -> query.reply(query.keyExpr).success(testValueA).res() + test2 -> query.reply(query.keyExpr).success(testValueB).res() } - }).getOrThrow() + }.res().getOrThrow() // Testing with null schema on a reply success scenario. var receivedSample: Sample? = null - session.get(test1, callback = { reply -> + session.get(test1).with { reply -> assertTrue(reply is Reply.Success) receivedSample = reply.sample - }).getOrThrow() + }.res().getOrThrow() Thread.sleep(200) assertNotNull(receivedSample) @@ -73,10 +72,10 @@ class EncodingTest { // Testing with non-null schema on a reply success scenario. receivedSample = null - session.get(test2, callback = { reply -> + session.get(test2).with { reply -> assertTrue(reply is Reply.Success) receivedSample = reply.sample - }).getOrThrow() + }.res().getOrThrow() Thread.sleep(200) assertNotNull(receivedSample) @@ -92,25 +91,25 @@ class EncodingTest { val session = Session.open().getOrThrow() val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() - val test1 = "example/testing/reply_error".intoSelector().getOrThrow() - val test2 = "example/testing/reply_error_with_schema".intoSelector().getOrThrow() + val test1 = "example/testing/reply_error".intoKeyExpr().getOrThrow() + val test2 = "example/testing/reply_error_with_schema".intoKeyExpr().getOrThrow() val testValueA = Value("test", Encoding(Encoding.ID.TEXT_CSV, null)) val testValueB = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) - val queryable = session.declareQueryable(keyExpr, callback = { query -> + val queryable = session.declareQueryable(keyExpr).with { query -> when (query.keyExpr) { - test1.keyExpr -> query.replyError(testValueA) - test2.keyExpr -> query.replyError(testValueB) + test1 -> query.reply(query.keyExpr).error(testValueA).res() + test2 -> query.reply(query.keyExpr).error(testValueB).res() } - }).getOrThrow() + }.res().getOrThrow() // Testing with null schema on a reply error scenario. var errorValue: Value? = null - session.get(test1, callback = { reply -> + session.get(test1).with { reply -> assertTrue(reply is Reply.Error) errorValue = reply.error - }).getOrThrow() + }.res().getOrThrow() Thread.sleep(200) assertNotNull(errorValue) @@ -119,10 +118,10 @@ class EncodingTest { // Testing with non-null schema on a reply error scenario. errorValue = null - session.get(test2, callback = { reply -> + session.get(test2).with { reply -> assertTrue(reply is Reply.Error) errorValue = reply.error - }).getOrThrow() + }.res().getOrThrow() Thread.sleep(200) assertNotNull(errorValue) @@ -136,32 +135,32 @@ class EncodingTest { @Test fun encoding_queryTest() { val session = Session.open().getOrThrow() - val selector = "example/testing/keyexpr".intoSelector().getOrThrow() - val encodingA = Encoding(Encoding.ID.TEXT_CSV, null) - val encodingB = Encoding(Encoding.ID.TEXT_CSV, "test_schema") + val keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() + val testValueA = Value("test", Encoding(Encoding.ID.TEXT_CSV, null)) + val testValueB = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) - var receivedEncoding: Encoding? = null - val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> - receivedEncoding = query.encoding + var receivedValue: Value? = null + val queryable = session.declareQueryable(keyExpr).with { query -> + receivedValue = query.value query.close() - }).getOrThrow() + }.res().getOrThrow() // Testing with null schema - session.get(selector, callback = {}, value = Value("test", encodingA)) + session.get(keyExpr).withValue(testValueA).res() Thread.sleep(200) - assertNotNull(receivedEncoding) - assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) - assertNull(receivedEncoding!!.schema) + assertNotNull(receivedValue) + assertEquals(Encoding.ID.TEXT_CSV, receivedValue!!.encoding.id) + assertNull(receivedValue!!.encoding.schema) // Testing non-null schema - receivedEncoding = null - session.get(selector, callback = {}, value = Value("test", encodingB)) + receivedValue = null + session.get(keyExpr).withValue(testValueB).res() Thread.sleep(200) - assertNotNull(receivedEncoding) - assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) - assertEquals("test_schema", receivedEncoding!!.schema) + assertNotNull(receivedValue) + assertEquals(Encoding.ID.TEXT_CSV, receivedValue!!.encoding.id) + assertEquals("test_schema", receivedValue!!.encoding.schema) queryable.close() session.close() diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt index c975e76d3..42617dbde 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt @@ -15,11 +15,12 @@ package io.zenoh import io.zenoh.handlers.Handler +import io.zenoh.keyexpr.KeyExpr +import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.SampleKind import io.zenoh.query.Reply import io.zenoh.queryable.Queryable import io.zenoh.selector.Selector -import io.zenoh.selector.intoSelector import io.zenoh.value.Value import org.apache.commons.net.ntp.TimeStamp import java.time.Duration @@ -35,43 +36,47 @@ class GetTest { } private lateinit var session: Session - private lateinit var selector: Selector + private lateinit var keyExpr: KeyExpr private lateinit var queryable: Queryable @BeforeTest fun setUp() { session = Session.open().getOrThrow() - selector = "example/testing/keyexpr".intoSelector().getOrThrow() - queryable = session.declareQueryable(selector.keyExpr, callback = { query -> - query.replySuccess(query.keyExpr, value, timestamp = timestamp) - }).getOrThrow() + keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() + queryable = session.declareQueryable(keyExpr).with { query -> + query.reply(query.keyExpr) + .success(value) + .timestamp(timestamp) + .res() + }.res().getOrThrow() } @AfterTest fun tearDown() { session.close() - selector.close() + keyExpr.close() queryable.close() } @Test fun get_runsWithCallback() { var reply: Reply? = null - session.get(selector, callback = { + session.get(keyExpr).with { reply = it - }, timeout = Duration.ofMillis(1000)) + }.timeout(Duration.ofMillis(1000)).res() assertTrue(reply is Reply.Success) val sample = (reply as Reply.Success).sample assertEquals(value, sample.value) assertEquals(kind, sample.kind) - assertEquals(selector.keyExpr, sample.keyExpr) + assertEquals(keyExpr, sample.keyExpr) assertEquals(timestamp, sample.timestamp) } @Test fun get_runsWithHandler() { - val receiver: ArrayList = session.get(selector, handler = TestHandler(), timeout = Duration.ofMillis(1000)).getOrThrow() + val receiver: ArrayList = session.get(keyExpr).with(TestHandler()) + .timeout(Duration.ofMillis(1000)).res().getOrThrow()!! for (reply in receiver) { reply as Reply.Success @@ -84,17 +89,17 @@ class GetTest { @Test fun getWithSelectorParamsTest() { - var receivedParams: String? = null - var receivedParamsMap : Map? = null - val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> + var receivedParams = String() + var receivedParamsMap = mapOf() + val queryable = session.declareQueryable(keyExpr).with { it.use { query -> receivedParams = query.parameters - receivedParamsMap = query.selector.parametersStringMap()?.getOrThrow() - }).getOrThrow() + receivedParamsMap = query.selector.parametersStringMap().getOrThrow() + }}.res().getOrThrow() val params = "arg1=val1&arg2=val2&arg3" val paramsMap = mapOf("arg1" to "val1", "arg2" to "val2", "arg3" to "") - val selectorWithParams = Selector(selector.keyExpr, params) - session.get(selectorWithParams, callback = {}, timeout = Duration.ofMillis(1000)) + val selector = Selector(keyExpr, params) + session.get(selector).with {}.timeout(Duration.ofMillis(1000)).res() queryable.close() diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt index 08d605045..ee589caa5 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt @@ -97,7 +97,7 @@ class KeyExprTest { @Test fun sessionDeclarationTest() { val session = Session.open().getOrThrow() - val keyExpr = session.declareKeyExpr("a/b/c").getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").res().getOrThrow() assertEquals("a/b/c", keyExpr.toString()) session.close() keyExpr.close() @@ -106,20 +106,20 @@ class KeyExprTest { @Test fun sessionUnDeclarationTest() { val session = Session.open().getOrThrow() - val keyExpr = session.declareKeyExpr("a/b/c").getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").res().getOrThrow() assertEquals("a/b/c", keyExpr.toString()) - val undeclare1 = session.undeclare(keyExpr) + val undeclare1 = session.undeclare(keyExpr).res() assertTrue(undeclare1.isSuccess) // Undeclaring twice a key expression shall fail. - val undeclare2 = session.undeclare(keyExpr) + val undeclare2 = session.undeclare(keyExpr).res() assertTrue(undeclare2.isFailure) assertTrue(undeclare2.exceptionOrNull() is SessionException) // Undeclaring a key expr that was not declared through a session. val keyExpr2 = "x/y/z".intoKeyExpr().getOrThrow() - val undeclare3 = session.undeclare(keyExpr2) + val undeclare3 = session.undeclare(keyExpr2).res() assertTrue(undeclare3.isFailure) assertTrue(undeclare3.exceptionOrNull() is SessionException) diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt index 44156ed0d..e90eabfbc 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt @@ -36,10 +36,10 @@ class PublisherTest { fun setUp() { session = Session.open().getOrThrow() keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - publisher = session.declarePublisher(keyExpr).getOrThrow() - subscriber = session.declareSubscriber(keyExpr, callback = { sample -> + publisher = session.declarePublisher(keyExpr).res().getOrThrow() + subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSamples.add(sample) - }).getOrThrow() + }.res().getOrThrow() receivedSamples = ArrayList() } @@ -55,12 +55,12 @@ class PublisherTest { fun putTest() { val testValues = arrayListOf( - Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), - Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), - Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) + Value("Test 1".encodeToByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2".encodeToByteArray(), Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3".encodeToByteArray(), Encoding(Encoding.ID.TEXT_CSV)) ) - testValues.forEach() { value -> publisher.put(value) } + testValues.forEach() { value -> publisher.put(value).res() } assertEquals(receivedSamples.size, testValues.size) for ((index, sample) in receivedSamples.withIndex()) { @@ -70,7 +70,7 @@ class PublisherTest { @Test fun deleteTest() { - publisher.delete() + publisher.delete().res() assertEquals(1, receivedSamples.size) assertEquals(SampleKind.DELETE, receivedSamples[0].kind) } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt index 541b39edb..cc26c77e3 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt @@ -34,9 +34,9 @@ class PutTest { val session = Session.open().getOrThrow() var receivedSample: Sample? = null val keyExpr = TEST_KEY_EXP.intoKeyExpr().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - val value = Value(TEST_PAYLOAD, Encoding(Encoding.ID.TEXT_PLAIN)) - session.put(keyExpr, value) + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val value = Value(TEST_PAYLOAD.toByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)) + session.put(keyExpr, value).res() subscriber.close() session.close() assertNotNull(receivedSample) diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt index 69101eb1c..5b86612c9 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt @@ -17,9 +17,10 @@ package io.zenoh import io.zenoh.handlers.Handler import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.prelude.* -import io.zenoh.prelude.Encoding.ID.ZENOH_STRING -import io.zenoh.protocol.into +import io.zenoh.prelude.CongestionControl +import io.zenoh.prelude.Priority +import io.zenoh.prelude.SampleKind +import io.zenoh.prelude.QoS import io.zenoh.query.Reply import io.zenoh.queryable.Query import io.zenoh.sample.Sample @@ -64,16 +65,16 @@ class QueryableTest { Value(testPayload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), - QoS() + QoS.default() ) - val queryable = session.declareQueryable(testKeyExpr, callback = { query -> - query.replySuccess(testKeyExpr, value = sample.value, timestamp = sample.timestamp) - }).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with { query -> + query.reply(testKeyExpr).success(sample.value).timestamp(sample.timestamp!!).res() + }.res().getOrThrow() var reply: Reply? = null val delay = Duration.ofMillis(1000) withTimeout(delay) { - session.get(testKeyExpr.intoSelector(), callback = { reply = it }, timeout = delay) + session.get(testKeyExpr).with { reply = it }.timeout(delay).res() } assertTrue(reply is Reply.Success) @@ -85,14 +86,14 @@ class QueryableTest { @Test fun queryable_runsWithHandler() = runBlocking { val handler = QueryHandler() - val queryable = session.declareQueryable(testKeyExpr, handler = handler).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with(handler).res().getOrThrow() delay(500) val receivedReplies = ArrayList() - session.get(testKeyExpr.intoSelector(), callback = { reply: Reply -> + session.get(testKeyExpr).with { reply: Reply -> receivedReplies.add(reply) - }) + }.res() delay(500) @@ -101,54 +102,61 @@ class QueryableTest { assertEquals(handler.performedReplies.size, receivedReplies.size) } + @Test + fun queryableBuilder_channelHandlerIsTheDefaultHandler() = runBlocking { + val queryable = session.declareQueryable(testKeyExpr).res().getOrThrow() + assertTrue(queryable.receiver is Channel) + queryable.close() + } + @Test fun queryTest() = runBlocking { var receivedQuery: Query? = null - val queryable = - session.declareQueryable(testKeyExpr, callback = { query -> receivedQuery = query }).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with { query -> receivedQuery = query }.res().getOrThrow() - session.get(testKeyExpr.intoSelector(), callback = {}) + session.get(testKeyExpr).res() - delay(100) + delay(1000) + queryable.close() assertNotNull(receivedQuery) - assertNull(receivedQuery!!.payload) - assertNull(receivedQuery!!.encoding) - assertNull(receivedQuery!!.attachment) + assertNull(receivedQuery!!.value) + } - receivedQuery = null - val payload = "Test value" - val attachment = "Attachment".into() - session.get(testKeyExpr.intoSelector(), callback = {}, value = Value(payload, ZENOH_STRING), attachment = attachment) + @Test + fun queryWithValueTest() = runBlocking { + var receivedQuery: Query? = null + val queryable = session.declareQueryable(testKeyExpr).with { query -> receivedQuery = query }.res().getOrThrow() - delay(100) - assertNotNull(receivedQuery) - assertEquals(payload, receivedQuery!!.payload!!.bytes.decodeToString()) - assertEquals(ZENOH_STRING, receivedQuery!!.encoding!!.id) - assertEquals(attachment, receivedQuery!!.attachment) + session.get(testKeyExpr).withValue("Test value").res() + delay(1000) queryable.close() + assertNotNull(receivedQuery) + assertEquals(Value("Test value"), receivedQuery!!.value) } @Test fun queryReplySuccessTest() { val message = "Test message" val timestamp = TimeStamp.getCurrentTime() - val qos = QoS(priority = Priority.DATA_HIGH, express = true, congestionControl = CongestionControl.DROP) val priority = Priority.DATA_HIGH val express = true val congestionControl = CongestionControl.DROP - val queryable = session.declareQueryable(testKeyExpr, callback = { query -> - query.replySuccess(testKeyExpr, value = Value(message), timestamp = timestamp, qos = qos) - }).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with { + it.use { query -> + query.reply(testKeyExpr).success(message).timestamp(timestamp).priority(priority).express(express) + .congestionControl(congestionControl).res() + } + }.res().getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) + session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() queryable.close() assertTrue(receivedReply is Reply.Success) val reply = receivedReply as Reply.Success - assertEquals(message, reply.sample.value.payload.bytes.decodeToString()) + assertEquals(message, reply.sample.value.payload.decodeToString()) assertEquals(timestamp, reply.sample.timestamp) assertEquals(priority, reply.sample.qos.priority) assertEquals(express, reply.sample.qos.express) @@ -158,12 +166,14 @@ class QueryableTest { @Test fun queryReplyErrorTest() { val message = "Error message" - val queryable = session.declareQueryable(testKeyExpr, callback = { query -> - query.replyError(error = Value(message)) - }).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with { + it.use { query -> + query.reply(testKeyExpr).error(Value(message)).res() + } + }.res().getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) + session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() Thread.sleep(1000) queryable.close() @@ -171,7 +181,7 @@ class QueryableTest { assertNotNull(receivedReply) assertTrue(receivedReply is Reply.Error) val reply = receivedReply as Reply.Error - assertEquals(message, reply.error.payload.bytes.decodeToString()) + assertEquals(message, reply.error.payload.decodeToString()) } @Test @@ -180,13 +190,15 @@ class QueryableTest { val priority = Priority.DATA_HIGH val express = true val congestionControl = CongestionControl.DROP - val qos = QoS(priority = Priority.DATA_HIGH, express = true, congestionControl = CongestionControl.DROP) + val queryable = session.declareQueryable(testKeyExpr).with { + it.use { query -> + query.reply(testKeyExpr).delete().timestamp(timestamp).priority(priority).express(express) + .congestionControl(congestionControl).res() + } + }.res().getOrThrow() - val queryable = session.declareQueryable(testKeyExpr, callback = { query -> - query.replyDelete(testKeyExpr, timestamp = timestamp, qos = qos) - }).getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) + session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() queryable.close() @@ -203,13 +215,11 @@ class QueryableTest { @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val channel = Channel() - val queryable = - session.declareQueryable(testKeyExpr, channel = channel, onClose = { onCloseWasCalled = true }).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() queryable.undeclare() assertTrue(onCloseWasCalled) - assertTrue(queryable.receiver.isClosedForReceive) + assertTrue(queryable.receiver!!.isClosedForReceive) } } @@ -238,9 +248,9 @@ private class QueryHandler : Handler { Value(payload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), - QoS() + QoS.default() ) performedReplies.add(sample) - query.replySuccess(query.keyExpr, value = sample.value, timestamp = sample.timestamp) + query.reply(query.keyExpr).success(sample.value).timestamp(sample.timestamp!!).res() } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt index 336e404f9..48f69ee9a 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt @@ -51,9 +51,9 @@ class SessionTest { @Test fun sessionClose_succeedsDespiteNotFreeingAllDeclarations() { val session = Session.open().getOrThrow() - val queryable = session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() - val publisher = session.declarePublisher(testKeyExpr).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr).with {}.res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).res().getOrThrow() session.close() queryable.close() @@ -65,23 +65,23 @@ class SessionTest { fun sessionClose_declarationsAreUndeclaredAfterClosingSessionTest() = runBlocking { val session = Session.open().getOrThrow() - val publisher = session.declarePublisher(testKeyExpr).getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr).res().getOrThrow() session.close() assertFalse(publisher.isValid()) assertFalse(subscriber.isValid()) - assertTrue(publisher.put("Test").isFailure) + assertTrue(publisher.put("Test").res().isFailure) } @Test fun sessionClose_newDeclarationsReturnNullAfterClosingSession() { val session = Session.open().getOrThrow() session.close() - assertFailsWith { session.declarePublisher(testKeyExpr).getOrThrow() } - assertFailsWith { session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() } - assertFailsWith { session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() } + assertFailsWith { session.declarePublisher(testKeyExpr).res().getOrThrow() } + assertFailsWith { session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() } + assertFailsWith { session.declareQueryable(testKeyExpr).with {}.res().getOrThrow() } } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt index ea968daba..a58f49edc 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt @@ -22,7 +22,6 @@ import io.zenoh.sample.Sample import io.zenoh.value.Value import io.zenoh.prelude.CongestionControl import io.zenoh.prelude.Priority -import io.zenoh.prelude.QoS import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking @@ -34,13 +33,13 @@ import kotlin.test.* class SubscriberTest { companion object { - val TEST_PRIORITY = Priority.DATA_HIGH - val TEST_CONGESTION_CONTROL = CongestionControl.BLOCK + val TEST_PRIORITY = Priority.DATA_HIGH; + val TEST_CONGESTION_CONTROL = CongestionControl.BLOCK; val testValues = arrayListOf( - Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), - Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), - Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) + Value("Test 1".encodeToByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2".encodeToByteArray(), Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3".encodeToByteArray(), Encoding(Encoding.ID.TEXT_CSV)) ) } @@ -63,17 +62,20 @@ class SubscriberTest { fun subscriber_runsWithCallback() { val receivedSamples = ArrayList() val subscriber = - session.declareSubscriber(testKeyExpr, callback = { sample -> receivedSamples.add(sample)}).getOrThrow() + session.declareSubscriber(testKeyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() testValues.forEach { value -> - session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) + session.put(testKeyExpr, value) + .priority(TEST_PRIORITY) + .congestionControl(TEST_CONGESTION_CONTROL) + .res() } assertEquals(receivedSamples.size, testValues.size) receivedSamples.zip(testValues).forEach { (sample, value) -> assertEquals(sample.value, value) - assertEquals(sample.qos.priority, TEST_PRIORITY) - assertEquals(sample.qos.congestionControl, TEST_CONGESTION_CONTROL) + assertEquals(sample.qos.priority(), TEST_PRIORITY) + assertEquals(sample.qos.congestionControl(), TEST_CONGESTION_CONTROL) } subscriber.close() @@ -82,22 +84,32 @@ class SubscriberTest { @Test fun subscriber_runsWithHandler() { val handler = QueueHandler() - val subscriber = session.declareSubscriber(testKeyExpr, handler = handler).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr).with(handler).res().getOrThrow() - testValues.forEach { value -> - session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) + testValues.forEach { value -> + session.put(testKeyExpr, value) + .priority(TEST_PRIORITY) + .congestionControl(TEST_CONGESTION_CONTROL) + .res() } assertEquals(handler.queue.size, testValues.size) handler.queue.zip(testValues).forEach { (sample, value) -> assertEquals(sample.value, value) - assertEquals(sample.qos.priority, TEST_PRIORITY) - assertEquals(sample.qos.congestionControl, TEST_CONGESTION_CONTROL) + assertEquals(sample.qos.priority(), TEST_PRIORITY) + assertEquals(sample.qos.congestionControl(), TEST_CONGESTION_CONTROL) } subscriber.close() } + @Test + fun subscriberBuilder_channelHandlerIsTheDefaultHandler() { + val subscriber = session.declareSubscriber(testKeyExpr).res().getOrThrow() + assertTrue(subscriber.receiver is Channel) + subscriber.close() + } + @Test fun subscriber_isDeclaredWithNonDeclaredKeyExpression() { // Declaring a subscriber with an undeclared key expression and verifying it properly receives samples. @@ -105,8 +117,8 @@ class SubscriberTest { val session = Session.open().getOrThrow() val receivedSamples = ArrayList() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSamples.add(sample) }).getOrThrow() - testValues.forEach { value -> session.put(testKeyExpr, value) } + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() + testValues.forEach { value -> session.put(testKeyExpr, value).res() } subscriber.close() assertEquals(receivedSamples.size, testValues.size) @@ -120,11 +132,11 @@ class SubscriberTest { @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val subscriber = session.declareSubscriber(testKeyExpr, channel = Channel(), onClose = { onCloseWasCalled = true }).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() subscriber.undeclare() assertTrue(onCloseWasCalled) - assertTrue(subscriber.receiver.isClosedForReceive) + assertTrue(subscriber.receiver!!.isClosedForReceive) } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt index 5413affe4..e4e233cd1 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt @@ -17,7 +17,6 @@ package io.zenoh import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding -import io.zenoh.protocol.ZBytes import io.zenoh.query.Reply import io.zenoh.sample.Sample import io.zenoh.value.Value @@ -33,7 +32,7 @@ class UserAttachmentTest { val value = Value("test", Encoding(Encoding.ID.TEXT_PLAIN)) const val keyExprString = "example/testing/attachment" const val attachment = "mock_attachment" - val attachmentZBytes = ZBytes.from(attachment) + val attachmentBytes = attachment.toByteArray() } @BeforeTest @@ -51,43 +50,41 @@ class UserAttachmentTest { @Test fun putWithAttachmentTest() { var receivedSample: Sample? = null - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - session.put(keyExpr, value, attachment = attachmentZBytes) + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + session.put(keyExpr, value).withAttachment(attachmentBytes).res() subscriber.close() assertNotNull(receivedSample) { - val receivedAttachment = it.attachment!! - assertEquals(attachment, receivedAttachment.toString()) + assertEquals(attachment, it.attachment!!.decodeToString()) } } @Test fun publisherPutWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> + val publisher = session.declarePublisher(keyExpr).res().getOrThrow() + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample - }).getOrThrow() + }.res().getOrThrow() - publisher.put("test", attachment = attachmentZBytes) + publisher.put("test").withAttachment(attachmentBytes).res() publisher.close() subscriber.close() assertNotNull(receivedSample) { - val receivedAttachment = it.attachment!! - assertEquals(attachment, receivedAttachment.deserialize().getOrNull()) + assertEquals(attachment, it.attachment!!.decodeToString()) } } @Test fun publisherPutWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + val publisher = session.declarePublisher(keyExpr).res().getOrThrow() + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - publisher.put("test") + publisher.put("test").res() publisher.close() subscriber.close() @@ -100,27 +97,26 @@ class UserAttachmentTest { @Test fun publisherDeleteWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + val publisher = session.declarePublisher(keyExpr).res().getOrThrow() + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - publisher.delete(attachment = attachmentZBytes) + publisher.delete().withAttachment(attachmentBytes).res() publisher.close() subscriber.close() assertNotNull(receivedSample) { - val receivedAttachment = it.attachment!! - assertEquals(attachment, receivedAttachment.toString()) + assertEquals(attachment, it.attachment!!.decodeToString()) } } @Test fun publisherDeleteWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).getOrThrow() - val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + val publisher = session.declarePublisher(keyExpr).res().getOrThrow() + val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - publisher.delete() + publisher.delete().res() publisher.close() subscriber.close() @@ -132,52 +128,51 @@ class UserAttachmentTest { @Test fun queryWithAttachmentTest() { - var receivedAttachment: ZBytes? = null - val queryable = session.declareQueryable(keyExpr, callback = { query -> + var receivedAttachment: ByteArray? = null + val queryable = session.declareQueryable(keyExpr).with { query -> receivedAttachment = query.attachment - query.replySuccess(keyExpr, value = Value("test")) - }).getOrThrow() + query.reply(keyExpr).success("test").res() + }.res().getOrThrow() - session.get(keyExpr.intoSelector(), callback = {}, attachment = attachmentZBytes, timeout = Duration.ofMillis(1000)).getOrThrow() + session.get(keyExpr).with {}.withAttachment(attachmentBytes).timeout(Duration.ofMillis(1000)).res().getOrThrow() queryable.close() assertNotNull(receivedAttachment) { - assertEquals(attachmentZBytes, it) + assertEquals(attachment, it.decodeToString()) } } @Test fun queryReplyWithAttachmentTest() { var reply: Reply? = null - val queryable = session.declareQueryable(keyExpr, callback = { query -> - query.replySuccess(keyExpr, value = Value("test"), attachment = attachmentZBytes) - }).getOrThrow() + val queryable = session.declareQueryable(keyExpr).with { query -> + query.reply(keyExpr).success("test").attachment(attachmentBytes).res() + }.res().getOrThrow() - session.get(keyExpr.intoSelector(), callback = { + session.get(keyExpr).with { if (it is Reply.Success) { reply = it } - }, timeout = Duration.ofMillis(1000)).getOrThrow() + }.timeout(Duration.ofMillis(1000)).res().getOrThrow() queryable.close() assertNotNull(reply) { - val receivedAttachment = (it as Reply.Success).sample.attachment!! - assertEquals(attachment, receivedAttachment.toString()) + assertEquals(attachment, (it as Reply.Success).sample.attachment!!.decodeToString()) } } @Test fun queryReplyWithoutAttachmentTest() { var reply: Reply? = null - val queryable = session.declareQueryable(keyExpr, callback = { query -> - query.replySuccess(keyExpr, value = Value("test")) - }).getOrThrow() + val queryable = session.declareQueryable(keyExpr).with { query -> + query.reply(keyExpr).success("test").res() + }.res().getOrThrow() - session.get(keyExpr.intoSelector(), callback = { + session.get(keyExpr).with { reply = it - }, timeout = Duration.ofMillis(1000)).getOrThrow() + }.timeout(Duration.ofMillis(1000)).res().getOrThrow() queryable.close() diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt deleted file mode 100644 index 6320cd5ce..000000000 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt +++ /dev/null @@ -1,591 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh - -import io.zenoh.protocol.Deserializable -import io.zenoh.protocol.Serializable -import io.zenoh.protocol.ZBytes -import io.zenoh.protocol.into -import org.junit.jupiter.api.Assertions.assertArrayEquals - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.reflect.KClass -import kotlin.reflect.typeOf -import kotlin.test.Test -import kotlin.test.assertTrue - -data class SimpleTestCase( - val originalItem: T, - val clazz: KClass -) - -data class ListTestCase( - val originalList: List, - val itemclazz: KClass -) - -data class MapTestCase( - val originalMap: Map, - val keyclazz: KClass, - val valueclazz: KClass, -) - -class ZBytesTests { - - companion object { - @JvmStatic - fun simpleTestCases(): List> { - return listOf( - SimpleTestCase(1.toByte(), Byte::class), - SimpleTestCase(1.toShort(), Short::class), - SimpleTestCase(1, Int::class), - SimpleTestCase(1L, Long::class), - SimpleTestCase(1.0f, Float::class), - SimpleTestCase(1.0, Double::class), - SimpleTestCase("value1", String::class), - SimpleTestCase(byteArrayOf(1, 2, 3), ByteArray::class), - SimpleTestCase(MyZBytes("foo"), MyZBytes::class) - ) - } - - @JvmStatic - fun listTestCases(): List> { - return listOf( - // Byte Lists - ListTestCase(listOf(1.toByte(), 2.toByte(), 3.toByte()), Byte::class), - // Short Lists - ListTestCase(listOf(1.toShort(), 2.toShort(), 3.toShort()), Short::class), - // Int Lists - ListTestCase(listOf(1, 2, 3), Int::class), - // Long Lists - ListTestCase(listOf(1L, 2L, 3L), Long::class), - // Float Lists - ListTestCase(listOf(1.0f, 2.0f, 3.0f), Float::class), - // Double Lists - ListTestCase(listOf(1.0, 2.0, 3.0), Double::class), - // String Lists - ListTestCase(listOf("value1", "value2", "value3"), String::class), - // ByteArray Lists - ListTestCase(listOf(byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6)), ByteArray::class), - // MyZBytes Lists - ListTestCase(listOf(MyZBytes("foo"), MyZBytes("bar")), MyZBytes::class) - ) - } - - @JvmStatic - fun mapTestCases(): List> { - return listOf( - // Byte Keys - MapTestCase(mapOf(1.toByte() to "value1", 2.toByte() to "value2"), Byte::class, String::class), - MapTestCase(mapOf(1.toByte() to 1.toByte(), 2.toByte() to 2.toByte()), Byte::class, Byte::class), - MapTestCase(mapOf(1.toByte() to 1.toShort(), 2.toByte() to 2.toShort()), Byte::class, Short::class), - MapTestCase(mapOf(1.toByte() to 1, 2.toByte() to 2), Byte::class, Int::class), - MapTestCase(mapOf(1.toByte() to 1L, 2.toByte() to 2L), Byte::class, Long::class), - MapTestCase(mapOf(1.toByte() to 1.0f, 2.toByte() to 2.0f), Byte::class, Float::class), - MapTestCase(mapOf(1.toByte() to 1.0, 2.toByte() to 2.0), Byte::class, Double::class), - MapTestCase(mapOf(1.toByte() to byteArrayOf(1, 2, 3), 2.toByte() to byteArrayOf(4, 5, 6)), Byte::class, ByteArray::class), - MapTestCase(mapOf(1.toByte() to MyZBytes("foo"), 2.toByte() to MyZBytes("bar")), Byte::class, MyZBytes::class), - - // Short Keys - MapTestCase(mapOf(1.toShort() to "value1", 2.toShort() to "value2"), Short::class, String::class), - MapTestCase(mapOf(1.toShort() to 1.toByte(), 2.toShort() to 2.toByte()), Short::class, Byte::class), - MapTestCase(mapOf(1.toShort() to 1.toShort(), 2.toShort() to 2.toShort()), Short::class, Short::class), - MapTestCase(mapOf(1.toShort() to 1, 2.toShort() to 2), Short::class, Int::class), - MapTestCase(mapOf(1.toShort() to 1L, 2.toShort() to 2L), Short::class, Long::class), - MapTestCase(mapOf(1.toShort() to 1.0f, 2.toShort() to 2.0f), Short::class, Float::class), - MapTestCase(mapOf(1.toShort() to 1.0, 2.toShort() to 2.0), Short::class, Double::class), - MapTestCase(mapOf(1.toShort() to byteArrayOf(1, 2, 3), 2.toShort() to byteArrayOf(4, 5, 6)), Short::class, ByteArray::class), - MapTestCase(mapOf(1.toShort() to MyZBytes("foo"), 2.toShort() to MyZBytes("bar")), Short::class, MyZBytes::class), - - // Int Keys - MapTestCase(mapOf(1 to "value1", 2 to "value2"), Int::class, String::class), - MapTestCase(mapOf(1 to 1.toByte(), 2 to 2.toByte()), Int::class, Byte::class), - MapTestCase(mapOf(1 to 1.toShort(), 2 to 2.toShort()), Int::class, Short::class), - MapTestCase(mapOf(1 to 1, 2 to 2), Int::class, Int::class), - MapTestCase(mapOf(1 to 1L, 2 to 2L), Int::class, Long::class), - MapTestCase(mapOf(1 to 1.0f, 2 to 2.0f), Int::class, Float::class), - MapTestCase(mapOf(1 to 1.0, 2 to 2.0), Int::class, Double::class), - MapTestCase(mapOf(1 to byteArrayOf(1, 2, 3), 2 to byteArrayOf(4, 5, 6)), Int::class, ByteArray::class), - MapTestCase(mapOf(1 to MyZBytes("foo"), 2 to MyZBytes("bar")), Int::class, MyZBytes::class), - - // Long Keys - MapTestCase(mapOf(1L to "value1", 2L to "value2"), Long::class, String::class), - MapTestCase(mapOf(1L to 1.toByte(), 2L to 2.toByte()), Long::class, Byte::class), - MapTestCase(mapOf(1L to 1.toShort(), 2L to 2.toShort()), Long::class, Short::class), - MapTestCase(mapOf(1L to 1, 2L to 2), Long::class, Int::class), - MapTestCase(mapOf(1L to 1L, 2L to 2L), Long::class, Long::class), - MapTestCase(mapOf(1L to 1.0f, 2L to 2.0f), Long::class, Float::class), - MapTestCase(mapOf(1L to 1.0, 2L to 2.0), Long::class, Double::class), - MapTestCase(mapOf(1L to byteArrayOf(1, 2, 3), 2L to byteArrayOf(4, 5, 6)), Long::class, ByteArray::class), - MapTestCase(mapOf(1L to MyZBytes("foo"), 2L to MyZBytes("bar")), Long::class, MyZBytes::class), - - // Float Keys - MapTestCase(mapOf(1.0f to "value1", 2.0f to "value2"), Float::class, String::class), - MapTestCase(mapOf(1.0f to 1.toByte(), 2.0f to 2.toByte()), Float::class, Byte::class), - MapTestCase(mapOf(1.0f to 1.toShort(), 2.0f to 2.toShort()), Float::class, Short::class), - MapTestCase(mapOf(1.0f to 1, 2.0f to 2), Float::class, Int::class), - MapTestCase(mapOf(1.0f to 1L, 2.0f to 2L), Float::class, Long::class), - MapTestCase(mapOf(1.0f to 1.0f, 2.0f to 2.0f), Float::class, Float::class), - MapTestCase(mapOf(1.0f to 1.0, 2.0f to 2.0), Float::class, Double::class), - MapTestCase(mapOf(1.0f to byteArrayOf(1, 2, 3), 2.0f to byteArrayOf(4, 5, 6)), Float::class, ByteArray::class), - MapTestCase(mapOf(1.0f to MyZBytes("foo"), 2.0f to MyZBytes("bar")), Float::class, MyZBytes::class), - - // Double Keys - MapTestCase(mapOf(1.0 to "value1", 2.0 to "value2"), Double::class, String::class), - MapTestCase(mapOf(1.0 to 1.toByte(), 2.0 to 2.toByte()), Double::class, Byte::class), - MapTestCase(mapOf(1.0 to 1.toShort(), 2.0 to 2.toShort()), Double::class, Short::class), - MapTestCase(mapOf(1.0 to 1, 2.0 to 2), Double::class, Int::class), - MapTestCase(mapOf(1.0 to 1L, 2.0 to 2L), Double::class, Long::class), - MapTestCase(mapOf(1.0 to 1.0f, 2.0 to 2.0f), Double::class, Float::class), - MapTestCase(mapOf(1.0 to 1.0, 2.0 to 2.0), Double::class, Double::class), - MapTestCase(mapOf(1.0 to byteArrayOf(1, 2, 3), 2.0 to byteArrayOf(4, 5, 6)), Double::class, ByteArray::class), - MapTestCase(mapOf(1.0 to MyZBytes("foo"), 2.0 to MyZBytes("bar")), Double::class, MyZBytes::class), - - // String Keys - MapTestCase(mapOf("key1" to "value1", "key2" to "value2"), String::class, String::class), - MapTestCase(mapOf("key1" to 1.toByte(), "key2" to 2.toByte()), String::class, Byte::class), - MapTestCase(mapOf("key1" to 1.toShort(), "key2" to 2.toShort()), String::class, Short::class), - MapTestCase(mapOf("key1" to 1, "key2" to 2), String::class, Int::class), - MapTestCase(mapOf("key1" to 1L, "key2" to 2L), String::class, Long::class), - MapTestCase(mapOf("key1" to 1.0f, "key2" to 2.0f), String::class, Float::class), - MapTestCase(mapOf("key1" to 1.0, "key2" to 2.0), String::class, Double::class), - MapTestCase(mapOf("key1" to byteArrayOf(1, 2, 3), "key2" to byteArrayOf(4, 5, 6)), String::class, ByteArray::class), - MapTestCase(mapOf("key1" to MyZBytes("foo"), "key2" to MyZBytes("bar")), String::class, MyZBytes::class), - - // ByteArray Keys - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to "value1", byteArrayOf(4, 5, 6) to "value2"), ByteArray::class, String::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.toByte(), byteArrayOf(4, 5, 6) to 2.toByte()), ByteArray::class, Byte::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.toShort(), byteArrayOf(4, 5, 6) to 2.toShort()), ByteArray::class, Short::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1, byteArrayOf(4, 5, 6) to 2), ByteArray::class, Int::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1L, byteArrayOf(4, 5, 6) to 2L), ByteArray::class, Long::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.0f, byteArrayOf(4, 5, 6) to 2.0f), ByteArray::class, Float::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.0, byteArrayOf(4, 5, 6) to 2.0), ByteArray::class, Double::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6) to byteArrayOf(4, 5, 6)), ByteArray::class, ByteArray::class), - MapTestCase(mapOf(byteArrayOf(1, 2, 3) to MyZBytes("foo"), byteArrayOf(4, 5, 6) to MyZBytes("bar")), ByteArray::class, MyZBytes::class), - - // MyZBytes (Serializable and Deserializable) Keys - MapTestCase(mapOf(MyZBytes("foo") to "value1", MyZBytes("bar") to "value2"), MyZBytes::class, String::class), - MapTestCase(mapOf(MyZBytes("foo") to 1.toByte(), MyZBytes("bar") to 2.toByte()), MyZBytes::class, Byte::class), - MapTestCase(mapOf(MyZBytes("foo") to 1.toShort(), MyZBytes("bar") to 2.toShort()), MyZBytes::class, Short::class), - MapTestCase(mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2), MyZBytes::class, Int::class), - MapTestCase(mapOf(MyZBytes("foo") to 1L, MyZBytes("bar") to 2L), MyZBytes::class, Long::class), - MapTestCase(mapOf(MyZBytes("foo") to 1.0f, MyZBytes("bar") to 2.0f), MyZBytes::class, Float::class), - MapTestCase(mapOf(MyZBytes("foo") to 1.0, MyZBytes("bar") to 2.0), MyZBytes::class, Double::class), - MapTestCase(mapOf(MyZBytes("foo") to byteArrayOf(1, 2, 3), MyZBytes("bar") to byteArrayOf(4, 5, 6)), MyZBytes::class, ByteArray::class), - MapTestCase(mapOf(MyZBytes("foo") to MyZBytes("foo"), MyZBytes("bar") to MyZBytes("bar")), MyZBytes::class, MyZBytes::class) - ) - } - } - - @ParameterizedTest - @MethodSource("simpleTestCases") - fun serializationAndDeserialization_simpleTest(testCase: SimpleTestCase) { - val originalItem = testCase.originalItem - val clazz = testCase.clazz - - val bytes = ZBytes.serialize(originalItem, clazz = clazz).getOrThrow() - val deserializedItem = bytes.deserialize(clazz = clazz).getOrThrow() - - if (originalItem is ByteArray) { - assertArrayEquals(originalItem, deserializedItem as ByteArray) - } else { - assertEquals(originalItem, deserializedItem) - } - } - - @ParameterizedTest - @MethodSource("listTestCases") - fun serializationAndDeserialization_listTest(testCase: ListTestCase) { - val originalList = testCase.originalList - val itemClass = testCase.itemclazz - - val bytes = ZBytes.serialize(originalList).getOrThrow() - - val deserializedList = bytes.deserialize(clazz = List::class, arg1clazz = itemClass).getOrThrow() - - if (originalList.isNotEmpty() && originalList[0] is ByteArray) { - originalList.forEachIndexed { index, value -> - assertArrayEquals(value as ByteArray, deserializedList[index] as ByteArray) - } - } else { - assertEquals(originalList, deserializedList) - } - } - - @ParameterizedTest - @MethodSource("mapTestCases") - fun serializationAndDeserialization_mapTest(testCase: MapTestCase) { - val originalMap = testCase.originalMap - val keyClass = testCase.keyclazz - val valueClass = testCase.valueclazz - - val bytes = ZBytes.serialize(originalMap).getOrThrow() - - val deserializedMap = bytes.deserialize( - clazz = Map::class, - arg1clazz = keyClass, - arg2clazz = valueClass - ).getOrThrow() - - if (keyClass == ByteArray::class && valueClass != ByteArray::class) { - val map1 = originalMap.map { (k, v) -> (k as ByteArray).toList() to v }.toMap() - val map2 = originalMap.map { (k, v) -> (k as ByteArray).toList() to v }.toMap() - assertEquals(map1, map2) - return - } - - if (keyClass != ByteArray::class && valueClass == ByteArray::class) { - val map1 = originalMap.map { (k, v) -> k to (v as ByteArray).toList() }.toMap() - val map2 = originalMap.map { (k, v) -> k to (v as ByteArray).toList() }.toMap() - assertEquals(map1, map2) - return - } - - if (keyClass == ByteArray::class && valueClass == ByteArray::class) { - val map1 = originalMap.map { (k, v) -> (k as ByteArray).toList() to (v as ByteArray).toList() }.toMap() - val map2 = originalMap.map { (k, v) -> (k as ByteArray).toList() to (v as ByteArray).toList() }.toMap() - assertEquals(map1, map2) - return - } - - assertEquals(originalMap, deserializedMap) - } - - @Test - fun deserializationWithMapOfDeserializationFunctionsTest() { - val stringMap = mapOf("key1" to "value1", "key2" to "value2") - val zbytesMap = stringMap.map { (k, v) -> k.into() to v.into() }.toMap() - val zbytesListOfPairs = stringMap.map { (k, v) -> k.into() to v.into() } - val intMap = mapOf(1 to 10, 2 to 20, 3 to 30) - val zbytesList = listOf(1.into(), 2.into(), 3.into()) - - val serializedBytes = serializeZBytesMap(zbytesMap) - - val customDeserializers = mapOf( - typeOf>() to ::deserializeIntoZBytesMap, - typeOf>() to ::deserializeIntoStringMap, - typeOf>() to ::deserializeIntoIntMap, - typeOf>() to ::deserializeIntoZBytesList, - typeOf>>() to ::deserializeIntoListOfPairs, - ) - - val deserializedMap = serializedBytes.deserialize>(customDeserializers).getOrThrow() - assertEquals(zbytesMap, deserializedMap) - - val deserializedMap2 = serializedBytes.deserialize>(customDeserializers).getOrThrow() - assertEquals(stringMap, deserializedMap2) - - val intMapBytes = serializeIntoIntMap(intMap) - val deserializedMap3 = intMapBytes.deserialize>(customDeserializers).getOrThrow() - assertEquals(intMap, deserializedMap3) - - val serializedZBytesList = serializeZBytesList(zbytesList) - val deserializedList = serializedZBytesList.deserialize>(customDeserializers).getOrThrow() - assertEquals(zbytesList, deserializedList) - - val serializedZBytesPairList = serializeZBytesMap(zbytesListOfPairs.toMap()) - val deserializedZBytesPairList = - serializedZBytesPairList.deserialize>>(customDeserializers).getOrThrow() - assertEquals(zbytesListOfPairs, deserializedZBytesPairList) - } - - /** - * A series of tests to verify the correct functioning of the [ZBytes.deserialize] function. - * - * The [ZBytes.deserialize] function with reification can not be tested in a parametrized fashion because - * it uses reified parameters which causes the testing framework (designed for Java) to fail to properly - * set up the tests. - */ - @Test - fun serializationAndDeserializationWithReification() { - /*********************************************** - * Standard serialization and deserialization. * - ***********************************************/ - - /** Numeric: byte, short, int, float, double */ - val intInput = 1234 - var payload = ZBytes.from(intInput) - val intOutput = payload.deserialize().getOrThrow() - assertEquals(intInput, intOutput) - - // Another example with float - val floatInput = 3.1415f - payload = ZBytes.from(floatInput) - val floatOutput = payload.deserialize().getOrThrow() - assertEquals(floatInput, floatOutput) - - /** String serialization and deserialization. */ - val stringInput = "example" - payload = ZBytes.from(stringInput) - val stringOutput = payload.deserialize().getOrThrow() - assertEquals(stringInput, stringOutput) - - /** ByteArray serialization and deserialization. */ - val byteArrayInput = "example".toByteArray() - payload = ZBytes.from(byteArrayInput) // Equivalent to `byteArrayInput.into()` - val byteArrayOutput = payload.deserialize().getOrThrow() - assertTrue(byteArrayInput.contentEquals(byteArrayOutput)) - - val inputList = listOf("sample1", "sample2", "sample3") - payload = ZBytes.serialize(inputList).getOrThrow() - val outputList = payload.deserialize>().getOrThrow() - assertEquals(inputList, outputList) - - val inputListZBytes = inputList.map { value -> value.into() } - payload = ZBytes.serialize(inputListZBytes).getOrThrow() - val outputListZBytes = payload.deserialize>().getOrThrow() - assertEquals(inputListZBytes, outputListZBytes) - - val inputListByteArray = inputList.map { value -> value.toByteArray() } - payload = ZBytes.serialize(inputListByteArray).getOrThrow() - val outputListByteArray = payload.deserialize>().getOrThrow() - assertTrue(compareByteArrayLists(inputListByteArray, outputListByteArray)) - - val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") - payload = ZBytes.serialize(inputMap).getOrThrow() - val outputMap = payload.deserialize>().getOrThrow() - assertEquals(inputMap, outputMap) - - val combinedInputMap = mapOf("key1" to ZBytes.from("zbytes1"), "key2" to ZBytes.from("zbytes2")) - payload = ZBytes.serialize(combinedInputMap).getOrThrow() - val combinedOutputMap = payload.deserialize>().getOrThrow() - assertEquals(combinedInputMap, combinedOutputMap) - - /********************************************* - * Custom serialization and deserialization. * - *********************************************/ - - val inputMyZBytes = MyZBytes("example") - payload = ZBytes.serialize(inputMyZBytes).getOrThrow() - val outputMyZBytes = payload.deserialize().getOrThrow() - assertEquals(inputMyZBytes, outputMyZBytes) - - /** List of MyZBytes. */ - val inputListMyZBytes = inputList.map { value -> MyZBytes(value) } - payload = ZBytes.serialize>(inputListMyZBytes).getOrThrow() - val outputListMyZBytes = payload.deserialize>().getOrThrow() - assertEquals(inputListMyZBytes, outputListMyZBytes) - - /** Map of MyZBytes. */ - val inputMapMyZBytes = inputMap.map { (k, v) -> MyZBytes(k) to MyZBytes(v)}.toMap() - payload = ZBytes.serialize>(inputMapMyZBytes).getOrThrow() - val outputMapMyZBytes = payload.deserialize>().getOrThrow() - assertEquals(inputMapMyZBytes, outputMapMyZBytes) - - val combinedMap = mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2) - payload = ZBytes.serialize>(combinedMap).getOrThrow() - val combinedOutput = payload.deserialize>().getOrThrow() - assertEquals(combinedMap, combinedOutput) - - /** - * Providing a map of deserializers. - */ - val fooMap = mapOf(Foo("foo1") to Foo("bar1"), Foo("foo2") to Foo("bar2")) - val fooMapSerialized = ZBytes.from(serializeFooMap(fooMap)) - val deserializersMap = mapOf(typeOf>() to ::deserializeFooMap) - val deserializedFooMap = fooMapSerialized.deserialize>(deserializersMap).getOrThrow() - assertEquals(fooMap, deserializedFooMap) - } - - /***************** - * Testing utils * - *****************/ - - /** - * Custom class for the tests. The purpose of this class is to test - * the proper functioning of the serialization and deserialization for - * a class implementing the [Serializable] and the [Deserializable] interface. - */ - class MyZBytes(val content: String) : Serializable, Deserializable { - - override fun into(): ZBytes = content.into() - - companion object : Deserializable.From { - override fun from(zbytes: ZBytes): MyZBytes { - return MyZBytes(zbytes.toString()) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MyZBytes - - return content == other.content - } - - override fun hashCode(): Int { - return content.hashCode() - } - } - - /** Example class for the deserialization map examples. */ - class Foo(val content: String) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Foo - - return content == other.content - } - - override fun hashCode(): Int { - return content.hashCode() - } - } - - private fun compareByteArrayLists(list1: List, list2: List): Boolean { - if (list1.size != list2.size) { - return false - } - for (i in list1.indices) { - if (!list1[i].contentEquals(list2[i])) { - return false - } - } - return true - } - - - /********************************************************************************** - * Serializers and deserializers for testing the functionality of deserialization * - * with deserializer functions. * - **********************************************************************************/ - - private fun serializeFooMap(testMap: Map): ByteArray { - return testMap.map { - val key = it.key.content.toByteArray() - val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() - val value = it.value.content.toByteArray() - val valueLength = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() - keyLength + key + valueLength + value - }.reduce { acc, bytes -> acc + bytes } - } - - private fun deserializeFooMap(serializedMap: ZBytes): Map { - var idx = 0 - var sliceSize: Int - val bytes = serializedMap.toByteArray() - val decodedMap = mutableMapOf() - while (idx < bytes.size) { - sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) - .order(ByteOrder.LITTLE_ENDIAN).int - idx += Int.SIZE_BYTES - - val key = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( - ByteOrder.LITTLE_ENDIAN - ).int - idx += Int.SIZE_BYTES - - val value = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - decodedMap[Foo(key.decodeToString())] = Foo(value.decodeToString()) - } - return decodedMap - } - - private fun serializeZBytesMap(testMap: Map): ZBytes { - return testMap.map { - val key = it.key.bytes - val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() - val value = it.value.bytes - val valueLength = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() - keyLength + key + valueLength + value - }.reduce { acc, bytes -> acc + bytes }.into() - } - - private fun deserializeIntoZBytesMap(serializedMap: ZBytes): Map { - var idx = 0 - var sliceSize: Int - val decodedMap = mutableMapOf() - while (idx < serializedMap.bytes.size) { - sliceSize = ByteBuffer.wrap(serializedMap.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) - .order(ByteOrder.LITTLE_ENDIAN).int - idx += Int.SIZE_BYTES - - val key = serializedMap.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - sliceSize = ByteBuffer.wrap(serializedMap.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( - ByteOrder.LITTLE_ENDIAN - ).int - idx += Int.SIZE_BYTES - - val value = serializedMap.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - decodedMap[key.into()] = value.into() - } - return decodedMap - } - - private fun serializeIntoIntMap(intMap: Map): ZBytes { - val zBytesMap = intMap.map { (k, v) -> k.into() to v.into() }.toMap() - return serializeZBytesMap(zBytesMap) - } - - private fun deserializeIntoStringMap(serializerMap: ZBytes): Map { - return deserializeIntoZBytesMap(serializerMap).map { (k, v) -> k.toString() to v.toString() }.toMap() - } - - private fun deserializeIntoIntMap(serializerMap: ZBytes): Map { - return deserializeIntoZBytesMap(serializerMap).map { (k, v) -> - k.deserialize().getOrThrow() to v.deserialize().getOrThrow() - }.toMap() - } - - private fun serializeZBytesList(list: List): ZBytes { - return list.map { - val item = it.bytes - val itemLength = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(item.size).array() - itemLength + item - }.reduce { acc, bytes -> acc + bytes }.into() - } - - private fun deserializeIntoZBytesList(serializedList: ZBytes): List { - var idx = 0 - var sliceSize: Int - val decodedList = mutableListOf() - while (idx < serializedList.bytes.size) { - sliceSize = ByteBuffer.wrap(serializedList.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) - .order(ByteOrder.LITTLE_ENDIAN).int - idx += Int.SIZE_BYTES - - val item = serializedList.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - decodedList.add(item.into()) - } - return decodedList - } - - private fun deserializeIntoListOfPairs(serializedList: ZBytes): List> { - return deserializeIntoZBytesMap(serializedList).map { (k, v) -> k to v } - } -} \ No newline at end of file