phoxal_bus/topic.rs
1//! Typed topics - the api-local builder output (D61), side-branded for L1 (plan #00).
2//!
3//! A [`Topic`] is a version-qualified topic key plus a phantom [`TopicKind`]
4//! that ties the key to its body type(s) **and to the side** the holder may
5//! take. The api tree's `topic` builders return these; the `SetupContext` handle
6//! builders consume them. The wire body never appears in the key, but the
7//! version does - the key is `v0.1/drive/state`, not `drive/state` (D62/D1):
8//! folding the version into the key is what makes different versioned names
9//! physically distinct Zenoh keys.
10//!
11//! # Side branding (L1)
12//!
13//! The kind marker is the compile-time gate that makes taking the **wrong side**
14//! of a topic a type error. The four markers split each wire shape by side:
15//!
16//! - [`Publish<B>`] - the participant *publishes* `B` (a client sending a command,
17//! or an owner publishing its state).
18//! - [`Subscribe<B>`] - the participant *subscribes/observes* `B` (a client
19//! observing state, or an owner reading its command input).
20//! - [`AskQuery<Req, Resp>`] - the **client** side of a query: it *calls* the owner.
21//! - [`ServeQuery<Req, Resp>`] - the **owner** side of a query: it *serves* requests.
22//!
23//! The brand is a COMPILE-TIME marker only: the underlying key and the actual
24//! `Publisher`/`Subscriber`/`Latest`/`Querier`/server ops are unchanged. The api
25//! tree emits the builder tree twice - a public *client* builder and an
26//! explicit *owner* builder over identical keys - so the side a participant gets
27//! is decided by which builder it calls, and a wrong side fails to compile in the
28//! `SetupContext` handle builder that consumes the `Topic`.
29
30use std::borrow::Cow;
31use std::marker::PhantomData;
32
33/// A pub/sub topic the participant **publishes** `B` on (client command, or owner
34/// state). The publish side of the former side-agnostic `PubSub<B>`.
35pub struct Publish<B>(PhantomData<fn() -> B>);
36
37/// A pub/sub topic the participant **subscribes/observes** `B` on (client
38/// observing state, or owner reading its command input). The subscribe side of
39/// the former side-agnostic `PubSub<B>`.
40pub struct Subscribe<B>(PhantomData<fn() -> B>);
41
42/// The **client** side of a query topic carrying request `Req`/response `Resp`:
43/// the holder *calls* the owner. The caller side of the former side-agnostic
44/// `Query<Req, Resp>`.
45pub struct AskQuery<Req, Resp>(PhantomData<fn() -> (Req, Resp)>);
46
47/// The **owner** side of a query topic carrying request `Req`/response `Resp`:
48/// the holder *serves* requests. The server side of the former side-agnostic
49/// `Query<Req, Resp>`.
50pub struct ServeQuery<Req, Resp>(PhantomData<fn() -> (Req, Resp)>);
51
52mod sealed {
53 pub trait Sealed {}
54}
55
56/// Marker for the kind (wire shape + side) of a [`Topic`]. Sealed.
57pub trait TopicKind: sealed::Sealed {}
58
59impl<B> sealed::Sealed for Publish<B> {}
60impl<B> TopicKind for Publish<B> {}
61impl<B> sealed::Sealed for Subscribe<B> {}
62impl<B> TopicKind for Subscribe<B> {}
63impl<Req, Resp> sealed::Sealed for AskQuery<Req, Resp> {}
64impl<Req, Resp> TopicKind for AskQuery<Req, Resp> {}
65impl<Req, Resp> sealed::Sealed for ServeQuery<Req, Resp> {}
66impl<Req, Resp> TopicKind for ServeQuery<Req, Resp> {}
67
68/// A typed topic: a version-qualified key bound to its body type(s) via `Kind`.
69pub struct Topic<Kind> {
70 key: Cow<'static, str>,
71 _kind: PhantomData<Kind>,
72}
73
74impl<Kind> Topic<Kind> {
75 /// Construct a topic from a static key.
76 ///
77 /// `#[doc(hidden)] pub` raw constructor - an unsupported escape hatch, NOT
78 /// part of the authored surface. It is `pub` only because it must be callable
79 /// across the `phoxal-api` / `phoxal-bus` crate split: the `phoxal_api_tree!`
80 /// macro (invoked in the `phoxal-api` crate, where the versioned APIs live)
81 /// calls it over each contract's canonical key, and a `pub(crate)` constructor
82 /// cannot cross that boundary. Author correctness does not come from hiding
83 /// this: it comes from the typed handles and the api-tree builders
84 /// (`api::topic::client()....()`), which keep the typed `Kind` and the bus key in
85 /// lockstep (D61/D62). The owner-side builder (`api::topic::owner()`) makes the intended side
86 /// explicit. This `#[doc(hidden)]` raw constructor remains an escape hatch: it
87 /// is generic over `Kind`, so hand-written code can forge either branded topic.
88 /// That is inherent to the macro/crate split because generated builder code in a
89 /// downstream crate needs a `pub` constructor.
90 #[doc(hidden)]
91 pub fn new_static(key: &'static str) -> Self {
92 Topic {
93 key: Cow::Borrowed(key),
94 _kind: PhantomData,
95 }
96 }
97
98 /// Construct a topic from an owned (dynamically built) key.
99 ///
100 /// `#[doc(hidden)] pub` raw constructor, the owned-key counterpart of
101 /// [`new_static`](Self::new_static): an unsupported escape hatch that is `pub`
102 /// only to cross the `phoxal-api` / `phoxal-bus` crate split. The generated api
103 /// builder calls it for nodes with dynamic segments, filling the carried
104 /// variables into the canonical key. Not part of the authored surface;
105 /// correctness for authors comes from the typed handles + api-tree builders.
106 /// The owner-side builder is [`api::topic::owner()`](https://docs.rs/phoxal-api),
107 /// while this constructor remains an explicit raw escape hatch like
108 /// [`new_static`](Self::new_static).
109 #[doc(hidden)]
110 pub fn new_owned(key: String) -> Self {
111 Topic {
112 key: Cow::Owned(key),
113 _kind: PhantomData,
114 }
115 }
116
117 /// The version-qualified topic key (e.g. `v0.1/drive/state`).
118 pub fn key(&self) -> &str {
119 &self.key
120 }
121
122 /// The key reusable as the publish key. Wildcard topics (`*`) are
123 /// subscribe-only and rejected here before transport.
124 pub fn publish_key(&self) -> Result<&str, WildcardPublish> {
125 if self.key.split('/').any(|seg| seg == "*" || seg == "**") {
126 Err(WildcardPublish {
127 key: self.key.to_string(),
128 })
129 } else {
130 Ok(&self.key)
131 }
132 }
133}
134
135impl<Kind> Clone for Topic<Kind> {
136 fn clone(&self) -> Self {
137 Topic {
138 key: self.key.clone(),
139 _kind: PhantomData,
140 }
141 }
142}
143
144/// Attempted to publish on a wildcard (subscribe-only) topic.
145#[derive(Debug, thiserror::Error)]
146#[error("cannot publish on wildcard topic '{key}' (wildcards are subscribe-only)")]
147pub struct WildcardPublish {
148 /// The offending key.
149 pub key: String,
150}