use std::{
future::{IntoFuture, Ready},
marker::PhantomData,
time::Duration,
};
use crate::bus::pubsub::Stamped;
use serde::{Serialize, de::DeserializeOwned};
use zenoh::{
Error, Result as ZResult, Wait,
bytes::{Encoding, ZBytes},
handlers::{Callback, DefaultHandler, FifoChannelHandler},
key_expr::KeyExpr,
matching::MatchingStatus,
pubsub::{Publisher, PublisherBuilder, Subscriber, SubscriberBuilder},
query::{Query, QueryConsolidation, QueryTarget, Queryable, QueryableBuilder, ReplyKeyExpr},
sample::{Locality, Sample},
session::Session,
};
type TypedDecodeError = rmp_serde::decode::Error;
fn typed_encoding(schema_name: &str) -> Encoding {
Encoding::from(format!("zenoh-ext/typed:{schema_name}"))
}
fn serialize_payload<T: Serialize>(value: &T) -> ZResult<ZBytes> {
rmp_serde::to_vec_named(value)
.map(ZBytes::from)
.map_err(Into::into)
}
fn deserialize_payload<T: DeserializeOwned>(payload: &ZBytes) -> Result<T, TypedDecodeError> {
rmp_serde::from_slice(payload.to_bytes().as_ref())
}
pub trait TypedSchema {
const SCHEMA_NAME: &'static str;
const SCHEMA_VERSION: u32;
}
pub trait BusyResponse {
fn busy() -> Self;
}
impl TypedSchema for () {
const SCHEMA_NAME: &'static str = "unit";
const SCHEMA_VERSION: u32 = 1;
}
impl<T: TypedSchema> TypedSchema for Stamped<T> {
const SCHEMA_NAME: &'static str = T::SCHEMA_NAME;
const SCHEMA_VERSION: u32 = T::SCHEMA_VERSION;
}
#[derive(Debug)]
pub enum TypedPayloadError {
MissingPayload,
MissingSchemaVersion { expected: u32, type_name: String },
InvalidSchemaVersion { expected: u32, type_name: String },
EncodingMismatch { expected: String, received: String },
VersionMismatch {
expected: u32,
received: u32,
type_name: String,
},
DeserializationFailed(TypedDecodeError),
}
impl std::fmt::Display for TypedPayloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingPayload => write!(f, "missing payload"),
Self::MissingSchemaVersion {
expected,
type_name,
} => write!(
f,
"missing schema version for {type_name}: expected {expected}"
),
Self::InvalidSchemaVersion {
expected,
type_name,
} => write!(
f,
"invalid schema version for {type_name}: expected {expected}"
),
Self::EncodingMismatch { expected, received } => write!(
f,
"encoding mismatch: expected {expected}, received {received}"
),
Self::VersionMismatch {
expected,
received,
type_name,
} => write!(
f,
"schema version mismatch for {type_name}: expected {expected}, received {received}"
),
Self::DeserializationFailed(e) => write!(f, "deserialization failed: {e}"),
}
}
}
impl std::error::Error for TypedPayloadError {}
impl From<TypedDecodeError> for TypedPayloadError {
fn from(e: TypedDecodeError) -> Self {
Self::DeserializationFailed(e)
}
}
fn encode_schema_version(version: u32) -> ZBytes {
ZBytes::from(version.to_le_bytes().to_vec())
}
fn decode_schema_version(zbytes: &ZBytes) -> Option<u32> {
let bytes = zbytes.to_bytes();
if bytes.len() == 4 {
Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
} else {
None
}
}
fn check_schema_version(
attachment: Option<&ZBytes>,
expected_version: Option<u32>,
schema_name: &str,
) -> Result<(), TypedPayloadError> {
let expected = match expected_version {
Some(v) => v,
None => return Ok(()),
};
let Some(attachment) = attachment else {
return Err(TypedPayloadError::MissingSchemaVersion {
expected,
type_name: schema_name.to_string(),
});
};
let Some(received) = decode_schema_version(attachment) else {
return Err(TypedPayloadError::InvalidSchemaVersion {
expected,
type_name: schema_name.to_string(),
});
};
if received != expected {
return Err(TypedPayloadError::VersionMismatch {
expected,
received,
type_name: schema_name.to_string(),
});
}
Ok(())
}
fn check_encoding(received: Option<&Encoding>, schema_name: &str) -> Result<(), TypedPayloadError> {
let expected = typed_encoding(schema_name);
let Some(received) = received else {
return Err(TypedPayloadError::EncodingMismatch {
expected: expected.to_string(),
received: "<none>".to_string(),
});
};
if *received != expected {
return Err(TypedPayloadError::EncodingMismatch {
expected: expected.to_string(),
received: received.to_string(),
});
}
Ok(())
}
#[derive(Debug)]
pub enum TypedGetError {
ReplyError(ZBytes),
Payload(TypedPayloadError),
}
impl std::fmt::Display for TypedGetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReplyError(_) => write!(f, "remote replied with error"),
Self::Payload(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for TypedGetError {}
impl From<TypedPayloadError> for TypedGetError {
fn from(e: TypedPayloadError) -> Self {
Self::Payload(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishPolicy {
Lazy,
Eager,
}
pub struct TypedPublisher<'a, T: Serialize + TypedSchema> {
inner: Publisher<'a>,
schema_version: Option<u32>,
publish_policy: PublishPolicy,
_phantom: PhantomData<T>,
}
impl<T: Serialize + TypedSchema> TypedPublisher<'_, T> {
pub async fn put(&self, payload: &T) -> ZResult<()> {
if let Ok(false) = self.should_publish().await {
return Ok(());
}
let payload = serialize_payload(payload)?;
let mut inner = self.inner.put(payload);
if let Some(version) = self.schema_version {
inner = inner.attachment(encode_schema_version(version));
}
inner.await
}
pub fn publish_policy(&self) -> PublishPolicy {
self.publish_policy
}
pub async fn has_matching_subscribers(&self) -> ZResult<bool> {
self.matching_status().await.map(|status| status.matching())
}
pub async fn should_publish(&self) -> ZResult<bool> {
match self.publish_policy {
PublishPolicy::Lazy => self.has_matching_subscribers().await,
PublishPolicy::Eager => Ok(true),
}
}
pub async fn matching_status(&self) -> ZResult<MatchingStatus> {
self.inner.matching_status().await
}
pub fn key_expr(&self) -> &KeyExpr<'_> {
self.inner.key_expr()
}
pub fn encoding(&self) -> &Encoding {
self.inner.encoding()
}
}
pub struct TypedSubscriber<T: DeserializeOwned + TypedSchema, Handler = FifoChannelHandler<Sample>>
{
inner: Subscriber<Handler>,
schema_version: Option<u32>,
_phantom: PhantomData<T>,
}
impl<T: DeserializeOwned + TypedSchema> TypedSubscriber<T, FifoChannelHandler<Sample>> {
pub async fn recv_async(&self) -> ZResult<Result<T, TypedPayloadError>> {
let sample = self.inner.recv_async().await?;
Ok(self.deserialize_sample(&sample))
}
pub fn recv(&self) -> ZResult<Result<T, TypedPayloadError>> {
let sample = self.inner.recv()?;
Ok(self.deserialize_sample(&sample))
}
}
impl<T: DeserializeOwned + TypedSchema, Handler> TypedSubscriber<T, Handler> {
fn deserialize_sample(&self, sample: &Sample) -> Result<T, TypedPayloadError> {
check_encoding(Some(sample.encoding()), T::SCHEMA_NAME)?;
check_schema_version(sample.attachment(), self.schema_version, T::SCHEMA_NAME)?;
deserialize_payload::<T>(sample.payload()).map_err(TypedPayloadError::from)
}
}
impl<T: DeserializeOwned + TypedSchema, Handler> TypedSubscriber<T, Handler> {
pub fn key_expr(&self) -> &KeyExpr<'static> {
self.inner.key_expr()
}
pub fn handler(&self) -> &Handler {
self.inner.handler()
}
}
pub struct TypedPublisherBuilder<'a, 'b, T: Serialize + TypedSchema> {
inner: PublisherBuilder<'a, 'b>,
schema_version: Option<u32>,
publish_policy: PublishPolicy,
_phantom: PhantomData<T>,
}
impl<'a, 'b, T: Serialize + TypedSchema> TypedPublisherBuilder<'a, 'b, T> {
pub fn schema_version(mut self, version: u32) -> Self {
self.schema_version = Some(version);
self
}
pub fn lazy(self) -> Self {
self.publish_policy(PublishPolicy::Lazy)
}
pub fn eager(self) -> Self {
self.publish_policy(PublishPolicy::Eager)
}
pub fn publish_policy(mut self, publish_policy: PublishPolicy) -> Self {
self.publish_policy = publish_policy;
self
}
pub fn allowed_destination(self, destination: Locality) -> Self {
Self {
inner: self.inner.allowed_destination(destination),
schema_version: self.schema_version,
publish_policy: self.publish_policy,
_phantom: self._phantom,
}
}
fn build(self) -> ZResult<TypedPublisher<'b, T>> {
assert!(
!T::SCHEMA_NAME.trim().is_empty(),
"TypedSchema::SCHEMA_NAME must not be empty or whitespace-only"
);
let inner = self.inner.wait()?;
Ok(TypedPublisher {
inner,
schema_version: self.schema_version,
publish_policy: self.publish_policy,
_phantom: PhantomData,
})
}
}
impl<'b, T: Serialize + TypedSchema> IntoFuture for TypedPublisherBuilder<'_, 'b, T> {
type Output = ZResult<TypedPublisher<'b, T>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.build())
}
}
pub struct TypedSubscriberBuilder<
'a,
'b,
T: DeserializeOwned + TypedSchema,
Handler = DefaultHandler,
> {
inner: SubscriberBuilder<'a, 'b, Handler>,
schema_version: Option<u32>,
_phantom: PhantomData<T>,
}
impl<'a, 'b, T: DeserializeOwned + TypedSchema, Handler>
TypedSubscriberBuilder<'a, 'b, T, Handler>
{
pub fn schema_version(mut self, version: u32) -> Self {
self.schema_version = Some(version);
self
}
pub fn allowed_origin(self, origin: Locality) -> Self {
Self {
inner: self.inner.allowed_origin(origin),
schema_version: self.schema_version,
_phantom: self._phantom,
}
}
}
impl<'a, 'b, T: DeserializeOwned + TypedSchema> TypedSubscriberBuilder<'a, 'b, T, DefaultHandler> {
pub fn with<NewHandler>(
self,
handler: NewHandler,
) -> TypedSubscriberBuilder<'a, 'b, T, NewHandler>
where
NewHandler: zenoh::handlers::IntoHandler<Sample>,
{
TypedSubscriberBuilder {
inner: self.inner.with(handler),
schema_version: self.schema_version,
_phantom: PhantomData,
}
}
pub fn callback<F>(self, callback: F) -> TypedSubscriberBuilder<'a, 'b, T, Callback<Sample>>
where
F: Fn(Sample) + Send + Sync + 'static,
{
TypedSubscriberBuilder {
inner: self.inner.callback(callback),
schema_version: self.schema_version,
_phantom: PhantomData,
}
}
pub fn callback_mut<F>(self, callback: F) -> TypedSubscriberBuilder<'a, 'b, T, Callback<Sample>>
where
F: FnMut(Sample) + Send + Sync + 'static,
{
TypedSubscriberBuilder {
inner: self.inner.callback_mut(callback),
schema_version: self.schema_version,
_phantom: PhantomData,
}
}
}
impl<T: DeserializeOwned + TypedSchema, Handler> TypedSubscriberBuilder<'_, '_, T, Handler>
where
Handler: zenoh::handlers::IntoHandler<Sample> + Send,
Handler::Handler: Send,
{
fn build(self) -> ZResult<TypedSubscriber<T, Handler::Handler>> {
assert!(
!T::SCHEMA_NAME.trim().is_empty(),
"TypedSchema::SCHEMA_NAME must not be empty or whitespace-only"
);
let inner = self.inner.wait()?;
Ok(TypedSubscriber {
inner,
schema_version: self.schema_version,
_phantom: PhantomData,
})
}
}
impl<T: DeserializeOwned + TypedSchema, Handler> IntoFuture
for TypedSubscriberBuilder<'_, '_, T, Handler>
where
Handler: zenoh::handlers::IntoHandler<Sample> + Send,
Handler::Handler: Send,
{
type Output = ZResult<TypedSubscriber<T, Handler::Handler>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.build())
}
}
pub struct TypedQuery<Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema> {
inner: Query,
_phantom: PhantomData<(Req, Resp)>,
}
impl<Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema> TypedQuery<Req, Resp> {
pub fn request(&self) -> Result<Req, TypedPayloadError> {
let payload = self
.inner
.payload()
.ok_or(TypedPayloadError::MissingPayload)?;
check_encoding(self.inner.encoding(), Req::SCHEMA_NAME)?;
check_schema_version(
self.inner.attachment(),
Some(Req::SCHEMA_VERSION),
Req::SCHEMA_NAME,
)?;
deserialize_payload::<Req>(payload).map_err(TypedPayloadError::from)
}
pub async fn reply(&self, resp: &Resp) -> ZResult<()> {
let zbytes = serialize_payload(resp)?;
self.inner
.reply(self.inner.key_expr(), zbytes)
.encoding(typed_encoding(Resp::SCHEMA_NAME))
.attachment(encode_schema_version(Resp::SCHEMA_VERSION))
.await
}
pub async fn reply_err<IntoZBytes: Into<ZBytes>>(&self, payload: IntoZBytes) -> ZResult<()> {
self.inner.reply_err(payload).await
}
pub fn query(&self) -> &Query {
&self.inner
}
}
pub struct TypedQueryable<
Req: DeserializeOwned + TypedSchema,
Resp: Serialize + TypedSchema,
Handler = FifoChannelHandler<Query>,
> {
inner: Queryable<Handler>,
_phantom: PhantomData<(Req, Resp)>,
}
impl<Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema>
TypedQueryable<Req, Resp, FifoChannelHandler<Query>>
{
pub async fn recv_async(&self) -> ZResult<TypedQuery<Req, Resp>> {
let query = self.inner.recv_async().await?;
Ok(TypedQuery {
inner: query,
_phantom: PhantomData,
})
}
pub fn recv(&self) -> ZResult<TypedQuery<Req, Resp>> {
let query = self.inner.recv()?;
Ok(TypedQuery {
inner: query,
_phantom: PhantomData,
})
}
}
pub struct TypedQueryableBuilder<
'a,
'b,
Req: DeserializeOwned + TypedSchema,
Resp: Serialize + TypedSchema,
> {
inner: QueryableBuilder<'a, 'b, DefaultHandler>,
_phantom: PhantomData<(Req, Resp)>,
}
impl<'a, 'b, Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema>
TypedQueryableBuilder<'a, 'b, Req, Resp>
{
pub fn with<Handler>(
self,
handler: Handler,
) -> TypedQueryableWithHandlerBuilder<'a, 'b, Req, Resp, Handler>
where
Handler: zenoh::handlers::IntoHandler<Query>,
{
TypedQueryableWithHandlerBuilder {
inner: self.inner.with(handler),
_phantom: PhantomData,
}
}
pub fn callback<F>(
self,
callback: F,
) -> TypedQueryableWithHandlerBuilder<'a, 'b, Req, Resp, Callback<Query>>
where
F: Fn(Query) + Send + Sync + 'static,
{
TypedQueryableWithHandlerBuilder {
inner: self.inner.callback(callback),
_phantom: PhantomData,
}
}
pub fn callback_mut<F>(
self,
callback: F,
) -> TypedQueryableWithHandlerBuilder<'a, 'b, Req, Resp, Callback<Query>>
where
F: FnMut(Query) + Send + Sync + 'static,
{
TypedQueryableWithHandlerBuilder {
inner: self.inner.callback_mut(callback),
_phantom: PhantomData,
}
}
pub fn complete(self, complete: bool) -> Self {
Self {
inner: self.inner.complete(complete),
_phantom: self._phantom,
}
}
pub fn allowed_origin(self, origin: Locality) -> Self {
Self {
inner: self.inner.allowed_origin(origin),
_phantom: self._phantom,
}
}
fn build(self) -> ZResult<TypedQueryable<Req, Resp>> {
let inner = self.inner.wait()?;
Ok(TypedQueryable {
inner,
_phantom: PhantomData,
})
}
}
impl<Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema> IntoFuture
for TypedQueryableBuilder<'_, '_, Req, Resp>
{
type Output = ZResult<TypedQueryable<Req, Resp>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.build())
}
}
pub struct TypedQueryableWithHandlerBuilder<
'a,
'b,
Req: DeserializeOwned + TypedSchema,
Resp: Serialize + TypedSchema,
Handler,
> {
inner: QueryableBuilder<'a, 'b, Handler>,
_phantom: PhantomData<(Req, Resp)>,
}
impl<'a, 'b, Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema, Handler>
TypedQueryableWithHandlerBuilder<'a, 'b, Req, Resp, Handler>
{
pub fn complete(self, complete: bool) -> Self {
Self {
inner: self.inner.complete(complete),
_phantom: self._phantom,
}
}
pub fn allowed_origin(self, origin: Locality) -> Self {
Self {
inner: self.inner.allowed_origin(origin),
_phantom: self._phantom,
}
}
}
impl<Req: DeserializeOwned + TypedSchema, Resp: Serialize + TypedSchema, Handler> IntoFuture
for TypedQueryableWithHandlerBuilder<'_, '_, Req, Resp, Handler>
where
Handler: zenoh::handlers::IntoHandler<Query> + Send,
Handler::Handler: Send,
{
type Output = ZResult<TypedQueryable<Req, Resp, Handler::Handler>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.inner.wait().map(|inner| TypedQueryable {
inner,
_phantom: PhantomData,
}))
}
}
pub struct TypedGetFuture<Resp: DeserializeOwned + TypedSchema> {
result: ZResult<Vec<Result<Resp, TypedGetError>>>,
}
impl<Resp: DeserializeOwned + TypedSchema> std::future::IntoFuture for TypedGetFuture<Resp> {
type Output = ZResult<Vec<Result<Resp, TypedGetError>>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.result)
}
}
pub trait TypedSessionExt {
fn declare_typed_publisher<'b, T: Serialize + TypedSchema, TryIntoKeyExpr>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedPublisherBuilder<'_, 'b, T>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>;
fn declare_typed_subscriber<'b, T: DeserializeOwned + TypedSchema, TryIntoKeyExpr>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedSubscriberBuilder<'_, 'b, T>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>;
fn declare_typed_queryable<
'b,
Req: DeserializeOwned + TypedSchema,
Resp: Serialize + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedQueryableBuilder<'_, 'b, Req, Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>;
fn typed_get_builder<
'b,
Req: Serialize + TypedSchema,
Resp: DeserializeOwned + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
request: &Req,
) -> TypedGetBuilder<'_, 'b, Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>;
fn typed_get<
'b,
Req: Serialize + TypedSchema,
Resp: DeserializeOwned + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
request: &Req,
) -> TypedGetFuture<Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>;
}
pub struct TypedGetBuilder<'a, 'b, Resp: DeserializeOwned + TypedSchema> {
session: &'a Session,
key_expr: ZResult<KeyExpr<'b>>,
payload: ZResult<ZBytes>,
encoding: Encoding,
schema_version: u32,
target: QueryTarget,
consolidation: QueryConsolidation,
destination: Locality,
timeout: Option<Duration>,
accept_replies: ReplyKeyExpr,
_phantom: PhantomData<Resp>,
}
impl<'a, 'b, Resp: DeserializeOwned + TypedSchema> TypedGetBuilder<'a, 'b, Resp> {
pub fn target(self, target: QueryTarget) -> Self {
Self { target, ..self }
}
pub fn consolidation<QC: Into<QueryConsolidation>>(self, consolidation: QC) -> Self {
Self {
consolidation: consolidation.into(),
..self
}
}
pub fn allowed_destination(self, destination: Locality) -> Self {
Self {
destination,
..self
}
}
pub fn timeout(self, timeout: Duration) -> Self {
Self {
timeout: Some(timeout),
..self
}
}
pub fn accept_replies(self, accept: ReplyKeyExpr) -> Self {
Self {
accept_replies: accept,
..self
}
}
}
impl<Resp: DeserializeOwned + TypedSchema> IntoFuture for TypedGetBuilder<'_, '_, Resp> {
type Output = ZResult<Vec<Result<Resp, TypedGetError>>>;
type IntoFuture = Ready<Self::Output>;
fn into_future(self) -> Self::IntoFuture {
let result = (|| -> ZResult<Vec<Result<Resp, TypedGetError>>> {
let key_expr = self.key_expr?;
let payload = self.payload?;
let receiver = self
.session
.get(key_expr)
.payload(payload)
.encoding(self.encoding)
.attachment(encode_schema_version(self.schema_version))
.target(self.target)
.consolidation(self.consolidation)
.allowed_destination(self.destination)
.accept_replies(self.accept_replies);
let receiver = if let Some(timeout) = self.timeout {
receiver.timeout(timeout).wait()?
} else {
receiver.wait()?
};
let mut replies = Vec::new();
while let Ok(reply) = receiver.recv() {
match reply.into_result() {
Ok(sample) => {
let result = (|| -> Result<Resp, TypedPayloadError> {
check_encoding(Some(sample.encoding()), Resp::SCHEMA_NAME)?;
check_schema_version(
sample.attachment(),
Some(Resp::SCHEMA_VERSION),
Resp::SCHEMA_NAME,
)?;
deserialize_payload::<Resp>(sample.payload())
.map_err(TypedPayloadError::from)
})();
replies.push(result.map_err(TypedGetError::from));
}
Err(reply_err) => {
replies.push(Err(TypedGetError::ReplyError(reply_err.payload().clone())));
}
}
}
Ok(replies)
})();
std::future::ready(result)
}
}
impl TypedSessionExt for Session {
fn declare_typed_publisher<'b, T: Serialize + TypedSchema, TryIntoKeyExpr>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedPublisherBuilder<'_, 'b, T>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>,
{
TypedPublisherBuilder {
inner: self
.declare_publisher(key_expr)
.encoding(typed_encoding(T::SCHEMA_NAME)),
schema_version: None,
publish_policy: PublishPolicy::Lazy,
_phantom: PhantomData,
}
}
fn declare_typed_subscriber<'b, T: DeserializeOwned + TypedSchema, TryIntoKeyExpr>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedSubscriberBuilder<'_, 'b, T>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>,
{
TypedSubscriberBuilder {
inner: self.declare_subscriber(key_expr),
schema_version: None,
_phantom: PhantomData,
}
}
fn declare_typed_queryable<
'b,
Req: DeserializeOwned + TypedSchema,
Resp: Serialize + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
) -> TypedQueryableBuilder<'_, 'b, Req, Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>,
{
TypedQueryableBuilder {
inner: self.declare_queryable(key_expr),
_phantom: PhantomData,
}
}
fn typed_get_builder<
'b,
Req: Serialize + TypedSchema,
Resp: DeserializeOwned + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
request: &Req,
) -> TypedGetBuilder<'_, 'b, Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>,
{
TypedGetBuilder {
session: self,
key_expr: key_expr.try_into().map_err(Into::into),
payload: serialize_payload(request),
encoding: typed_encoding(Req::SCHEMA_NAME),
schema_version: Req::SCHEMA_VERSION,
target: QueryTarget::DEFAULT,
consolidation: QueryConsolidation::DEFAULT,
destination: Locality::default(),
timeout: None,
accept_replies: ReplyKeyExpr::MatchingQuery,
_phantom: PhantomData,
}
}
fn typed_get<
'b,
Req: Serialize + TypedSchema,
Resp: DeserializeOwned + TypedSchema,
TryIntoKeyExpr,
>(
&self,
key_expr: TryIntoKeyExpr,
request: &Req,
) -> TypedGetFuture<Resp>
where
TryIntoKeyExpr: TryInto<KeyExpr<'b>>,
<TryIntoKeyExpr as TryInto<KeyExpr<'b>>>::Error: Into<Error>,
{
let result = (|| -> ZResult<Vec<Result<Resp, TypedGetError>>> {
let builder = self.typed_get_builder::<Req, Resp, TryIntoKeyExpr>(key_expr, request);
let key_expr = builder.key_expr?;
let payload = builder.payload?;
let receiver = builder
.session
.get(key_expr)
.payload(payload)
.encoding(builder.encoding)
.attachment(encode_schema_version(builder.schema_version))
.target(builder.target)
.consolidation(builder.consolidation)
.allowed_destination(builder.destination)
.accept_replies(builder.accept_replies);
let receiver = if let Some(timeout) = builder.timeout {
receiver.timeout(timeout).wait()?
} else {
receiver.wait()?
};
let mut replies = Vec::new();
while let Ok(reply) = receiver.recv() {
match reply.into_result() {
Ok(sample) => {
let result = (|| -> Result<Resp, TypedPayloadError> {
check_encoding(Some(sample.encoding()), Resp::SCHEMA_NAME)?;
check_schema_version(
sample.attachment(),
Some(Resp::SCHEMA_VERSION),
Resp::SCHEMA_NAME,
)?;
deserialize_payload::<Resp>(sample.payload())
.map_err(TypedPayloadError::from)
})();
replies.push(result.map_err(TypedGetError::from));
}
Err(reply_err) => {
replies.push(Err(TypedGetError::ReplyError(reply_err.payload().clone())));
}
}
}
Ok(replies)
})();
TypedGetFuture { result }
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use serial_test::serial;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use zenoh::key_expr::KeyExpr;
use zenoh::sample::SampleBuilder;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct TestPayload {
value: u32,
}
impl TypedSchema for TestPayload {
const SCHEMA_NAME: &'static str = "typed-publisher-test-payload";
const SCHEMA_VERSION: u32 = 1;
}
struct PanicPayload;
impl Serialize for PanicPayload {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
panic!("payload should not be serialized")
}
}
impl TypedSchema for PanicPayload {
const SCHEMA_NAME: &'static str = "typed-publisher-panic-payload";
const SCHEMA_VERSION: u32 = 1;
}
fn unique_topic(suffix: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("phoxal/tests/{suffix}/{nanos}")
}
fn local_test_config() -> zenoh::Config {
let mut config = zenoh::Config::default();
config.insert_json5("listen/endpoints", "[]").unwrap();
config
.insert_json5("scouting/multicast/enabled", "false")
.unwrap();
config
}
async fn open_session() -> zenoh::Session {
zenoh::open(local_test_config())
.await
.expect("test zenoh session should open")
}
fn make_sample_with_encoding(encoding: &str) -> Sample {
let payload = serialize_payload(&42u32).unwrap();
let key: KeyExpr<'static> = KeyExpr::try_from("test/key").unwrap();
SampleBuilder::put(key, payload)
.encoding(Encoding::from(encoding))
.into()
}
fn make_sample_default_encoding() -> Sample {
let payload = serialize_payload(&42u32).unwrap();
let key: KeyExpr<'static> = KeyExpr::try_from("test/key").unwrap();
SampleBuilder::put(key, payload).into()
}
#[test]
fn check_encoding_accepts_correct_typed_encoding() {
let sample = make_sample_with_encoding("zenoh-ext/typed:test-payload");
assert!(check_encoding(Some(sample.encoding()), "test-payload").is_ok());
}
#[test]
fn check_encoding_rejects_wrong_typed_encoding() {
let sample = make_sample_with_encoding("zenoh-ext/typed:other-type");
let result = check_encoding(Some(sample.encoding()), "test-payload");
assert!(result.is_err());
match result.unwrap_err() {
TypedPayloadError::EncodingMismatch { expected, received } => {
assert!(
expected.contains("test-payload"),
"expected should contain schema name, got: {expected}"
);
assert!(
received.contains("other-type"),
"received should contain actual schema name, got: {received}"
);
}
other => panic!("expected EncodingMismatch, got: {other:?}"),
}
}
#[test]
fn check_encoding_rejects_untyped_encoding() {
let sample = make_sample_with_encoding("application/json");
let result = check_encoding(Some(sample.encoding()), "test-payload");
assert!(result.is_err());
match result.unwrap_err() {
TypedPayloadError::EncodingMismatch { expected, received } => {
assert!(
expected.contains("test-payload"),
"expected should reference schema name, got: {expected}"
);
assert!(
received.contains("application/json"),
"received should show actual encoding, got: {received}"
);
}
other => panic!("expected EncodingMismatch, got: {other:?}"),
}
}
#[test]
fn check_encoding_rejects_default_encoding() {
let sample = make_sample_default_encoding();
let result = check_encoding(Some(sample.encoding()), "test-payload");
assert!(
result.is_err(),
"default encoding should not pass typed encoding check"
);
assert!(
matches!(
result.unwrap_err(),
TypedPayloadError::EncodingMismatch { .. }
),
"should be EncodingMismatch variant"
);
}
#[test]
fn encoding_mismatch_display_distinguishes_from_deserialization_error() {
let encoding_err = TypedPayloadError::EncodingMismatch {
expected: "zenoh-ext/typed:foo".to_string(),
received: "application/json".to_string(),
};
let deser_err = TypedPayloadError::DeserializationFailed(
deserialize_payload::<u32>(&ZBytes::from(Vec::<u8>::new())).unwrap_err(),
);
let encoding_msg = encoding_err.to_string();
let deser_msg = deser_err.to_string();
assert!(
encoding_msg.contains("encoding mismatch"),
"encoding error should say 'encoding mismatch', got: {encoding_msg}"
);
assert!(
deser_msg.contains("deserialization"),
"deser error should mention 'deserialization', got: {deser_msg}"
);
assert_ne!(
encoding_msg, deser_msg,
"error messages should be distinguishable"
);
}
#[test]
fn strict_schema_version_requires_attachment() {
let sample = make_sample_with_encoding("zenoh-ext/typed:test-payload");
let result = check_schema_version(sample.attachment(), Some(1), "test-payload");
assert!(matches!(
result,
Err(TypedPayloadError::MissingSchemaVersion { expected: 1, .. })
));
}
#[serial]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn typed_publishers_default_to_lazy_policy() {
let session = open_session().await;
let topic = unique_topic("lazy-default");
let publisher = session
.declare_typed_publisher::<TestPayload, _>(topic)
.await
.expect("publisher should build");
assert_eq!(publisher.publish_policy(), PublishPolicy::Lazy);
drop(publisher);
session.close().await.expect("session should close");
}
#[serial]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn lazy_publisher_skips_serialization_when_unmatched() {
let session = open_session().await;
let topic = unique_topic("lazy-unmatched");
let publisher = session
.declare_typed_publisher::<PanicPayload, _>(topic)
.await
.expect("publisher should build");
publisher
.put(&PanicPayload)
.await
.expect("unmatched lazy publisher should short-circuit");
drop(publisher);
session.close().await.expect("session should close");
}
#[serial]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn eager_publisher_reports_publishable_without_subscribers() {
let session = open_session().await;
let topic = unique_topic("eager-unmatched");
let publisher = session
.declare_typed_publisher::<TestPayload, _>(topic)
.eager()
.await
.expect("publisher should build");
assert_eq!(publisher.publish_policy(), PublishPolicy::Eager);
assert!(
publisher
.should_publish()
.await
.expect("matching status should resolve")
);
drop(publisher);
session.close().await.expect("session should close");
}
#[serial]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn lazy_publisher_publishes_once_subscriber_matches() {
let session = open_session().await;
let topic = unique_topic("lazy-matched");
let publisher = session
.declare_typed_publisher::<TestPayload, _>(&topic)
.await
.expect("publisher should build");
let subscriber = session
.declare_typed_subscriber::<TestPayload, _>(&topic)
.await
.expect("subscriber should build");
tokio::time::timeout(Duration::from_secs(5), async {
loop {
if publisher
.has_matching_subscribers()
.await
.expect("matching status should resolve")
{
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.expect("publisher should observe subscriber match");
publisher
.put(&TestPayload { value: 7 })
.await
.expect("matched lazy publisher should publish");
let received = tokio::time::timeout(Duration::from_secs(5), subscriber.recv_async())
.await
.expect("subscriber should receive payload")
.expect("subscriber channel should stay open")
.expect("payload should decode");
assert_eq!(received, TestPayload { value: 7 });
drop(subscriber);
drop(publisher);
session.close().await.expect("session should close");
}
}