use std::marker::PhantomData;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct KeySegment(String);
impl KeySegment {
pub fn new(value: impl Into<String>) -> Result<Self, KeySegmentError> {
let value = value.into();
if value.is_empty()
|| value.contains('/')
|| value.contains('*')
|| value.chars().any(|character| character.is_control())
|| zenoh::key_expr::OwnedKeyExpr::new(value.clone()).is_err()
{
return Err(KeySegmentError(value));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for KeySegment {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl TryFrom<String> for KeySegment {
type Error = KeySegmentError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for KeySegment {
type Error = KeySegmentError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error(
"topic key segment must be non-empty, concrete, and contain no '/', '*', or control characters; got {0:?}"
)]
pub struct KeySegmentError(String);
pub struct Publish<E>(PhantomData<fn() -> E>);
pub struct Subscribe<E>(PhantomData<fn() -> E>);
pub struct AskQuery<E>(PhantomData<fn() -> E>);
pub struct ServeQuery<E>(PhantomData<fn() -> E>);
mod sealed {
pub trait Sealed {}
}
pub trait TopicKind: sealed::Sealed {}
impl<E> sealed::Sealed for Publish<E> {}
impl<E> TopicKind for Publish<E> {}
impl<E> sealed::Sealed for Subscribe<E> {}
impl<E> TopicKind for Subscribe<E> {}
impl<E> sealed::Sealed for AskQuery<E> {}
impl<E> TopicKind for AskQuery<E> {}
impl<E> sealed::Sealed for ServeQuery<E> {}
impl<E> TopicKind for ServeQuery<E> {}
pub struct Topic<Kind> {
key: String,
_kind: PhantomData<Kind>,
}
impl<Kind> Topic<Kind> {
pub(crate) fn new(key: String) -> Self {
Topic {
key,
_kind: PhantomData,
}
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
pub fn publish_key(&self) -> Result<&str, WildcardPublish> {
if self.key.split('/').any(|seg| seg == "*" || seg == "**") {
Err(WildcardPublish {
key: self.key.clone(),
})
} else {
Ok(&self.key)
}
}
}
impl<Kind> Clone for Topic<Kind> {
fn clone(&self) -> Self {
Topic {
key: self.key.clone(),
_kind: PhantomData,
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("cannot publish on wildcard topic '{key}' (wildcards are subscribe-only)")]
pub struct WildcardPublish {
pub key: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_segments_reject_non_concrete_values() {
for invalid in ["", "a/b", "*", "**", "a\n"] {
assert!(
KeySegment::new(invalid).is_err(),
"{invalid:?} must not cross a dynamic builder boundary"
);
}
let segment = KeySegment::new("front_left").expect("concrete segment");
assert_eq!(segment.as_str(), "front_left");
assert_eq!(segment.to_string(), "front_left");
}
#[test]
fn a_concrete_key_is_publishable_and_a_wildcard_one_is_not() {
let concrete = Topic::<Publish<()>>::new("robot/drive/state".to_owned());
assert_eq!(
concrete
.publish_key()
.expect("a concrete key is publishable"),
"robot/drive/state"
);
for wildcard in ["robot/component/*/state", "robot/component/**"] {
let topic = Topic::<Subscribe<()>>::new(wildcard.to_owned());
let rejected = topic
.publish_key()
.expect_err("a wildcard topic is subscribe-only");
assert_eq!(rejected.key, wildcard);
}
}
}