Skip to main content

Crate phoxal_api

Crate phoxal_api 

Source
Expand description

The single API layer (D60/D61).

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 runtime 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 {} implementing ApiVersion, whose ApiVersion::ID is the dated module name ("y2026_1") - the canonical version identity;
  • the version-local wire bodies, one pub mod per contract node holding plain serde structs/enums and their ContractBody impls;
  • an api-local topic builder rooted at topic::new().

Versions can build on earlier ones with version y2026_2 extends y2026_1: every inherited node and type is re-emitted fresh under the new module - same ContractBody::FAMILY and ContractBody::TOPIC, a different Api - so an unchanged contract stays wire-identical while the compile-time Api bound stays distinct. See phoxal_api_tree! for the inheritance and shape-identity rules.

§One API version per runtime, one per graph

A runtime authors against exactly one version module (use phoxal_api::y2026_1 as api;) and declares it on the derive (#[phoxal(api = y2026_1)]). Every handle body is bound ContractBody<Api = R::Api>, so passing a body from another API version is a compile error (D59/D60). At deploy time a single graph runs a single API version: all of its runtimes share one Api, so contracts on the bus are mutually consistent by construction (D59).

§Plain serde wire bodies, version in metadata

A wire body is just its serde encoding - there is no {"v":…} envelope or any other version tag inside the payload (D62). The three pieces of routing identity travel as bus metadata alongside the encoded body, not in it:

Keeping these out of the payload means the body bytes for an unchanged contract are identical across versions and codecs; the envelope/metadata layer owns version and family.

§Family and topic

Both identifiers are derived from the contract node’s path in the tree, never written by hand:

  • ContractBody::FAMILY is the ::-joined node path plus the body type name, e.g. component::motor::Command. It excludes dynamic variables, so every instance of a per-instance node shares one family.
  • ContractBody::TOPIC is the versionless /-joined key with each dynamic node contributing a {var} placeholder, e.g. component/{instance}/motor/{capability}/command. A fully static path has a literal key (drive/state).

§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 static key drive/state, and api::topic::new().component("base").motor("left").command() fills the dynamic segments to produce component/base/motor/left/command. Because the builder is generated from the same tree as FAMILY/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 runtime 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 runtime 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 #id — version-local wire bodies + topics.

Traits§

ApiVersion
The contract primitive traits, re-exported from the phoxal-bus crate (the ABI floor) so they stay addressable at phoxal_api::ApiVersion / phoxal_api::ContractBody.
ContractBody
The contract primitive traits, re-exported from the phoxal-bus crate (the ABI floor) so they stay addressable at phoxal_api::ApiVersion / phoxal_api::ContractBody.