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 carried in bus metadata as
18/// informational provenance, never in the wire body or the topic key (D62).
19/// [`IS_PREVIEW`] records authoring lifecycle only; it has no wire effect.
20///
21/// [`ID`]: ApiVersion::ID
22/// [`IS_PREVIEW`]: ApiVersion::IS_PREVIEW
23/// [`phoxal_api_tree!`]: https://docs.rs/phoxal
24pub trait ApiVersion: 'static {
25    /// The dated API-version identifier, equal to the version module name, e.g.
26    /// `"y2026_1"`.
27    const ID: &'static str;
28    /// Whether this generated API version is still in the preview lifecycle.
29    ///
30    /// This is control-plane metadata only. It is not encoded in bus payloads,
31    /// topics, schema ids, or encoding strings.
32    const IS_PREVIEW: bool = false;
33}
34
35/// The semantic role a topic plays in its owning service's contract.
36///
37/// Every topic in a [`phoxal_api_tree!`] declares one of these (D63, plan #00).
38/// The role records *intent*, separate from the wire shape: a `Command` and a
39/// `State` topic are both pub/sub on the wire, but the owner subscribes a
40/// `Command` (it is the service's control input) and publishes a `State` (it is
41/// the service's telemetry output). `Query` is the request/response role.
42///
43/// The role drives the side branding (L1): the api tree's builders read it to pick
44/// each leaf's side-branded topic kind (`Publish`/`Subscribe`/`AskQuery`/`ServeQuery`),
45/// so taking the wrong side of a topic is a compile error. The role also rides
46/// alongside each generated contract body as a `ROLE` const; that const is not yet
47/// emitted by `emit-apis` (a later increment of plan #00).
48///
49/// [`phoxal_api_tree!`]: https://docs.rs/phoxal
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum TopicRole {
52    /// A control input the owning service subscribes (e.g. `drive/target`).
53    Command,
54    /// A telemetry/output the owning service publishes (e.g. `drive/state`).
55    State,
56    /// A request/response topic the owning service answers (e.g. `map/submap`).
57    Query,
58}
59
60impl TopicRole {
61    /// The lowercase grammar keyword for this role (`"command"` / `"state"` /
62    /// `"query"`), matching how it is written in `phoxal_api_tree!`.
63    pub const fn as_str(self) -> &'static str {
64        match self {
65            TopicRole::Command => "command",
66            TopicRole::State => "state",
67            TopicRole::Query => "query",
68        }
69    }
70}
71
72/// A version-local wire body: a plain serde type bound to exactly one
73/// [`ApiVersion`] and one contract family/topic (D61).
74///
75/// Every body declared inside a `phoxal_api_tree!` node gets a generated impl.
76/// Handles, `SetupContext` builders, and the `Service`/`Driver` derive assertions
77/// all key off [`Api`](ContractBody::Api)/[`FAMILY`](ContractBody::FAMILY)/[`TOPIC`](ContractBody::TOPIC),
78/// which is how a body from the wrong API version is rejected at compile time.
79///
80/// The serde encoding of an implementor *is* the wire payload; there is no version
81/// envelope (D62). Version and family travel as bus metadata, derived from
82/// [`Api`](ContractBody::Api) and [`FAMILY`](ContractBody::FAMILY).
83///
84/// Runtime decode compatibility keys off [`SCHEMA_ID`](ContractBody::SCHEMA_ID),
85/// not the graph API version. `SCHEMA_ID` is a normalized hash of the body's
86/// transitive wire shape. The canonical hash input is `phoxal.schema/v0\n`
87/// followed by this grammar:
88///
89/// ```text
90/// body    = unit | newtype | tuple | struct | enum
91/// struct  = "struct{" field* "}"
92/// field   = "f" byte_len ":" utf8_name "=" ty ";"
93/// enum    = "enum(" repr "){" variant* "}"
94/// repr    = "external" | "internal(tag=" name ")"
95///         | "adjacent(tag=" name ",content=" name ")" | "untagged"
96/// variant = "v" byte_len ":" utf8_name "=" payload ";"
97/// payload = unit | "newtype(" ty ")" | tuple | struct
98/// newtype = "newtype(" ty ")"
99/// tuple   = "tuple(" (ty ";")* ")"
100/// ty      = nil | bool | string | bytes | integer | float | option | seq | array
101///         | map | tuple | body
102/// integer = "u8" | "u16" | "u32" | "u64" | "u128"
103///         | "i8" | "i16" | "i32" | "i64" | "i128"
104/// float   = "f32" | "f64"
105/// option  = "option(" ty ")"
106/// seq     = "seq(" ty ")"
107/// array   = "array[" decimal_len "](" ty ")"
108/// map     = "map(" ty "," ty ")"
109/// name    = byte_len ":" utf8_name
110/// ```
111///
112/// Fields stay in declared order and use their serde wire names, including
113/// `rename` and `rename_all`. Enums include serde representation mode, variant
114/// wire names, and payload shapes. Nested API-tree structs and enums are expanded
115/// transitively, with generic type parameters substituted by concrete type
116/// arguments before expansion. Only understood serde attributes and known
117/// wire-neutral attributes such as doc comments and simple derives are accepted.
118/// Non-wire details such as Rust type identifiers when the wire shape has already
119/// been expanded are excluded. The displayed id is lower-hex
120/// `sha256(canonical_string)`, truncated to the first 16 hex characters.
121pub trait ContractBody:
122    serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static
123{
124    /// The single API version this body belongs to. Two bodies from different
125    /// versions have different `Api`, so the type system keeps them apart.
126    type Api: ApiVersion;
127    /// Canonical contract family id: the `::`-joined node path plus the body type
128    /// name, with dynamic variables excluded, e.g. `"drive::State"` or
129    /// `"component::motor::Command"`.
130    const FAMILY: &'static str;
131    /// Per-contract compatibility id: lower-hex SHA-256 of the canonical
132    /// transitive wire-shape string, truncated to 16 hex characters.
133    const SCHEMA_ID: &'static str;
134    /// Versionless topic key: the `/`-joined node path plus the topic leaf, with
135    /// each dynamic node contributing a `{var}` placeholder, e.g. `"drive/state"`
136    /// or `"component/{instance}/motor/{capability}/command"`. The concrete key is
137    /// produced by the api-local `topic` builder, which fills the placeholders.
138    const TOPIC: &'static str;
139}