Skip to main content

phoxal_bus/
contract.rs

1//! The contract primitive traits (D60/D61): the API-version marker and the
2//! version-local wire body.
3//!
4//! These are the two traits the bus client is generic over - the ABI floor every
5//! contract body and api-version marker implements. The concrete dated API
6//! versions (`phoxal_api::y2026_1`, …) and the `phoxal_api_tree!` macro that
7//! generates their `ApiVersion` / `ContractBody` impls live in the `phoxal-api`
8//! crate, which re-exports these traits - so they are reachable as
9//! `phoxal_api::ApiVersion` / `phoxal_api::ContractBody`. The `phoxal` engine
10//! re-exports this bus crate at `phoxal::bus`, so they are also reachable as
11//! `phoxal::bus::ApiVersion` / `phoxal::bus::ContractBody`.
12
13/// Marker trait identifying one dated API version (D60).
14///
15/// Implemented only by the zero-variant `enum Api {}` that
16/// [`phoxal_api_tree!`] generates inside each version module. The [`ID`] is the
17/// dated module name (`"y2026_1"`) and is the canonical version identity: it is
18/// carried in bus metadata, never in the wire body or the topic key (D62).
19///
20/// [`ID`]: ApiVersion::ID
21/// [`phoxal_api_tree!`]: https://docs.rs/phoxal
22pub trait ApiVersion: 'static {
23    /// The dated API-version identifier, equal to the version module name, e.g.
24    /// `"y2026_1"`.
25    const ID: &'static str;
26}
27
28/// The semantic role a topic plays in its owning service's contract.
29///
30/// Every topic in a [`phoxal_api_tree!`] declares one of these (D63, plan #00).
31/// The role records *intent*, separate from the wire shape: a `Command` and a
32/// `State` topic are both pub/sub on the wire, but the owner subscribes a
33/// `Command` (it is the service's control input) and publishes a `State` (it is
34/// the service's telemetry output). `Query` is the request/response role.
35///
36/// The role drives the side branding (L1): the api tree's builders read it to pick
37/// each leaf's side-branded topic kind (`Publish`/`Subscribe`/`AskQuery`/`ServeQuery`),
38/// so taking the wrong side of a topic is a compile error. The role also rides
39/// alongside each generated contract body as a `ROLE` const; that const is not yet
40/// emitted by `emit-apis` (a later increment of plan #00).
41///
42/// [`phoxal_api_tree!`]: https://docs.rs/phoxal
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub enum TopicRole {
45    /// A control input the owning service subscribes (e.g. `drive/target`).
46    Command,
47    /// A telemetry/output the owning service publishes (e.g. `drive/state`).
48    State,
49    /// A request/response topic the owning service answers (e.g. `map/submap`).
50    Query,
51}
52
53impl TopicRole {
54    /// The lowercase grammar keyword for this role (`"command"` / `"state"` /
55    /// `"query"`), matching how it is written in `phoxal_api_tree!`.
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            TopicRole::Command => "command",
59            TopicRole::State => "state",
60            TopicRole::Query => "query",
61        }
62    }
63}
64
65/// A version-local wire body: a plain serde type bound to exactly one
66/// [`ApiVersion`] and one contract family/topic (D61).
67///
68/// Every body declared inside a `phoxal_api_tree!` node gets a generated impl.
69/// Handles, `SetupContext` builders, and the `#[derive(Runtime)]` assertions all
70/// key off [`Api`](ContractBody::Api)/[`FAMILY`](ContractBody::FAMILY)/[`TOPIC`](ContractBody::TOPIC),
71/// which is how a body from the wrong API version is rejected at compile time.
72///
73/// The serde encoding of an implementor *is* the wire payload; there is no version
74/// envelope (D62). Version and family travel as bus metadata, derived from
75/// [`Api`](ContractBody::Api) and [`FAMILY`](ContractBody::FAMILY).
76pub trait ContractBody:
77    serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static
78{
79    /// The single API version this body belongs to. Two bodies from different
80    /// versions have different `Api`, so the type system keeps them apart.
81    type Api: ApiVersion;
82    /// Canonical contract family id: the `::`-joined node path plus the body type
83    /// name, with dynamic variables excluded, e.g. `"drive::State"` or
84    /// `"component::motor::Command"`.
85    const FAMILY: &'static str;
86    /// Versionless topic key: the `/`-joined node path plus the topic leaf, with
87    /// each dynamic node contributing a `{var}` placeholder, e.g. `"drive/state"`
88    /// or `"component/{instance}/motor/{capability}/command"`. The runtime key is
89    /// produced by the api-local `topic` builder, which fills the placeholders.
90    const TOPIC: &'static str;
91}