ocpi-kit

Rust · OCPI 2.3.0 · 2.2.1 · 2.1.1

OCPI for Rust,
without the rounding errors

Spec-exact wire models for every OCPI version in production, plus the transport envelope, an async client, an axum server, the pieces a roaming hub needs, and an auditable tariff engine. One crate, behind cargo features.

cargo add ocpi-kit

Three properties that decide quality

Money is never a float

Every number is an exact decimal. No public field of any OCPI object is an f32 or f64, the pricing engine has none either, and a repository check enforces it in CI.

A binary float cannot represent 0.10. Every other Rust OCPI type set models prices as f64; cents decide real disputes.

Numbers and money →

Nothing a peer sent is thrown away

Undocumented JSON fields land in an Extensions map and are written back verbatim. An enum value this crate has never heard of keeps its text.

A hub built on ocpi-kit forwards a vendor extension it does not understand without damaging it — which is what OCPI 2.3.0's extensibility chapter asks for.

Extensions →

Parsing and conformance are separate

A peer that overruns a string(45) cannot make a whole page of Locations undecodable. The value arrives; validation reports it with an RFC 6901 JSON Pointer.

Parse permissively, validate explicitly, construct strictly.

The rule →

The handshake, as a typestate

Most OCPI integrations break in the credentials exchange, and every failure mode is a state mistake: using CREDENTIALS_TOKEN_A after registering, POSTing credentials twice and getting a 405, forgetting to re-fetch endpoints after a PUT.

So the states are types. You cannot call a module endpoint before discovery, and the bootstrap token is gone once you have registered.

Client →
let peer = Registration::new(versions_url, token_a)
    .discover(client.transport()).await?
    .select_best(client.transport()).await?;

// Refuse a peer that does not implement what you need — before anything is sent.
peer.require(&[(ModuleId::Locations, InterfaceRole::Sender)])?;

let peer = peer.register(client.transport(), &my_credentials).await?;

// Then pull, following every `Link: rel="next"`.
let mut locations = peer.locations(client.transport(), me).list(PageQuery::new())?;
while let Some(location) = locations.next().await? {
    println!("{} {}", location.id, location.name.as_deref().unwrap_or(""));
}

What is in the box

Checked against the specification, not against intent

Every public item names the AsciiDoc anchor it implements, and repository automation compares the crate with the specification on every push.

How it is verified