Cargo.toml, Toolchain, and lib.rs
Foundations gave you the mental model; this part builds a real client crate,
in the order you would build one. A Capi client is
a fixed set of files, each with one job,
so scaffolding is mostly laying down known slots. This chapter lays the
foundation: the manifest, the verification policy it carries, the toolchain
pin, the generated test-lane aliases, and the lib.rs that wires the modules
together.
The running example is a hypothetical parcel_capi_rs. Every block below is
the shape the family’s reference clients ship — google_places_capi_rs,
grok_capi_rs, aws_s3_capi_rs — with the service’s names swapped out.
The manifest
[package]
name = "parcel_capi_rs"
version = "0.1.0"
edition = "2021"
rust-version = "1.88.0"
resolver = "3"
description = "Client for the Parcel API — shipments, rates, and tracking — built on Capi."
license = "MIT OR Apache-2.0"
[package.metadata.docs.rs]
all-features = true
# Verification policy the family's verifier reads (`cargo capi verify`).
[package.metadata.capi]
[features]
default = ["default-codecs", "std"]
## Batteries-included codecs: `capiw_serde_json` for JSON bodies and
## `capiw_urlencoded` for query strings, unlocking `ParcelApi::new_with_defaults`.
default-codecs = ["dep:capiw_serde_json", "dep:capiw_urlencoded"]
## Asynchronous APIs; the same endpoint definitions compile sync or async.
## Browser wasm always carries the async surface.
async = ["capi_rs/async", "capi_websocket?/async", "capic_wasm_fetch/async"]
## WebSocket endpoints (the live tracking feed).
websocket = ["dep:capi_websocket", "std"]
## Standard-library support on native/WASI; inert as a std switch on browser
## wasm.
std = ["capi_rs/std", "capi_wire_datetime/std", "thiserror/std", "capi_websocket?/std"]
[dependencies]
capi_rs = { version = "0.5", default-features = false, features = ["derive"] }
thiserror = { version = "2.0", default-features = false }
# Wire model + derives: the derive-generated code references `::capi_wire_model::…`.
capi_wire_model = { version = "0.5", default-features = false, features = ["derive"] }
capi_wire_datetime = { version = "0.5", default-features = false }
capiw_ext_json = { version = "0.5", default-features = false } # the `Json` marker
capiw_ext_urlencoded = { version = "0.5", default-features = false } # the `UrlForm` marker
# Capability crates the client opts into behind a feature of its own.
capi_websocket = { version = "0.5", default-features = false, optional = true }
# Optional batteries-included codecs, gated so a consumer can bring their own.
capiw_serde_json = { version = "0.5", default-features = false, optional = true }
capiw_urlencoded = { version = "0.5", default-features = false, optional = true }
# Browser wasm ships the Fetch transport with the client — the target selects
# it; no feature is involved.
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
capic_wasm_fetch = { version = "0.5" }
[dev-dependencies]
# The replay harness rides its std default. Async-lane test runs select the
# lane on the command line (`cargo test-async`, below).
capi_test_framework = { version = "0.5" }
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dev-dependencies]
# The recording lane is native-only: the family's own engine-backed
# transport, through which fixtures are captured.
capi_test_framework = { version = "0.5", features = ["native-client"] }
[lints.rust]
missing_docs = "warn"
unsafe_code = "deny"
[lints.rustdoc]
broken_intra_doc_links = "deny"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "allow", priority = -1 }
nursery = { level = "allow", priority = -1 }
Reading it top to bottom:
rust-version is declared, and it is the framework’s floor. Every
package in the family declares rust-version; the verifier’s manifest lint
treats a missing one as an error with no allow form. 1.88.0 is the floor
for capi_rs and the framework’s core crates, and the lowest a client
built on them can declare. A client that leans on later Rust declares its
own, higher floor — and pins its toolchain to match, below.
resolver = "3" makes Cargo’s version selection MSRV-aware, so a
transitive dependency that needs a later compiler than the declared floor
is passed over rather than pulled in. Edition 2021 defaults to resolver 2,
so the opt-in is spelled out.
description is the crate’s opening sentence. The first //! line of
lib.rs repeats it verbatim; the two are kept in lockstep so docs.rs, the
registry, and the crate root say the same thing.
docs.rs renders all-features. Features are additive per target, so
the richest set renders; the verifier’s docsrs lane builds the same set
under -D warnings. The one feature never in a docs.rs set is
single-threaded, which no client declares.
[package.metadata.capi] is the verification policy. The family’s
verifier, cargo capi verify, reads how a repo verifies from the repo
itself, in this table. An empty table is a complete policy: the default
lanes apply. A client adds a key when its shape needs one:
[package.metadata.capi]
# A feature that pulls platform-bound crates (a std-locked codec, the
# jwt/oauth2 flows): the featureless lanes leave it off.
platform-std-features = ["auth-jwt", "auth-oauth2"]
# A second transport the async test lane must also switch on.
extra-async-test-features = ["capic_reqwest/grpc"]
# Extra aliases for the generated block (below).
aliases = { test-live = "test --test live_tests -- --ignored" }
# A client that cannot build for the browser at all.
browser = false
Every key is validated against the metadata it describes — an unknown key, or a feature or dependency name the package does not have, is an error.
[features] forwards, never activates. The three framework axes reach
every dependency that declares them: std and async into the facade, the
wire crates that carry them, thiserror, and any capability crate. Two
details are load-bearing. The ? in capi_websocket?/async forwards the
axis if the optional dependency is already in the build and does not pull
it in otherwise, so --features async alone leaves the WebSocket code out
while --features async,websocket builds it on the async lane. And async
must also reach capic_wasm_fetch: the Fetch transport declares its own
async axis and guards it, so a browser build with --features async that
did not forward it fails at that crate’s guard. The verifier’s Rule F —
the axes close over the whole family chain — has no allow form, because
the resulting lane cannot compile. No feature selects a platform; the
target does, per Features and Targets.
The facade is default-features = false plus derive. With
defaults off, the client’s own std decides when the facade’s std is on,
which is what makes the featureless build a real lane. derive
brings the endpoint macros; add cookies when the client uses the
cookie config layer, and auth-basic when it sends HTTP Basic.
The wire model and the format contracts are direct dependencies. The
facade re-exports the Capi framework workspace and nothing else. The
#[derive(WireModel)] output names ::capi_wire_model, so the crate must be
resolvable under that name; Codec<Json> needs the marker crate that
defines Json; the datetime carriers come from capi_wire_datetime. The
same applies to every add-on the client drives — capi_auth_oauth2,
capi_auth_jwt, capi_websocket, capi_html_forms, capi_grpc — each
with its own feature forwarding.
The transport table. Native consumers pick their own adapter, so none
appears in [dependencies]. The browser adapter is the one exception:
capic_wasm_fetch ships with the client from a browser-wasm target table,
because it is the only client a browser can run. A client that does not
target the browser at all sets browser = false in its policy and omits
the table.
Two dev-dependency tables. The replay harness is a plain
dev-dependency; the recording lane — native-client, the family’s own
engine-backed transport, or reqwest — lives in a dev-dependency table
whose target excludes browser wasm, because it cannot compile there. The
verifier’s Rule B requires that placement for any dev-dependency that
cannot build for the browser, and Rule A forbids a library feature from
forwarding into a dev-only dependency at all: Cargo would propagate the
feature name without activating the optional dependency it gates, and the
failure surfaces as an unresolved import far from its cause. That is why
the async test lane is named as an alias rather than forwarded.
[lints]. unsafe_code = "deny" and missing_docs = "warn" are the
family standard; broken_intra_doc_links = "deny" is what keeps the
rustdoc honest when a type moves; the clippy groups set all to warn and
leave pedantic and nursery off.
The codec dependency taxonomy
Recall the contract-vs-codec split: the
format contracts a client needs for its types (capiw_ext_json’s
Json, capiw_ext_urlencoded’s UrlForm) are unconditional dependencies —
they carry marker types and the format’s attribute vocabulary, and there is
nothing to gate. The codecs that turn bytes (capiw_serde_json,
capiw_urlencoded, capiw_quick_xml, capiw_prost) are optional, behind
default-codecs, so a consumer who wants a different engine is not made to
compile yours. XML and protobuf add their own contract crates
(capiw_ext_xml, capiw_ext_protobuf).
The gRPC lane is the one exception to the gate. A GrpcConfig must answer
protobuf_codec() unconditionally, so a client with gRPC endpoints carries
capiw_prost under its grpc feature rather than under default-codecs:
grpc = ["dep:capi_grpc", "dep:capiw_ext_protobuf", "dep:capiw_prost"]
capiw_quick_xml links the standard library, so a client whose
default-codecs includes it lists that feature under
platform-std-features, and the featureless lane leaves it off.
rust-toolchain.toml
[toolchain]
channel = "1.88.0"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]
The pin and the manifest’s rust-version are one number: the verifier’s
Rule J requires the toolchain file to pin exactly the highest
rust-version the repo’s packages declare, so the declared floor is the
compiler that builds them — a floating channel and a missing file are both
violations. The targets line is the one that bites when omitted: without
the wasm32 target installed, cargo check --target wasm32-unknown-unknown
fails with a missing-target error that reads like a code bug. Pinning it
here has rustup install it on first build.
.cargo/config.toml
The test lanes are named, not inferred, because async reshapes the traits
graph-wide and cannot be forwarded into the dev-only harness from a library
feature. cargo capi aliases writes them, and cargo capi aliases --check
fails when the committed block has drifted from what the manifest’s
metadata implies:
# >>> generated by `cargo capi aliases` — do not edit
[alias]
test-sync = "test"
test-async = "test --features async,capi_test_framework/async"
check-wasm = "check --target wasm32-unknown-unknown --all-targets"
# <<< end generated
Only the fenced block belongs to the generator; durable settings outside the
fences survive regeneration. One thing never goes in this file: a
[patch.crates-io] table. A patch entry names a filesystem path, and this
file is committed. During development against unpublished family crates the
table lives in a directory above every checkout, written by
cargo capi patch — the consuming chapter
covers it.
lib.rs
The crate root is no_std at its core — a client is mostly static data,
and staying no_std keeps it usable on every lane:
//! Client for the Parcel API — shipments, rates, and tracking — built on
//! Capi.
#![no_std]
#![allow(missing_copy_implementations)]
extern crate alloc;
#[cfg(all(feature = "std", not(all(target_arch = "wasm32", target_os = "unknown"))))]
extern crate std;
pub mod auth;
mod config;
pub mod decoders;
pub mod endpoints;
pub mod errors;
mod interface;
pub mod types;
pub use capi_rs::reexport;
pub use config::*;
pub use interface::*;
Four conventions:
extern crate stdis gated off browser wasm. Browser wasm never links the standard library; there,stdis the platform-capability switch, not a library. Thecfgsays exactly that.- Reach for
alloc, neverstd. Spellalloc::string::String,alloc::vec::Vec,alloc::boxed::Box,alloc::formatin every module. The prelude (use capi_rs::prelude::*;) is the authoring surface for framework names; of thealloctypes it re-exports onlyCow,format, andToString, soStringandVecare imported where they are used. A straystd::path is a resolution error on the browser lane and in the featureless build. configandinterfaceare private modules, glob-re-exported. Users writeparcel_capi_rs::ParcelConfig, not::config::ParcelConfig, while the types stay exported.auth,decoders,endpoints,errors, andtypesarepubbecause their contents are the documented surface.pub use capi_rs::reexport;passes the framework’s own re-export module through, so a consumer reaches framework types without addingcapi_rsas a direct dependency.
Verifying the crate
cargo capi verify, run in the repo, is the gate: the manifest lint, clippy
in the fixed lanes (default, featureless, std,async, and single-threaded
where declared), the lane-matched test runs, the doctests, and the
browser-wasm check. --full adds the feature powersets, the docs.rs
renders, the fixtures gate, and formatting — Verifying a Repo
is the reference for all of it. With the foundation in place, the next
chapter fills the most important file: the config.