Expand description
The single API layer (D60/D61/D1).
This crate is the versioned 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::v1 as api;, so
phoxal_api::v1, phoxal_api::ApiVersion, and phoxal_api::ContractBody
are the canonical paths.
§Stable and preview API versions
An API version is a conventional vN module generated by
phoxal_api_tree!. Each version module carries:
- a zero-variant marker
enum Api {}implementingApiVersion, whoseApiVersion::IDis the module name ("v1"or"v2"); - the version-local wire bodies, one
pub modper contract node holding plain serde structs/enums and theirContractBodyimpls; - an api-local
topicbuilder rooted attopic::new().
v1 is the current production surface and may change in place while the
pre-stability topology is simplified. v2 is the single preview surface for
contracts that have not entered production. Preview work does not mint a new
public version for each change.
v2 is authored as preview version v2 { … } and is available when the
preview-v2 Cargo feature is enabled. ApiVersion::IS_PREVIEW records the
lifecycle without changing topics or wire bytes.
§Per-field versions and per-contract identity
A participant declares its bus surface with a companion
#[derive(phoxal::Api)] handle struct.
Each handle field names its own version-qualified contract type, so one
participant may freely mix fields from modules such as v1 and
v2.
The derive records each field’s resolved ContractBody::VERSION and
ContractBody::CONTRACT in the participant’s embedded metadata and does
not declare a participant-wide API version.
Across the graph, compatibility is name identity (D1) - two participants
interoperate on a contract iff they use the exact same version-qualified name
(v1::drive::Target), which is real on the wire because the version
is folded into the key (ContractBody::TOPIC). There is no schema_id: a
stable 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 version-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 version, then the /-joined node path plus the
topic leaf, with each dynamic node contributing a {var} placeholder, e.g.
v1/component/{instance}/motor/{capability}/command. A fully static path
has a literal key (v1/drive/state). Folding the version 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 version-qualified key v1/drive/state, and
api::topic::new().component("base").motor("left").command() fills the dynamic
segments to produce v1/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§
- v1
- Production API version
v1- 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.