diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 0b43e1c03..e3aa4aba3 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -31,6 +31,7 @@ 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 new file mode 100644 index 000000000..b5c844e17 --- /dev/null +++ b/examples/src/main/kotlin/io.zenoh/ZBytes.kt @@ -0,0 +1,237 @@ +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 6d4f1712d..384820b39 100644 --- a/examples/src/main/kotlin/io.zenoh/ZDelete.kt +++ b/examples/src/main/kotlin/io.zenoh/ZDelete.kt @@ -28,10 +28,8 @@ class ZDelete(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - println("Deleting resources matching '$keyExpr'...") - session.delete(keyExpr).res() - } + println("Deleting resources matching '$keyExpr'...") + session.delete(keyExpr) } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZGet.kt b/examples/src/main/kotlin/io.zenoh/ZGet.kt index 6fce767f8..91c26f4f1 100644 --- a/examples/src/main/kotlin/io.zenoh/ZGet.kt +++ b/examples/src/main/kotlin/io.zenoh/ZGet.kt @@ -17,10 +17,12 @@ 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.query.ConsolidationMode +import io.zenoh.protocol.into 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 @@ -29,38 +31,28 @@ 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 -> - 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}')") - } + 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}')") } } + } } - } } } } @@ -72,8 +64,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 value by option( - "-v", "--value", help = "An optional value to put in the query.", metavar = "value" + private val payload by option( + "-p", "--payload", help = "An optional payload to put in the query.", metavar = "payload" ) 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 ac8becd62..ba62d6611 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPub.kt @@ -17,6 +17,7 @@ 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" @@ -28,27 +29,23 @@ class ZPub(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - 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++ - } - } + 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++ } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt index 9db3be0bf..81ef45956 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt @@ -25,6 +25,7 @@ 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( @@ -38,35 +39,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 config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + val qos = QoS( + congestionControl = CongestionControl.BLOCK, + priority = priorityInput?.let { Priority.entries[it] } ?: Priority.default(), + ) Session.open(config).onSuccess { it.use { session -> - 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() - } - } + 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() } } } + } } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZPut.kt b/examples/src/main/kotlin/io.zenoh/ZPut.kt index 60841792d..ae5492bf8 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPut.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPut.kt @@ -17,31 +17,21 @@ 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.prelude.SampleKind -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority +import io.zenoh.protocol.into 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) - .congestionControl(CongestionControl.BLOCK) - .priority(Priority.REALTIME) - .apply { - attachment?.let { - withAttachment(it.toByteArray()) - } - } - .res() + session.put(keyExpr, value, attachment = attachment?.into()) .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 995015f1e..aabd85192 100644 --- a/examples/src/main/kotlin/io.zenoh/ZQueryable.kt +++ b/examples/src/main/kotlin/io.zenoh/ZQueryable.kt @@ -16,10 +16,8 @@ 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.prelude.SampleKind -import io.zenoh.queryable.Query +import io.zenoh.value.Value import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import org.apache.commons.net.ntp.TimeStamp @@ -34,16 +32,17 @@ class ZQueryable(private val emptyArgs: Boolean) : CliktCommand( Session.open(config).onSuccess { session -> session.use { key.intoKeyExpr().onSuccess { keyExpr -> - 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) - } - } + 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") } } } } @@ -52,17 +51,6 @@ 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 d9b20f1ad..5a7fd15d7 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSub.kt @@ -17,6 +17,7 @@ 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( @@ -24,7 +25,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 -> @@ -32,17 +33,15 @@ class ZSub(private val emptyArgs: Boolean) : CliktCommand( key.intoKeyExpr().onSuccess { keyExpr -> keyExpr.use { println("Declaring Subscriber on '$keyExpr'...") - 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() - } ?: "" - })") - } + + 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" + } ?: "" + })") } } } diff --git a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt index f192e0010..f413d7333 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt @@ -18,6 +18,7 @@ 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 @@ -79,7 +80,11 @@ class ZSubThr(private val emptyArgs: Boolean) : CliktCommand( session.use { println("Press CTRL-C to quit...") subscriber = - session.declareSubscriber(keyExpr).reliable().with { listener(number) }.res().getOrThrow() + session.declareSubscriber( + keyExpr, + callback = { listener(number) }, + reliability = Reliability.RELIABLE + ).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 edca4ab89..a254c67f5 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) => { - Error::Jni($arg.to_string()) + $crate::errors::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 edfba47a4..a982babe4 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -21,6 +21,7 @@ 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 780e693e9..85362ac47 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -17,10 +17,7 @@ use jni::{ JNIEnv, }; -use crate::{ - errors::{Error, Result}, - jni_error, throw_exception, -}; +use crate::{errors::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 2391261c5..4246cb6a3 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -587,8 +587,7 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> R ) })?; - let (with_value, payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() - { + let (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 @@ -599,10 +598,9 @@ 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))?; - (true, byte_array, encoding_id, encoding_schema) + (byte_array, encoding_id, encoding_schema) } else { ( - false, env.auto_local(JByteArray::default()), 0, env.auto_local(JString::default()), @@ -634,11 +632,10 @@ 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;Z[BILjava/lang/String;[BJ)V", + "(Ljava/lang/String;Ljava/lang/String;[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), @@ -763,7 +760,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`: Parameters of the selector. +/// - `selector_params`: Optional 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. @@ -771,11 +768,9 @@ 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. -/// - `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. +/// - `payload`: Optional payload for the query. +/// - `encoding_id`: The encoding of the payload. +/// - `encoding_schema`: The encoding schema of the payload, may be null. /// /// Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. @@ -794,7 +789,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: JString, + selector_params: /*nullable*/ JString, session_ptr: *const Session, callback: JObject, on_close: JObject, @@ -802,7 +797,6 @@ 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, @@ -815,10 +809,14 @@ 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 = Selector::owned(&key_expr, &*selector_params); + 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 mut get_builder = session .get(selector) .callback(move |reply| { @@ -850,7 +848,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( .timeout(timeout) .consolidation(consolidation); - if with_value != 0 { + if !payload.is_null() { 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 new file mode 100644 index 000000000..da067f499 --- /dev/null +++ b/zenoh-jni/src/zbytes.rs @@ -0,0 +1,197 @@ +// +// 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 16c49998c..40eaaef6e 100644 --- a/zenoh-kotlin/build.gradle.kts +++ b/zenoh-kotlin/build.gradle.kts @@ -60,11 +60,15 @@ 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) { @@ -105,13 +109,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 deleted file mode 100644 index decb9ae39..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt +++ /dev/null @@ -1,23 +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 - -/** - * 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 4da978e57..4b89afaf0 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt @@ -16,9 +16,12 @@ 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 @@ -104,137 +107,314 @@ class Session private constructor(private val config: Config) : AutoCloseable { * Session.open().onSuccess { * it.use { session -> * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * 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++ - * } + * 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++ * } * } + * } * } * } * } * ``` * * @param keyExpr The [KeyExpr] the publisher will be associated to. - * @return A resolvable [Publisher.Builder] + * @param qos The [QoS] configuration of the publisher. + * @return The result of the declaration, returning the publisher in case of success. */ - fun declarePublisher(keyExpr: KeyExpr): Publisher.Builder = Publisher.Builder(this, keyExpr) + fun declarePublisher(keyExpr: KeyExpr, qos: QoS = QoS.default()): Result { + return resolvePublisher(keyExpr, qos) + } /** - * Declare a [Subscriber] on the session. - * - * The default receiver is a [Channel], but can be changed with the [Subscriber.Builder.with] functions. + * 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.") + * } + * } + * } + * } + * ``` + * + * @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. * + * 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) - * .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) - * } - * } + * session.declareSubscriber(keyExpr, handler = ExampleHandler()) + * .onSuccess { + * println("Declared subscriber on $keyExpr.") * } - * } + * } * } * } - * } * ``` * * @param keyExpr The [KeyExpr] the subscriber will be associated to. - * @return A [Subscriber.Builder] with a [Channel] receiver. + * @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): Subscriber.Builder> = Subscriber.newBuilder(this, keyExpr) + 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 [Queryable] on the session. + * Declare a [Subscriber] on the session, specifying a channel pipe the received samples. * - * The default receiver is a [Channel], but can be changed with the [Queryable.Builder.with] functions. + * 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.") + * } + * } + * // ... + * } + * } + * ``` + * + * @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. + */ + 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) + } + + /** + * Declare a [Queryable] on the session with a callback. * * Example: * ```kotlin * Session.open().onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * 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) } - * } - * } + * val queryable = session.declareQueryable(keyExpr, callback = { query -> + * query.replySuccess(keyExpr, value = Value("Hello!")) + * .onSuccess { println("Replied hello.") } + * .onFailure { println(it) } + * }).getOrThrow() + * } + * }} + * ``` + * + * @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: + * ```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") } * } * } * } * } * } - * }} + * } * ``` * - * * @param keyExpr The [KeyExpr] the queryable will be associated to. - * @return A [Queryable.Builder] with a [Channel] receiver. + * @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]. */ - fun declareQueryable(keyExpr: KeyExpr): Queryable.Builder> = Queryable.newBuilder(this, keyExpr) + 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) + } /** * 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. * - * 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. + * 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. * * Example: * ```kotlin * Session.open().onSuccess { session -> session.use { - * session.declareKeyExpr("demo/kotlin/example").res().onSuccess { keyExpr -> - * keyExpr.use { - * session.declarePublisher(it).res().onSuccess { publisher -> - * // ... - * } - * } + * val keyExpr = session.declareKeyExpr("demo/kotlin/example").getOrThrow() + * for (i in 0..999) { + * put(keyExpr, "Put number $i!") * } * }} * ``` * - * @param keyExpr The intended Key expression. - * @return A resolvable returning an optimized representation of the passed `keyExpr`. + * @param keyExpr The intended key expression. + * @return A result with the declared key expression. */ - fun declareKeyExpr(keyExpr: String): Resolvable = Resolvable { - return@Resolvable jniSession?.run { + fun declareKeyExpr(keyExpr: String): Result { + return jniSession?.run { declareKeyExpr(keyExpr).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } @@ -246,155 +426,421 @@ 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 resolvable returning the status of the undeclare operation. + * @return A result with the status of the undeclare operation. */ - fun undeclare(keyExpr: KeyExpr): Resolvable = Resolvable { - return@Resolvable jniSession?.run { + fun undeclare(keyExpr: KeyExpr): Result { + return jniSession?.run { undeclareKeyExpr(keyExpr) } ?: Result.failure(sessionClosedException) } /** - * Declare a [Get] with a [Channel] receiver. + * Performs a Get query on the [selector], handling the replies with a callback. * + * A callback must be provided to handle the incoming replies. A basic query can be achieved + * as follows: * ```kotlin - * 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()) + * 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") * } * } * } * } * ``` - * @param selector The [KeyExpr] to be used for the get operation. - * @return a resolvable [Get.Builder] with a [Channel] receiver. + * + * @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). */ - fun get(selector: Selector): Get.Builder> = Get.newBuilder(this, selector) + 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 + ) + } /** - * Declare a [Get] with a [Channel] receiver. + * Performs a Get query on the [selector], handling the replies with a [Handler]. * + * A handler must be provided to handle the incoming replies. For instance, imagine we implement + * a `QueueHandler`: * ```kotlin - * 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()) + * 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") * } * } * } * } * ``` - * @param keyExpr The [KeyExpr] to be used for the get operation. - * @return a resolvable [Get.Builder] with a [Channel] receiver. + * + * @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). */ - fun get(keyExpr: KeyExpr): Get.Builder> = Get.newBuilder(this, Selector(keyExpr)) + 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 + ) + } /** - * Declare a [Put] with the provided value on the specified key expression. + * Performs a Get query on the [selector], handling the replies with a blocking [Channel]. * * Example: * ```kotlin - * 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.") } - * }} + * 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") + * } + * } + * } * } * ``` * - * @param keyExpr The [KeyExpr] to be used for the put operation. - * @param value The [Value] to be put. - * @return A resolvable [Put.Builder]. + * 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 put(keyExpr: KeyExpr, value: Value): Put.Builder = Put.newBuilder(this, keyExpr, value) + 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 + ) + } /** * Declare a [Put] with the provided value on 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.") } - * }} + * 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() + * } + * // ... + * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the put operation. - * @param message The message to be put. - * @return A resolvable [Put.Builder]. + * @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. */ - fun put(keyExpr: KeyExpr, message: String): Put.Builder = Put.newBuilder(this, keyExpr, Value(message)) + 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) + } /** - * Declare a [Delete]. + * Declare a [Put] with the provided message 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 - * println("Opening Session") * Session.open().onSuccess { session -> * session.use { - * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> - * session.delete(keyExpr) - * .res() - * .onSuccess { - * println("Performed a delete on $keyExpr.") - * } + * "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() + * } + * // ... + * } + * } + * ``` + * + * @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. + */ + 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) + } + + /** + * Perform a delete operation. + * + * Example: + * ```kotlin + * Session.open(config).onSuccess { session -> + * session.use { + * key.intoKeyExpr().onSuccess { keyExpr -> + * println("Deleting resources matching '$keyExpr'...") + * session.delete(keyExpr) * } * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the delete operation. - * @return a resolvable [Delete.Builder]. + * @param qos The [QoS] configuration. + * @param attachment Optional [ZBytes] attachment. + * @return a [Result] with the status of the operation. */ - fun delete(keyExpr: KeyExpr): Delete.Builder = Delete.newBuilder(this, keyExpr) + fun delete(keyExpr: KeyExpr, qos: QoS = QoS.default(), attachment: ZBytes? = null): Result { + val delete = Delete(keyExpr, qos, attachment) + return resolveDelete(keyExpr, delete) + } /** Returns if session is open or has been closed. */ fun isOpen(): Boolean { return jniSession != null } - internal fun resolvePublisher(keyExpr: KeyExpr, qos: QoS): Result { + private fun resolvePublisher(keyExpr: KeyExpr, qos: QoS): Result { return jniSession?.run { declarePublisher(keyExpr, qos).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } - internal fun resolveSubscriber( + private fun resolveSubscriber( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, reliability: Reliability ): Result> { return jniSession?.run { @@ -402,11 +848,11 @@ class Session private constructor(private val config: Config) : AutoCloseable { } ?: Result.failure(sessionClosedException) } - internal fun resolveQueryable( + private fun resolveQueryable( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, complete: Boolean ): Result> { return jniSession?.run { @@ -414,27 +860,38 @@ class Session private constructor(private val config: Config) : AutoCloseable { } ?: Result.failure(sessionClosedException) } - internal fun resolveGet( + private fun resolveGet( selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, value: Value?, - attachment: ByteArray?, - ): Result { + attachment: ZBytes?, + ): Result { return jniSession?.run { - performGet(selector, callback, onClose, receiver, timeout, target, consolidation, value, attachment) + performGet( + selector, + callback, + onClose, + receiver, + timeout, + target, + consolidation, + value?.payload, + value?.encoding, + attachment + ) } ?: Result.failure(sessionClosedException) } - internal fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { + private fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { jniSession?.run { performPut(keyExpr, put) } } - internal fun resolveDelete(keyExpr:KeyExpr, delete: Delete): Result = runCatching { + private 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 06097dcfa..16d6c91f5 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt @@ -21,15 +21,13 @@ import kotlinx.coroutines.runBlocking /** * Channel handler * - * 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. + * Implementation of a [Handler] with a [Channel] receiver. * * @param T * @property channel * @constructor Create empty Channel handler */ -class ChannelHandler(private val channel: Channel) : Handler> { +internal 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 774c3ce29..18da152a9 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt @@ -23,7 +23,6 @@ import io.zenoh.ZenohType * **Example**: * ```kotlin * class QueueHandler : Handler> { - * * private val queue: ArrayDeque = ArrayDeque() * * override fun handle(t: T) { @@ -43,11 +42,7 @@ import io.zenoh.ZenohType * * That `QueueHandler` could then be used as follows, for instance for a subscriber: * ```kotlin - * val handler = QueueHandler() - * val receiver = session.declareSubscriber(keyExpr) - * .with(handler) - * .res() - * .onSuccess { ... } + * val subscriber = session.declareSubscriber(keyExpr, handler = QueueHandler()).getOrThrow() * ``` * * @param T A receiving [ZenohType], either a [io.zenoh.sample.Sample], a [io.zenoh.query.Reply] or a [io.zenoh.queryable.Query]. @@ -72,8 +67,7 @@ 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 - * (see [io.zenoh.query.Get]), it is invoked when no more elements of type [T] are expected - * to be received. + * 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 93dbffa7e..f1b57f971 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -14,6 +14,7 @@ package io.zenoh.jni +import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** @@ -29,8 +30,8 @@ internal class JNIPublisher(private val ptr: Long) { * @param value The [Value] to be put. * @param attachment Optional attachment. */ - fun put(value: Value, attachment: ByteArray?): Result = runCatching { - putViaJNI(value.payload, value.encoding.id.ordinal, value.encoding.schema, attachment, ptr) + fun put(value: Value, attachment: ZBytes?): Result = runCatching { + putViaJNI(value.payload.bytes, value.encoding.id.ordinal, value.encoding.schema, attachment?.bytes, ptr) } /** @@ -38,8 +39,8 @@ internal class JNIPublisher(private val ptr: Long) { * * @param attachment Optional attachment. */ - fun delete(attachment: ByteArray?): Result = runCatching { - deleteViaJNI(attachment, ptr) + fun delete(attachment: ZBytes?): Result = runCatching { + deleteViaJNI(attachment?.bytes, 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 fd5eae3c3..f881fbc1e 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -16,6 +16,7 @@ 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 @@ -35,12 +36,12 @@ internal class JNIQuery(private val ptr: Long) { ptr, sample.keyExpr.jniKeyExpr?.ptr ?: 0, sample.keyExpr.keyExpr, - sample.value.payload, + sample.value.payload.bytes, sample.value.encoding.id.ordinal, sample.value.encoding.schema, timestampEnabled, if (timestampEnabled) sample.timestamp!!.ntpValue() else 0, - sample.attachment, + sample.attachment?.bytes, sample.qos.express, sample.qos.priority.value, sample.qos.congestionControl.value @@ -48,10 +49,10 @@ internal class JNIQuery(private val ptr: Long) { } fun replyError(errorValue: Value): Result = runCatching { - replyErrorViaJNI(ptr, errorValue.payload, errorValue.encoding.id.ordinal, errorValue.encoding.schema) + replyErrorViaJNI(ptr, errorValue.payload.bytes, errorValue.encoding.id.ordinal, errorValue.encoding.schema) } - fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: ByteArray?, qos: QoS): Result = + fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: ZBytes?, qos: QoS): Result = runCatching { val timestampEnabled = timestamp != null replyDeleteViaJNI( @@ -60,7 +61,7 @@ internal class JNIQuery(private val ptr: Long) { keyExpr.keyExpr, timestampEnabled, if (timestampEnabled) timestamp!!.ntpValue() else 0, - attachment, + attachment?.bytes, 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 ceaba4678..bd10d4f56 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -25,7 +25,9 @@ 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 @@ -82,7 +84,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 -> @@ -92,8 +94,8 @@ internal class JNISession { Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), SampleKind.fromInt(kind), timestamp, - QoS(express, congestionControl, priority), - attachmentBytes + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() ) callback.run(sample) } @@ -104,16 +106,19 @@ 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, withValue: Boolean, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long -> + JNIQueryableCallback { keyExpr: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long -> val jniQuery = JNIQuery(queryPtr) val keyExpr2 = KeyExpr(keyExpr, null) - 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) + 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) callback.run(query) } val queryableRawPtr = declareQueryableViaJNI( @@ -126,13 +131,14 @@ internal class JNISession { selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, - value: Value?, - attachment: ByteArray? - ): Result = runCatching { + payload: ZBytes?, + encoding: Encoding?, + attachment: ZBytes? + ): Result = runCatching { val getCallback = JNIGetCallback { replierId: String?, success: Boolean, @@ -158,8 +164,8 @@ internal class JNISession { Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), SampleKind.fromInt(kind), timestamp, - QoS(express, congestionControl, priority), - attachmentBytes + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() ) reply = Reply.Success(replierId?.let { ZenohID(it) }, sample) } @@ -169,13 +175,16 @@ internal class JNISession { replierId?.let { ZenohID(it) }, KeyExpr(keyExpr!!, null), timestamp, - attachmentBytes, - QoS(express, congestionControl, priority) + attachmentBytes?.into(), + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express) ) } } } 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) } @@ -190,11 +199,10 @@ internal class JNISession { timeout.toMillis(), target.ordinal, consolidation.ordinal, - attachment, - value != null, - value?.payload, - value?.encoding?.id?.ordinal ?: 0, - value?.encoding?.schema + attachment?.bytes, + payload?.bytes, + encoding?.id?.ordinal ?: ID.default().id, + encoding?.schema ) receiver } @@ -220,13 +228,13 @@ internal class JNISession { keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr.get(), - put.value.payload, + put.value.payload.bytes, put.value.encoding.id.ordinal, put.value.encoding.schema, put.qos.congestionControl.value, put.qos.priority.value, put.qos.express, - put.attachment + put.attachment?.bytes ) } @@ -242,7 +250,7 @@ internal class JNISession { delete.qos.congestionControl.value, delete.qos.priority.value, delete.qos.express, - delete.attachment + delete.attachment?.bytes ) } @@ -295,7 +303,7 @@ internal class JNISession { private external fun getViaJNI( keyExprPtr: Long, keyExprString: String, - selectorParams: String, + selectorParams: String?, sessionPtr: Long, callback: JNIGetCallback, onClose: JNIOnCloseCallback, @@ -303,7 +311,6 @@ 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 new file mode 100644 index 000000000..804cd07d5 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -0,0 +1,36 @@ +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 84ac7ca2f..31f5885f8 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,7 +17,6 @@ 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 41c7f6e11..e6654f088 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 An empty [Resolvable]. + * @return A [Result] with the operation status. */ - fun undeclare(session: Session): Resolvable { + fun undeclare(session: Session): Result { return session.undeclare(this) } @@ -128,6 +128,10 @@ 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 22e3fdc5f..4fa635b70 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt @@ -26,23 +26,7 @@ package io.zenoh.prelude * This is particularly useful in helping Zenoh to perform additional network optimizations. * */ -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 - } +data class Encoding(val id: ID, val schema: String? = null) { /** * The ID of the encoding. @@ -111,7 +95,8 @@ class Encoding(val id: ID, val schema: String? = null) { companion object { private val idToEnum = entries.associateBy(ID::id) - fun fromId(id: Int): ID? = idToEnum[id] + internal fun fromId(id: Int): ID? = idToEnum[id] + internal fun default() = ZENOH_BYTES } } } 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 1984006a3..c8f0420ea 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt @@ -17,52 +17,19 @@ 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. */ -class QoS internal constructor( - internal val express: Boolean, - internal val congestionControl: CongestionControl, - internal val priority: Priority +data class QoS ( + val congestionControl: CongestionControl = CongestionControl.DROP, + val priority: Priority = Priority.DATA, + val express: Boolean = false ) { - 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 { - 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 } + private val defaultQoS = QoS() - fun build() = QoS(express, congestionControl, priority) + fun default() = defaultQoS } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt new file mode 100644 index 000000000..9ad142452 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt @@ -0,0 +1,26 @@ +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 new file mode 100644 index 000000000..0c6bd8182 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..1cb2dc75e --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt @@ -0,0 +1,552 @@ +// +// 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 ea7785b8e..5406429e4 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt @@ -14,90 +14,17 @@ 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 to perform on Zenoh on a key expression. + * Delete operation. * - * 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`. + * @property keyExpr The [KeyExpr] for the delete operation. + * @property qos The [QoS] configuration. + * @property attachment Optional attachment. */ -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) - } - } -} +internal class Delete ( + val keyExpr: KeyExpr, val qos: QoS, val attachment: ZBytes? +) 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 2aee4315d..9db411a94 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt @@ -21,6 +21,7 @@ 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 /** @@ -31,35 +32,25 @@ import io.zenoh.value.Value * A publisher is automatically dropped when using it with the 'try-with-resources' statement (i.e. 'use' in Kotlin). * The session from which it was declared will also keep a reference to it and undeclare it once the session is closed. * - * In order to declare a publisher, [Session.declarePublisher] must be called, which returns a [Publisher.Builder] from - * which we can specify the [Priority], and the [CongestionControl]. - * - * Example: - * ``` + * Example of a publisher declaration: + * ```kotlin * val keyExpr = "demo/kotlin/greeting" * Session.open().onSuccess { * it.use { session -> * session * .declarePublisher(keyExpr) - * .priority(Priority.REALTIME) - * .congestionControl(CongestionControl.DROP) - * .res() * .onSuccess { pub -> - * pub.use { - * var i = 0 - * while (true) { - * pub.put("Hello for the ${i}th time!").res() - * Thread.sleep(1000) - * i++ - * } + * var i = 0 + * while (true) { + * pub.put("Hello for the ${i}th time!") + * Thread.sleep(1000) + * i++ * } * } * } * } * ``` * - * The publisher configuration parameters can be later changed using the setter functions. - * * ## Lifespan * * Internally, the [Session] from which the [Publisher] was declared keeps a reference to it, therefore keeping it alive @@ -70,10 +61,11 @@ import io.zenoh.value.Value * @property qos [QoS] configuration of the publisher. * @property jniPublisher Delegate class handling the communication with the native code. * @constructor Create empty Publisher with the default configuration. + * @see Session.declarePublisher */ class Publisher internal constructor( val keyExpr: KeyExpr, - private var qos: QoS, + val qos: QoS, private var jniPublisher: JNIPublisher?, ) : SessionDeclaration, AutoCloseable { @@ -81,28 +73,21 @@ 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) = Put(jniPublisher, value) + fun put(value: Value, attachment: ZBytes? = null) = jniPublisher?.put(value, attachment) ?: InvalidPublisherResult + /** Performs a PUT operation on the specified [keyExpr] with the specified string [value]. */ - fun put(value: String) = Put(jniPublisher, Value(value)) + fun put(value: String, attachment: ZBytes? = null) = jniPublisher?.put(Value(value), attachment) ?: InvalidPublisherResult /** * Performs a DELETE operation on the specified [keyExpr] - * - * @return A [Resolvable] operation. */ - fun delete() = Delete(jniPublisher) - - /** Get congestion control policy. */ - fun getCongestionControl(): CongestionControl { - return qos.congestionControl() - } - - /** Get priority policy. */ - fun getPriority(): Priority { - return qos.priority() - } + fun delete(attachment: ZBytes? = null) = jniPublisher?.delete(attachment) ?: InvalidPublisherResult fun isValid(): Boolean { return jniPublisher != null @@ -116,60 +101,5 @@ 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 a0339b909..93c7340a7 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt @@ -14,100 +14,22 @@ package io.zenoh.publication -import io.zenoh.Resolvable -import io.zenoh.Session import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.* +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes 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. */ -class Put private constructor( +internal data class Put ( val keyExpr: KeyExpr, val value: Value, val qos: QoS, - 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) } - } - } -} + val attachment: ZBytes? +) diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt deleted file mode 100644 index c0eacb1d0..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt +++ /dev/null @@ -1,195 +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.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 e1c9f0c8f..00c39661d 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt @@ -14,99 +14,51 @@ 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] operation and to a remote [Query]. + * Class to represent a Zenoh Reply to a get query and to a remote [Query]. * - * 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]. + * 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. * * Example: * ```kotlin - * session.declareQueryable(keyExpr).with { query -> - * query.reply(keyExpr) - * .success(Value("Hello")) - * .withTimeStamp(TimeStamp(Date.from(Instant.now()))) - * .res() - * }.res() - * ... + * 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() + * } + * } + * } + * } + * } + * } * ``` * - * **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. + * @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). */ -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) - - } +sealed class Reply private constructor(open val replierId: ZenohID?) : ZenohType { /** * A successful [Reply]. @@ -117,175 +69,45 @@ sealed class Reply private constructor(val replierId: ZenohID?) : ZenohType { * * @param replierId The replierId of the remotely generated reply. */ - 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() - } - } + data class Success internal constructor(override val replierId: ZenohID? = null, val sample: Sample) : Reply(replierId) { 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. - * @constructor The constructor is private since reply instances are created through JNI when receiving a reply to a query. - * + * @property error: value with the error information.* * @param replierId: unique ID identifying the replier. */ - 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() - } - } + data class Error internal constructor(override val replierId: ZenohID? = null, val error: Value) : Reply(replierId) { 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 keyExpr - * @constructor - * - * @param replierId + * @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. */ - class Delete internal constructor( - replierId: ZenohID?, + data class Delete internal constructor( + override val replierId: ZenohID? = null, val keyExpr: KeyExpr, val timestamp: TimeStamp?, - val attachment: ByteArray?, + val attachment: ZBytes?, val qos: QoS ) : Reply(replierId) { - 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() - } + override fun toString(): String { + return "Delete(keyexpr=$keyExpr)" } } } 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 01ae9b6fa..28af57d7a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt @@ -14,14 +14,18 @@ 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.query.Reply +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 /** * Represents a Zenoh Query in Kotlin. @@ -40,58 +44,92 @@ class Query internal constructor( val keyExpr: KeyExpr, val selector: Selector, val value: Value?, - val attachment: ByteArray?, + val attachment: ZBytes?, 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 to the specified key expression. + * 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. * * @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. - * @return a [Reply.Builder] + * @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. */ - fun reply(keyExpr: KeyExpr) = Reply.Builder(this, keyExpr) - - override fun close() { - jniQuery?.apply { - this.close() + 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?.let { + val result = it.replySuccess(sample) jniQuery = null - } + result + } ?: Result.failure(SessionException("Query is invalid")) } - protected fun finalize() { - close() + /** + * 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?.let { + val result = it.replyError(error) + jniQuery = null + result + } ?: Result.failure(SessionException("Query is invalid")) } /** - * Perform a reply operation to the remote [Query]. + * Perform a delete 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 reply The [Reply] to the Query. - * @return A [Resolvable] that returns a [Result] with the status of the reply operation. + * @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. */ - internal fun reply(reply: Reply): Resolvable = Resolvable { + fun replyDelete( + keyExpr: KeyExpr, + qos: QoS = QoS.default(), + timestamp: TimeStamp? = null, + attachment: ZBytes? = null + ): Result { + return jniQuery?.let { + val result = it.replyDelete(keyExpr, timestamp, attachment, qos) + jniQuery = null + result + } ?: Result.failure(SessionException("Query is invalid")) + } + + override fun close() { jniQuery?.apply { - 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) - } - } + this.close() jniQuery = null - return@Resolvable result } - 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 5e07f0900..8ac784044 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt @@ -15,8 +15,6 @@ 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 @@ -34,7 +32,7 @@ import kotlinx.coroutines.channels.Channel * Session.open().onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * session.declareQueryable(keyExpr).res().onSuccess { queryable -> + * session.declareQueryable(keyExpr).wait().onSuccess { queryable -> * queryable.use { * it.receiver?.let { receiverChannel -> * runBlocking { @@ -46,7 +44,7 @@ import kotlinx.coroutines.channels.Channel * .success("Hello!") * .withKind(SampleKind.PUT) * .withTimeStamp(TimeStamp.getCurrentTime()) - * .res() + * .wait() * .onSuccess { println("Replied hello.") } * .onFailure { println(it) } * } @@ -64,15 +62,14 @@ import kotlinx.coroutines.channels.Channel * until the session is closed. For the cases where we want to stop the queryable earlier, it's necessary * to keep a reference to it in order to undeclare it later. * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, [R] will be [Unit]. + * @param R Receiver type of the [Handler] implementation. * @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. Instances of Queryable must be created through the [Builder] obtained after - * calling [Session.declareQueryable] or alternatively through [newBuilder]. + * @see Session.declareQueryable */ 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 { @@ -87,97 +84,5 @@ 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 e94206782..a69c098e3 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt @@ -18,6 +18,7 @@ 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 @@ -34,35 +35,11 @@ import org.apache.commons.net.ntp.TimeStamp * @property qos The Quality of Service settings used to deliver the sample. * @property attachment Optional attachment. */ -class Sample( +data class Sample( val keyExpr: KeyExpr, val value: Value, val kind: SampleKind, val timestamp: TimeStamp?, val qos: QoS, - 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 - } -} + val attachment: ZBytes? = null +): ZenohType 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 ff637cb73..78d71b11a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt @@ -28,23 +28,28 @@ 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 = "") : AutoCloseable { +class Selector(val keyExpr: KeyExpr, val parameters: String? = null) : AutoCloseable { - /** 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) + /** + * 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) } } } override fun toString(): String { - return if (parameters.isEmpty()) "$keyExpr" else "$keyExpr?$parameters" + return parameters?.let { "$keyExpr?$parameters" } ?: keyExpr.toString() } /** 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 bc47f36b7..d675758eb 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.publication.CongestionControl.DROP] option, + * Note that if a publisher puts a sample with the [io.zenoh.prelude.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 6670c38dc..ce8b83be3 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt @@ -15,47 +15,22 @@ 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 /** * # Subscriber * * A subscriber that allows listening to updates on a key expression and reacting to changes. * - * Its main purpose is to keep the subscription active as long as it exists. - * - * Example using the default [Channel] handler: - * + * Simple example using a callback to handle the received samples: * ```kotlin - * 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) - * } - * } - * } - * } - * } - * } - * } + * val session = Session.open().getOrThrow() + * val keyexpr = "a/b/c".intoKeyExpr().getOrThrow() + * session.declareSubscriber(keyexpr, callback = { sample -> + * println(">> [Subscriber] Received $sample") + * }) * ``` * * ## Lifespan @@ -64,15 +39,14 @@ import kotlinx.coroutines.channels.Channel * until the session is closed. For the cases where we want to stop the subscriber earlier, it's necessary * to keep a reference to it in order to undeclare it later. * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. + * @param R Receiver type of the [Handler] implementation. * @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 jniSubscriber Delegate object in charge of communicating with the underlying native code. - * @constructor Internal constructor. Instances of Subscriber must be created through the [Builder] obtained after - * calling [Session.declareSubscriber] or alternatively through [newBuilder]. + * @see Session.declareSubscriber */ 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 { @@ -87,102 +61,4 @@ 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 ba787bcbc..0b2168dd1 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt @@ -15,6 +15,9 @@ 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. @@ -24,17 +27,56 @@ import io.zenoh.prelude.Encoding * @property payload The payload of this Value. * @property encoding An encoding description indicating how the associated payload is encoded. */ -class Value(val payload: ByteArray, val encoding: Encoding) { +class Value(val payload: ZBytes, val encoding: Encoding) { /** * Constructs a value with the provided message, using [Encoding.ID.TEXT_PLAIN] for encoding. */ - constructor(message: String): this(message.toByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)) + constructor(message: String): this(message.toByteArray().into(), Encoding(Encoding.ID.TEXT_PLAIN)) /** * Constructs a value with the provided message and encoding. */ - constructor(message: String, encoding: Encoding): this(message.toByteArray(), 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)) + companion object { @@ -44,9 +86,7 @@ class Value(val payload: ByteArray, val encoding: Encoding) { } } - override fun toString(): String { - return payload.decodeToString() - } + override fun toString(): String = payload.toString() override fun equals(other: Any?): Boolean { if (this === other) return true @@ -54,12 +94,13 @@ class Value(val payload: ByteArray, val encoding: Encoding) { other as Value - if (!payload.contentEquals(other.payload)) return false + if (payload != other.payload) return false + return encoding == other.encoding } override fun hashCode(): Int { - var result = payload.contentHashCode() + var result = payload.bytes.hashCode() 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 223008c71..884f64fd5 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).with { sample -> receivedSample = sample }.res().getOrThrow() - session.delete(keyExpr).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + session.delete(keyExpr) 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 cba59dc81..7d3152cbb 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt @@ -4,6 +4,7 @@ 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.* @@ -16,11 +17,11 @@ class EncodingTest { // Testing non null schema var receivedSample: Sample? = null - val subscriber = session.declareSubscriber(keyExpr).with { sample -> + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample - }.res().getOrThrow() + }).getOrThrow() var value = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) - session.put(keyExpr, value).res() + session.put(keyExpr, value) Thread.sleep(200) assertNotNull(receivedSample) @@ -30,7 +31,7 @@ class EncodingTest { // Testing null schema receivedSample = null value = Value("test2", Encoding(Encoding.ID.ZENOH_STRING, null)) - session.put(keyExpr, value).res() + session.put(keyExpr, value) Thread.sleep(200) assertNotNull(receivedSample) @@ -45,25 +46,25 @@ class EncodingTest { fun encoding_replySuccessTest() { val session = Session.open().getOrThrow() val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() - val test1 = "example/testing/reply_success".intoKeyExpr().getOrThrow() - val test2 = "example/testing/reply_success_with_schema".intoKeyExpr().getOrThrow() + val test1 = "example/testing/reply_success".intoSelector().getOrThrow() + val test2 = "example/testing/reply_success_with_schema".intoSelector().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).with { query -> + val queryable = session.declareQueryable(keyExpr, callback = { query -> when (query.keyExpr) { - test1 -> query.reply(query.keyExpr).success(testValueA).res() - test2 -> query.reply(query.keyExpr).success(testValueB).res() + test1.keyExpr -> query.replySuccess(query.keyExpr, value = testValueA) + test2.keyExpr -> query.replySuccess(query.keyExpr, value = testValueB) } - }.res().getOrThrow() + }).getOrThrow() // Testing with null schema on a reply success scenario. var receivedSample: Sample? = null - session.get(test1).with { reply -> + session.get(test1, callback = { reply -> assertTrue(reply is Reply.Success) receivedSample = reply.sample - }.res().getOrThrow() + }).getOrThrow() Thread.sleep(200) assertNotNull(receivedSample) @@ -72,10 +73,10 @@ class EncodingTest { // Testing with non-null schema on a reply success scenario. receivedSample = null - session.get(test2).with { reply -> + session.get(test2, callback = { reply -> assertTrue(reply is Reply.Success) receivedSample = reply.sample - }.res().getOrThrow() + }).getOrThrow() Thread.sleep(200) assertNotNull(receivedSample) @@ -91,25 +92,25 @@ class EncodingTest { val session = Session.open().getOrThrow() val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() - val test1 = "example/testing/reply_error".intoKeyExpr().getOrThrow() - val test2 = "example/testing/reply_error_with_schema".intoKeyExpr().getOrThrow() + val test1 = "example/testing/reply_error".intoSelector().getOrThrow() + val test2 = "example/testing/reply_error_with_schema".intoSelector().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).with { query -> + val queryable = session.declareQueryable(keyExpr, callback = { query -> when (query.keyExpr) { - test1 -> query.reply(query.keyExpr).error(testValueA).res() - test2 -> query.reply(query.keyExpr).error(testValueB).res() + test1.keyExpr -> query.replyError(testValueA) + test2.keyExpr -> query.replyError(testValueB) } - }.res().getOrThrow() + }).getOrThrow() // Testing with null schema on a reply error scenario. var errorValue: Value? = null - session.get(test1).with { reply -> + session.get(test1, callback = { reply -> assertTrue(reply is Reply.Error) errorValue = reply.error - }.res().getOrThrow() + }).getOrThrow() Thread.sleep(200) assertNotNull(errorValue) @@ -118,10 +119,10 @@ class EncodingTest { // Testing with non-null schema on a reply error scenario. errorValue = null - session.get(test2).with { reply -> + session.get(test2, callback = { reply -> assertTrue(reply is Reply.Error) errorValue = reply.error - }.res().getOrThrow() + }).getOrThrow() Thread.sleep(200) assertNotNull(errorValue) @@ -135,32 +136,32 @@ class EncodingTest { @Test fun encoding_queryTest() { val session = Session.open().getOrThrow() - 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")) + val selector = "example/testing/keyexpr".intoSelector().getOrThrow() + val encodingA = Encoding(Encoding.ID.TEXT_CSV, null) + val encodingB = Encoding(Encoding.ID.TEXT_CSV, "test_schema") - var receivedValue: Value? = null - val queryable = session.declareQueryable(keyExpr).with { query -> - receivedValue = query.value + var receivedEncoding: Encoding? = null + val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> + receivedEncoding = query.encoding query.close() - }.res().getOrThrow() + }).getOrThrow() // Testing with null schema - session.get(keyExpr).withValue(testValueA).res() + session.get(selector, callback = {}, value = Value("test", encodingA)) Thread.sleep(200) - assertNotNull(receivedValue) - assertEquals(Encoding.ID.TEXT_CSV, receivedValue!!.encoding.id) - assertNull(receivedValue!!.encoding.schema) + assertNotNull(receivedEncoding) + assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) + assertNull(receivedEncoding!!.schema) // Testing non-null schema - receivedValue = null - session.get(keyExpr).withValue(testValueB).res() + receivedEncoding = null + session.get(selector, callback = {}, value = Value("test", encodingB)) Thread.sleep(200) - assertNotNull(receivedValue) - assertEquals(Encoding.ID.TEXT_CSV, receivedValue!!.encoding.id) - assertEquals("test_schema", receivedValue!!.encoding.schema) + assertNotNull(receivedEncoding) + assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) + assertEquals("test_schema", receivedEncoding!!.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 42617dbde..c975e76d3 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt @@ -15,12 +15,11 @@ 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 @@ -36,47 +35,43 @@ class GetTest { } private lateinit var session: Session - private lateinit var keyExpr: KeyExpr + private lateinit var selector: Selector private lateinit var queryable: Queryable @BeforeTest fun setUp() { session = Session.open().getOrThrow() - keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(query.keyExpr) - .success(value) - .timestamp(timestamp) - .res() - }.res().getOrThrow() + selector = "example/testing/keyexpr".intoSelector().getOrThrow() + queryable = session.declareQueryable(selector.keyExpr, callback = { query -> + query.replySuccess(query.keyExpr, value, timestamp = timestamp) + }).getOrThrow() } @AfterTest fun tearDown() { session.close() - keyExpr.close() + selector.close() queryable.close() } @Test fun get_runsWithCallback() { var reply: Reply? = null - session.get(keyExpr).with { + session.get(selector, callback = { reply = it - }.timeout(Duration.ofMillis(1000)).res() + }, timeout = Duration.ofMillis(1000)) assertTrue(reply is Reply.Success) val sample = (reply as Reply.Success).sample assertEquals(value, sample.value) assertEquals(kind, sample.kind) - assertEquals(keyExpr, sample.keyExpr) + assertEquals(selector.keyExpr, sample.keyExpr) assertEquals(timestamp, sample.timestamp) } @Test fun get_runsWithHandler() { - val receiver: ArrayList = session.get(keyExpr).with(TestHandler()) - .timeout(Duration.ofMillis(1000)).res().getOrThrow()!! + val receiver: ArrayList = session.get(selector, handler = TestHandler(), timeout = Duration.ofMillis(1000)).getOrThrow() for (reply in receiver) { reply as Reply.Success @@ -89,17 +84,17 @@ class GetTest { @Test fun getWithSelectorParamsTest() { - var receivedParams = String() - var receivedParamsMap = mapOf() - val queryable = session.declareQueryable(keyExpr).with { it.use { query -> + var receivedParams: String? = null + var receivedParamsMap : Map? = null + val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> receivedParams = query.parameters - receivedParamsMap = query.selector.parametersStringMap().getOrThrow() - }}.res().getOrThrow() + receivedParamsMap = query.selector.parametersStringMap()?.getOrThrow() + }).getOrThrow() val params = "arg1=val1&arg2=val2&arg3" val paramsMap = mapOf("arg1" to "val1", "arg2" to "val2", "arg3" to "") - val selector = Selector(keyExpr, params) - session.get(selector).with {}.timeout(Duration.ofMillis(1000)).res() + val selectorWithParams = Selector(selector.keyExpr, params) + session.get(selectorWithParams, callback = {}, timeout = Duration.ofMillis(1000)) 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 ee589caa5..08d605045 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").res().getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").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").res().getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").getOrThrow() assertEquals("a/b/c", keyExpr.toString()) - val undeclare1 = session.undeclare(keyExpr).res() + val undeclare1 = session.undeclare(keyExpr) assertTrue(undeclare1.isSuccess) // Undeclaring twice a key expression shall fail. - val undeclare2 = session.undeclare(keyExpr).res() + val undeclare2 = session.undeclare(keyExpr) 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).res() + val undeclare3 = session.undeclare(keyExpr2) 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 e90eabfbc..44156ed0d 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).res().getOrThrow() - subscriber = session.declareSubscriber(keyExpr).with { sample -> + publisher = session.declarePublisher(keyExpr).getOrThrow() + subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSamples.add(sample) - }.res().getOrThrow() + }).getOrThrow() receivedSamples = ArrayList() } @@ -55,12 +55,12 @@ class PublisherTest { fun putTest() { val testValues = arrayListOf( - 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)) + Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) ) - testValues.forEach() { value -> publisher.put(value).res() } + testValues.forEach() { value -> publisher.put(value) } assertEquals(receivedSamples.size, testValues.size) for ((index, sample) in receivedSamples.withIndex()) { @@ -70,7 +70,7 @@ class PublisherTest { @Test fun deleteTest() { - publisher.delete().res() + publisher.delete() 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 cc26c77e3..541b39edb 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).with { sample -> receivedSample = sample }.res().getOrThrow() - val value = Value(TEST_PAYLOAD.toByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)) - session.put(keyExpr, value).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + val value = Value(TEST_PAYLOAD, Encoding(Encoding.ID.TEXT_PLAIN)) + session.put(keyExpr, value) 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 5b86612c9..69101eb1c 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt @@ -17,10 +17,9 @@ package io.zenoh import io.zenoh.handlers.Handler import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS +import io.zenoh.prelude.* +import io.zenoh.prelude.Encoding.ID.ZENOH_STRING +import io.zenoh.protocol.into import io.zenoh.query.Reply import io.zenoh.queryable.Query import io.zenoh.sample.Sample @@ -65,16 +64,16 @@ class QueryableTest { Value(testPayload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), - QoS.default() + QoS() ) - val queryable = session.declareQueryable(testKeyExpr).with { query -> - query.reply(testKeyExpr).success(sample.value).timestamp(sample.timestamp!!).res() - }.res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replySuccess(testKeyExpr, value = sample.value, timestamp = sample.timestamp) + }).getOrThrow() var reply: Reply? = null val delay = Duration.ofMillis(1000) withTimeout(delay) { - session.get(testKeyExpr).with { reply = it }.timeout(delay).res() + session.get(testKeyExpr.intoSelector(), callback = { reply = it }, timeout = delay) } assertTrue(reply is Reply.Success) @@ -86,14 +85,14 @@ class QueryableTest { @Test fun queryable_runsWithHandler() = runBlocking { val handler = QueryHandler() - val queryable = session.declareQueryable(testKeyExpr).with(handler).res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, handler = handler).getOrThrow() delay(500) val receivedReplies = ArrayList() - session.get(testKeyExpr).with { reply: Reply -> + session.get(testKeyExpr.intoSelector(), callback = { reply: Reply -> receivedReplies.add(reply) - }.res() + }) delay(500) @@ -102,61 +101,54 @@ 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).with { query -> receivedQuery = query }.res().getOrThrow() + val queryable = + session.declareQueryable(testKeyExpr, callback = { query -> receivedQuery = query }).getOrThrow() - session.get(testKeyExpr).res() + session.get(testKeyExpr.intoSelector(), callback = {}) - delay(1000) - queryable.close() + delay(100) assertNotNull(receivedQuery) - assertNull(receivedQuery!!.value) - } + assertNull(receivedQuery!!.payload) + assertNull(receivedQuery!!.encoding) + assertNull(receivedQuery!!.attachment) - @Test - fun queryWithValueTest() = runBlocking { - var receivedQuery: Query? = null - val queryable = session.declareQueryable(testKeyExpr).with { query -> receivedQuery = query }.res().getOrThrow() + receivedQuery = null + val payload = "Test value" + val attachment = "Attachment".into() + session.get(testKeyExpr.intoSelector(), callback = {}, value = Value(payload, ZENOH_STRING), attachment = attachment) - session.get(testKeyExpr).withValue("Test value").res() + delay(100) + assertNotNull(receivedQuery) + assertEquals(payload, receivedQuery!!.payload!!.bytes.decodeToString()) + assertEquals(ZENOH_STRING, receivedQuery!!.encoding!!.id) + assertEquals(attachment, receivedQuery!!.attachment) - 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).with { - it.use { query -> - query.reply(testKeyExpr).success(message).timestamp(timestamp).priority(priority).express(express) - .congestionControl(congestionControl).res() - } - }.res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replySuccess(testKeyExpr, value = Value(message), timestamp = timestamp, qos = qos) + }).getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) queryable.close() assertTrue(receivedReply is Reply.Success) val reply = receivedReply as Reply.Success - assertEquals(message, reply.sample.value.payload.decodeToString()) + assertEquals(message, reply.sample.value.payload.bytes.decodeToString()) assertEquals(timestamp, reply.sample.timestamp) assertEquals(priority, reply.sample.qos.priority) assertEquals(express, reply.sample.qos.express) @@ -166,14 +158,12 @@ class QueryableTest { @Test fun queryReplyErrorTest() { val message = "Error message" - val queryable = session.declareQueryable(testKeyExpr).with { - it.use { query -> - query.reply(testKeyExpr).error(Value(message)).res() - } - }.res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replyError(error = Value(message)) + }).getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) Thread.sleep(1000) queryable.close() @@ -181,7 +171,7 @@ class QueryableTest { assertNotNull(receivedReply) assertTrue(receivedReply is Reply.Error) val reply = receivedReply as Reply.Error - assertEquals(message, reply.error.payload.decodeToString()) + assertEquals(message, reply.error.payload.bytes.decodeToString()) } @Test @@ -190,15 +180,13 @@ class QueryableTest { val priority = Priority.DATA_HIGH val express = true val 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 qos = QoS(priority = Priority.DATA_HIGH, express = true, congestionControl = CongestionControl.DROP) + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replyDelete(testKeyExpr, timestamp = timestamp, qos = qos) + }).getOrThrow() var receivedReply: Reply? = null - session.get(testKeyExpr).with { receivedReply = it }.timeout(Duration.ofMillis(10)).res() + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) queryable.close() @@ -215,11 +203,13 @@ class QueryableTest { @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val queryable = session.declareQueryable(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() + val channel = Channel() + val queryable = + session.declareQueryable(testKeyExpr, channel = channel, onClose = { onCloseWasCalled = true }).getOrThrow() queryable.undeclare() assertTrue(onCloseWasCalled) - assertTrue(queryable.receiver!!.isClosedForReceive) + assertTrue(queryable.receiver.isClosedForReceive) } } @@ -248,9 +238,9 @@ private class QueryHandler : Handler { Value(payload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), - QoS.default() + QoS() ) performedReplies.add(sample) - query.reply(query.keyExpr).success(sample.value).timestamp(sample.timestamp!!).res() + query.replySuccess(query.keyExpr, value = sample.value, timestamp = sample.timestamp) } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt index 48f69ee9a..336e404f9 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).with {}.res().getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() - val publisher = session.declarePublisher(testKeyExpr).res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).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).res().getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr).res().getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() session.close() assertFalse(publisher.isValid()) assertFalse(subscriber.isValid()) - assertTrue(publisher.put("Test").res().isFailure) + assertTrue(publisher.put("Test").isFailure) } @Test fun sessionClose_newDeclarationsReturnNullAfterClosingSession() { val session = Session.open().getOrThrow() session.close() - assertFailsWith { session.declarePublisher(testKeyExpr).res().getOrThrow() } - assertFailsWith { session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() } - assertFailsWith { session.declareQueryable(testKeyExpr).with {}.res().getOrThrow() } + assertFailsWith { session.declarePublisher(testKeyExpr).getOrThrow() } + assertFailsWith { session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() } + assertFailsWith { session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() } } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt index a58f49edc..ea968daba 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt @@ -22,6 +22,7 @@ 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 @@ -33,13 +34,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".encodeToByteArray(), Encoding(Encoding.ID.TEXT_PLAIN)), - Value("Test 2".encodeToByteArray(), Encoding(Encoding.ID.TEXT_JSON)), - Value("Test 3".encodeToByteArray(), Encoding(Encoding.ID.TEXT_CSV)) + Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) ) } @@ -62,20 +63,17 @@ class SubscriberTest { fun subscriber_runsWithCallback() { val receivedSamples = ArrayList() val subscriber = - session.declareSubscriber(testKeyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() + session.declareSubscriber(testKeyExpr, callback = { sample -> receivedSamples.add(sample)}).getOrThrow() testValues.forEach { value -> - session.put(testKeyExpr, value) - .priority(TEST_PRIORITY) - .congestionControl(TEST_CONGESTION_CONTROL) - .res() + session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) } 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() @@ -84,32 +82,22 @@ class SubscriberTest { @Test fun subscriber_runsWithHandler() { val handler = QueueHandler() - val subscriber = session.declareSubscriber(testKeyExpr).with(handler).res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, handler = handler).getOrThrow() - testValues.forEach { value -> - session.put(testKeyExpr, value) - .priority(TEST_PRIORITY) - .congestionControl(TEST_CONGESTION_CONTROL) - .res() + testValues.forEach { value -> + session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) } 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. @@ -117,8 +105,8 @@ class SubscriberTest { val session = Session.open().getOrThrow() val receivedSamples = ArrayList() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() - testValues.forEach { value -> session.put(testKeyExpr, value).res() } + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSamples.add(sample) }).getOrThrow() + testValues.forEach { value -> session.put(testKeyExpr, value) } subscriber.close() assertEquals(receivedSamples.size, testValues.size) @@ -132,11 +120,11 @@ class SubscriberTest { @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val subscriber = session.declareSubscriber(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, channel = Channel(), onClose = { onCloseWasCalled = true }).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 e4e233cd1..5413affe4 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt @@ -17,6 +17,7 @@ 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 @@ -32,7 +33,7 @@ class UserAttachmentTest { val value = Value("test", Encoding(Encoding.ID.TEXT_PLAIN)) const val keyExprString = "example/testing/attachment" const val attachment = "mock_attachment" - val attachmentBytes = attachment.toByteArray() + val attachmentZBytes = ZBytes.from(attachment) } @BeforeTest @@ -50,41 +51,43 @@ class UserAttachmentTest { @Test fun putWithAttachmentTest() { var receivedSample: Sample? = null - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - session.put(keyExpr, value).withAttachment(attachmentBytes).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + session.put(keyExpr, value, attachment = attachmentZBytes) subscriber.close() assertNotNull(receivedSample) { - assertEquals(attachment, it.attachment!!.decodeToString()) + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.toString()) } } @Test fun publisherPutWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample - }.res().getOrThrow() + }).getOrThrow() - publisher.put("test").withAttachment(attachmentBytes).res() + publisher.put("test", attachment = attachmentZBytes) publisher.close() subscriber.close() assertNotNull(receivedSample) { - assertEquals(attachment, it.attachment!!.decodeToString()) + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.deserialize().getOrNull()) } } @Test fun publisherPutWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.put("test").res() + publisher.put("test") publisher.close() subscriber.close() @@ -97,26 +100,27 @@ class UserAttachmentTest { @Test fun publisherDeleteWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.delete().withAttachment(attachmentBytes).res() + publisher.delete(attachment = attachmentZBytes) publisher.close() subscriber.close() assertNotNull(receivedSample) { - assertEquals(attachment, it.attachment!!.decodeToString()) + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.toString()) } } @Test fun publisherDeleteWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.delete().res() + publisher.delete() publisher.close() subscriber.close() @@ -128,51 +132,52 @@ class UserAttachmentTest { @Test fun queryWithAttachmentTest() { - var receivedAttachment: ByteArray? = null - val queryable = session.declareQueryable(keyExpr).with { query -> + var receivedAttachment: ZBytes? = null + val queryable = session.declareQueryable(keyExpr, callback = { query -> receivedAttachment = query.attachment - query.reply(keyExpr).success("test").res() - }.res().getOrThrow() + query.replySuccess(keyExpr, value = Value("test")) + }).getOrThrow() - session.get(keyExpr).with {}.withAttachment(attachmentBytes).timeout(Duration.ofMillis(1000)).res().getOrThrow() + session.get(keyExpr.intoSelector(), callback = {}, attachment = attachmentZBytes, timeout = Duration.ofMillis(1000)).getOrThrow() queryable.close() assertNotNull(receivedAttachment) { - assertEquals(attachment, it.decodeToString()) + assertEquals(attachmentZBytes, it) } } @Test fun queryReplyWithAttachmentTest() { var reply: Reply? = null - val queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(keyExpr).success("test").attachment(attachmentBytes).res() - }.res().getOrThrow() + val queryable = session.declareQueryable(keyExpr, callback = { query -> + query.replySuccess(keyExpr, value = Value("test"), attachment = attachmentZBytes) + }).getOrThrow() - session.get(keyExpr).with { + session.get(keyExpr.intoSelector(), callback = { if (it is Reply.Success) { reply = it } - }.timeout(Duration.ofMillis(1000)).res().getOrThrow() + }, timeout = Duration.ofMillis(1000)).getOrThrow() queryable.close() assertNotNull(reply) { - assertEquals(attachment, (it as Reply.Success).sample.attachment!!.decodeToString()) + val receivedAttachment = (it as Reply.Success).sample.attachment!! + assertEquals(attachment, receivedAttachment.toString()) } } @Test fun queryReplyWithoutAttachmentTest() { var reply: Reply? = null - val queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(keyExpr).success("test").res() - }.res().getOrThrow() + val queryable = session.declareQueryable(keyExpr, callback = { query -> + query.replySuccess(keyExpr, value = Value("test")) + }).getOrThrow() - session.get(keyExpr).with { + session.get(keyExpr.intoSelector(), callback = { reply = it - }.timeout(Duration.ofMillis(1000)).res().getOrThrow() + }, timeout = Duration.ofMillis(1000)).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 new file mode 100644 index 000000000..6320cd5ce --- /dev/null +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt @@ -0,0 +1,591 @@ +// +// 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