Expand description
The single API layer (D60/D61/D1).
This crate is the dated API contract tree. It depends only on the
phoxal-bus ABI floor (the contract primitive traits and the
typed-topic builders) and the phoxal-macros proc-macros; it
does not depend on the phoxal engine. A participant depends on both crates
and imports the API tree directly with use phoxal_api::y2026_1 as api;, so
phoxal_api::y2026_1, phoxal_api::ApiVersion, and phoxal_api::ContractBody
are the canonical paths.
§Dated API versions
An API version is a dated module (phoxal_api::y2026_1, …) generated by
phoxal_api_tree!. Each version module carries:
- a zero-variant marker
enum Api {}implementingApiVersion, whoseApiVersion::IDis the dated module name ("y2026_1"); - the version-local wire bodies, one
pub modper contract node holding plain serde structs/enums and theirContractBodyimpls; - an api-local
topicbuilder rooted attopic::new().
Each generation is a standalone, sparse batch (D1): only the contracts
minted in that batch, never a copy of an earlier generation. There is no
extends - an unchanged contract simply keeps its existing name as the one
live identity; a changed contract is minted fresh in the current generation.
A not-yet-promoted generation is authored as preview version y2026_N { … };
it still lives at the final module path (phoxal_api::y2026_N) but is
available only when the matching preview-y2026_N Cargo feature is enabled.
The generated module docs call out the preview status, and
ApiVersion::IS_PREVIEW records the lifecycle without changing topics or
wire bytes.
§Per-field generations and per-contract identity
A participant declares its bus surface with a companion
#[derive(phoxal::Api)] handle struct.
Each handle field names its own generation-qualified contract type, so one
participant may freely mix fields from modules such as y2026_1 and
y2026_7.
The derive records each field’s resolved ContractBody::GENERATION and
ContractBody::CONTRACT in the participant’s embedded metadata and does
not declare a participant-wide API generation.
Across the graph, compatibility is name identity (D1) - two participants
interoperate on a contract iff they use the exact same version-qualified name
(y2026_1::drive::Target), which is real on the wire because the generation
is folded into the key (ContractBody::TOPIC). There is no schema_id: a
released contract type is immutable, so the name alone is the whole identity.
§Plain serde wire bodies, provenance in metadata
A wire body is just its serde encoding - there is no {"v":…} envelope or any
other version tag inside the payload (D62). Identity lives entirely in the
Zenoh key (the generation-qualified ContractBody::TOPIC); the bus metadata
alongside the encoded body carries only provenance (source + logical time) and
the codec that produced the bytes - never schema/family/version. Keeping
identity out of both the payload and the metadata means the body bytes for an
unchanged contract are identical across codecs, and a receiver’s per-key
subscription is the whole fast-reject.
§Topic
ContractBody::TOPIC is derived from the contract node’s path in the tree,
never written by hand: the generation, then the /-joined node path plus the
topic leaf, with each dynamic node contributing a {var} placeholder, e.g.
y2026_1/component/{instance}/motor/{capability}/command. A fully static path
has a literal key (y2026_1/drive/state). Folding the generation into the key
(D1) is what makes two differently-versioned contracts physically distinct
Zenoh keys - they cannot collide, so there is no SCHEMA_ID/FAMILY needed to
disambiguate them.
§The api-local topic builder
Each version module exposes a topic builder that mirrors the node tree:
api::topic::new() returns a root, one method per top-level node walks down the
tree, a dynamic node’s method takes its variable as impl Display, and a leaf
method binds the topic’s side-branded kind to its version-local body. For
example api::topic::new().drive().state() yields a
Topic<Subscribe<drive::State>> (the CLIENT observes the owner’s state) over
the generation-qualified key y2026_1/drive/state, and
api::topic::new().component("base").motor("left").command() fills the dynamic
segments to produce y2026_1/component/base/motor/left/command. Because the
builder is generated from the same tree as TOPIC, the built key and the
documented key stay in lockstep.
§Owner side: topic::internal
The PUBLIC topic::new()... chain above is the client side. The matching
owner side lives at api::topic::internal::new(cap)... (L1 + L2, plan #00):
the same node tree and keys, but the leaf brands flip so the owner gets the side
it must take - api::topic::internal::new(cap).drive().state() is
Topic<Publish<drive::State>> (the owner publishes its telemetry), and
api::topic::internal::new(cap).drive().target() is Topic<Subscribe<drive::Target>>
(the owner reads its command input). A query owner reaches its ServeQuery
brand the same way. The internal chain is the deliberate, greppable owner
opt-in; a participant acquires the topics of its OWN node through it and everything
it consumes through the public chain.
The internal::new entry requires the runner-minted owner capability
(OwnerCap, Layer 2): a participant obtains it from
phoxal::SetupContext::owner_capability() and passes it in. On the documented
surface, owning a topic therefore cannot happen by accident - only with a
capability the runner mints.
Modules§
- y2026_1
- Dated API version
y2026_1- version-local wire bodies + topics. - y2026_7
- Dated API version
y2026_7- version-local wire bodies + topics. - y2026_8
- Dated API version
y2026_8- version-local wire bodies + topics.
Traits§
- ApiVersion
- The contract primitive traits, re-exported from the
phoxal-buscrate (the ABI floor) so they stay addressable atphoxal_api::ApiVersion/phoxal_api::ContractBody. - Contract
Body - The contract primitive traits, re-exported from the
phoxal-buscrate (the ABI floor) so they stay addressable atphoxal_api::ApiVersion/phoxal_api::ContractBody.