Skip to main content

phoxal_bus/
contract.rs

1//! The contract primitive traits: the contract-family marker and endpoint
2//! descriptors.
3//!
4//! These are the two traits the bus client is generic over - the ABI floor
5//! every contract family uses: ordinary Rust payloads plus one generated
6//! descriptor per endpoint. This crate owns the shared ABI traits; `phoxal-api`
7//! owns every concrete declaration.
8
9/// Marker trait identifying one generated contract tree.
10///
11/// Implemented only by the zero-variant `enum Api {}` that `phoxal_api_tree!`
12/// generates inside each family module. The [`ID`] is a semantic contract
13/// namespace - `"robot"`, `"runtime"`, `"supervisor"` - and is both the tree's
14/// identity and the leading segment of every key it declares. It is carried in
15/// bus metadata as informational provenance, never in the wire body.
16///
17/// A family names meaning, not a revision. Compatibility between two
18/// participants is the framework train version they were built from, compared
19/// for exact equality, so no key or descriptor carries a per-API version.
20///
21/// The marker keeps one family's bodies from standing in for another's at
22/// compile time. `ParticipantSpec::ContractApi` pins a participant to exactly
23/// one of them.
24///
25/// [`ID`]: ApiFamily::ID
26pub trait ApiFamily: 'static {
27    /// The family's wire identifier, such as `"robot"`.
28    const ID: &'static str;
29}
30
31/// A plain serde payload carried by one bus endpoint.
32///
33/// Payloads deliberately contain no transport identity or delivery policy.
34/// Those facts belong to an [`EndpointDescriptor`], which is the type used by
35/// typed topics and handles.  The blanket implementation keeps ordinary
36/// structs and enums frictionless: an author only derives serde for a payload
37/// and never has to repeat a topic, role, or queue policy on the payload type.
38pub trait Payload: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static {}
39
40impl<T> Payload for T where T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static
41{}
42
43/// The minimal transport semantic family a contract requires.
44///
45/// Temporal stamping is intentionally separate: a `sample` may carry a device
46/// capture window while a `state` is stamped by the runner's logical step.
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub enum DeliveryFamily {
49    /// Retain the newest observable snapshot.
50    State,
51    /// Preserve bounded ordered observations with explicit loss evidence.
52    Sample,
53    /// Retain only the newest actionable intent.
54    Setpoint,
55    /// Preserve ordered chunks and surface saturation/gaps.
56    Stream,
57    /// Bounded immediate lookup/admission.
58    Query,
59}
60
61/// The fixed semantic kind of an endpoint.
62///
63/// This is endpoint metadata, not payload metadata.  The five pub/sub kinds
64/// intentionally have no user-selectable queue policy: their bus behavior is
65/// fixed by the kind.  `Query` remains the bounded request/reply path rather
66/// than an outbound scheduler lane.
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68pub enum EndpointKind {
69    /// A current state snapshot, stamped at a logical step.
70    State,
71    /// A captured observation, ordered with explicit bounded loss evidence.
72    Sample,
73    /// A state-temporal event, ordered and gap-observable.
74    Event,
75    /// An ordered stream chunk with refusal-preserving admission.
76    Stream,
77    /// A newest-actionable intent, coalesced before transport.
78    Setpoint,
79    /// A bounded request/reply endpoint.
80    Query,
81}
82
83impl EndpointKind {
84    /// The transport family fixed by this endpoint kind.
85    pub const fn delivery_family(self) -> DeliveryFamily {
86        match self {
87            Self::State => DeliveryFamily::State,
88            Self::Sample => DeliveryFamily::Sample,
89            Self::Event | Self::Stream => DeliveryFamily::Stream,
90            Self::Setpoint => DeliveryFamily::Setpoint,
91            Self::Query => DeliveryFamily::Query,
92        }
93    }
94}
95
96/// Endpoint-owned identity and semantic descriptor.
97///
98/// `Payload` is the only wire body.  `TOPIC`, `KIND`, and the contract identity
99/// are owned by this separate descriptor type, so reusing one payload in two
100/// endpoints cannot silently reuse the first endpoint's transport behavior.
101/// The API tree generator emits one descriptor per endpoint and implements the
102/// semantic marker appropriate to [`EndpointDescriptor::KIND`].
103pub trait EndpointDescriptor: 'static {
104    /// The contract family this endpoint belongs to.
105    type Api: ApiFamily;
106    /// The plain serde payload carried by this endpoint.
107    type Payload: Payload;
108    /// Family-qualified endpoint identity.
109    const NAME: &'static str;
110    /// Endpoint tree identity, such as `"robot"` or `"supervisor"`.
111    const FAMILY: &'static str;
112    /// Stable endpoint path within its tree.
113    const CONTRACT: &'static str;
114    /// Family-rooted concrete wire key template.
115    const TOPIC: &'static str;
116    /// Fixed semantic endpoint kind.
117    const KIND: EndpointKind;
118}
119
120/// Descriptor for a typed request/reply endpoint.
121///
122/// A semantic endpoint owns both payload paths while remaining one zero-sized
123/// type in the generated tree.
124pub trait QueryEndpointDescriptor: EndpointDescriptor {
125    /// Request payload decoded from the query.
126    type Request: Payload;
127    /// Response payload encoded in the reply.
128    type Response: Payload;
129}
130
131/// Short name for an endpoint descriptor used by typed bus APIs.
132pub trait Endpoint: EndpointDescriptor {}
133
134impl<T: EndpointDescriptor> Endpoint for T {}
135
136/// Marker for an endpoint whose temporal meaning is current state.
137///
138/// Generated by `phoxal_api_tree!` for every ordinary `State<T>` endpoint. Deliberately
139/// NOT implemented for the runtime-owned world-clock hand - see
140/// [`WorldClockContract`] for why that exclusion is the enforcement mechanism,
141/// not an oversight.
142pub trait StateContract: EndpointDescriptor {}
143
144/// Marker for the framework's single runtime-owned world-clock hand.
145///
146/// Deliberately a SIBLING of [`StateContract`], not a subtrait of it: if the
147/// world clock also implemented `StateContract`, it would still satisfy the
148/// ordinary, unrestricted `state_publisher` builder every participant has,
149/// which would make "only a simulator can mint world steps" an unenforced
150/// convention rather than a compiler rule.
151/// Excluding it from `StateContract` is what makes that builder reject it at
152/// compile time, forcing every caller through the world-authority-gated
153/// `SetupContext::world_clock_publisher` in the `phoxal` crate instead
154/// (`Self: world-authority surface`).
155///
156/// Bounds [`WorldClockPublisher`](crate::handle::publisher::WorldClockPublisher), a
157/// dedicated handle type separate from
158/// [`StatePublisher`](crate::handle::publisher::StatePublisher) even though both publish
159/// at a logical step with the same [`StepStamp`](crate::handle::stamp::StepStamp)
160/// path: sharing one generic handle type across both traits would force
161/// `StatePublisher`'s bound onto a common supertrait, which would blur an
162/// ordinary participant's "wrong contract for `StatePublisher`" compile error
163/// (today a precise `B: StateContract` message with the real `state` topics
164/// listed as candidates) into a less legible one naming an internal plumbing
165/// trait instead. Two small handle types keep that diagnostic exact.
166pub trait WorldClockContract: EndpointDescriptor {}
167
168/// Marker for an endpoint whose temporal meaning is an ordered stream chunk.
169pub trait StreamContract: EndpointDescriptor {}
170
171/// Marker for a state-temporal, ordered event endpoint.
172///
173/// Events use the stream transport's ordered/gap-visible behavior but are
174/// stamped like state at a logical step. Generated endpoint descriptors should
175/// implement this marker and `StreamDeliveryContract`; the payload remains a
176/// plain serde type.
177pub trait EventContract: EndpointDescriptor + StreamDeliveryContract {}
178
179/// Marker for the endpoint kind `Sample`.
180pub trait SampleContract: EndpointDescriptor {}
181
182/// Marker for the endpoint kind `Setpoint`.
183pub trait SetpointContract: EndpointDescriptor {}
184
185/// Marker for a contract whose transport retains the newest state snapshot.
186///
187/// This is deliberately independent from the temporal publisher marker above:
188/// a diagnostic or event can use state-temporal stamping while requiring an
189/// ordered transport family.
190pub trait StateDeliveryContract: EndpointDescriptor {}
191
192/// Marker for a contract whose transport preserves bounded ordered samples
193/// with explicit loss evidence.
194pub trait SampleDeliveryContract: EndpointDescriptor {}
195
196/// Marker for a contract whose transport retains only the newest actionable
197/// intent.
198pub trait SetpointDeliveryContract: EndpointDescriptor {}
199
200/// Marker for a contract whose transport preserves ordered chunks and surfaces
201/// saturation rather than evicting an older chunk.
202pub trait StreamDeliveryContract: EndpointDescriptor {}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
209    struct SharedPayload {
210        value: u8,
211    }
212
213    enum TestApi {}
214
215    impl ApiFamily for TestApi {
216        const ID: &'static str = "endpoint-test";
217    }
218
219    struct StateEndpoint;
220    struct EventEndpoint;
221
222    impl EndpointDescriptor for StateEndpoint {
223        type Api = TestApi;
224        type Payload = SharedPayload;
225
226        const NAME: &'static str = "endpoint-test::state";
227        const FAMILY: &'static str = "endpoint-test";
228        const CONTRACT: &'static str = "state";
229        const TOPIC: &'static str = "endpoint-test/state";
230        const KIND: EndpointKind = EndpointKind::State;
231    }
232
233    impl StateContract for StateEndpoint {}
234    impl StateDeliveryContract for StateEndpoint {}
235
236    impl EndpointDescriptor for EventEndpoint {
237        type Api = TestApi;
238        type Payload = SharedPayload;
239
240        const NAME: &'static str = "endpoint-test::event";
241        const FAMILY: &'static str = "endpoint-test";
242        const CONTRACT: &'static str = "event";
243        const TOPIC: &'static str = "endpoint-test/event";
244        const KIND: EndpointKind = EndpointKind::Event;
245    }
246
247    impl EventContract for EventEndpoint {}
248    impl StreamDeliveryContract for EventEndpoint {}
249
250    fn accepts_state_endpoint<E: EndpointDescriptor<Payload = SharedPayload>>() {}
251
252    fn accepts_event_handles(
253        _: Option<crate::handle::publisher::EventPublisher<EventEndpoint>>,
254        _: Option<crate::handle::subscriber::EventReceiver<EventEndpoint>>,
255    ) {
256    }
257
258    #[test]
259    fn one_plain_payload_can_be_reused_by_distinct_endpoint_descriptors() {
260        accepts_state_endpoint::<StateEndpoint>();
261        accepts_state_endpoint::<EventEndpoint>();
262        accepts_event_handles(None, None);
263
264        assert_eq!(StateEndpoint::KIND, EndpointKind::State);
265        assert_eq!(EventEndpoint::KIND, EndpointKind::Event);
266        assert_ne!(StateEndpoint::TOPIC, EventEndpoint::TOPIC);
267        assert_eq!(
268            EndpointKind::Event.delivery_family(),
269            DeliveryFamily::Stream
270        );
271    }
272}