ocpi-kit 0.2.0

OCPI (Open Charge Point Interface) toolkit for EV roaming: spec-exact typed models for OCPI 2.3.0 / 2.2.1 / 2.1.1, transport envelope, client, server and hub building blocks, and an auditable tariff engine.
Documentation
[workspace]
members = ["xtask"]
# `cargo test`, `cargo build` and friends act on the library alone; `xtask` is repository
# automation and is run explicitly with `cargo run -p xtask`.
default-members = ["."]

[package]
name = "ocpi-kit"
version = "0.2.0"
edition = "2024"
rust-version = "1.96"
description = "OCPI (Open Charge Point Interface) toolkit for EV roaming: spec-exact typed models for OCPI 2.3.0 / 2.2.1 / 2.1.1, transport envelope, client, server and hub building blocks, and an auditable tariff engine."
license = "MIT OR Apache-2.0"
repository = "https://github.com/hupe1980/ocpi-kit"
homepage = "https://github.com/hupe1980/ocpi-kit"
documentation = "https://docs.rs/ocpi-kit"
readme = "README.md"
keywords = ["ocpi", "ev", "charging", "roaming", "emobility"]
categories = ["network-programming", "web-programming", "api-bindings"]
exclude = ["/specs", "/.github", "/site", "/xtask"]

[lib]
name = "ocpi_kit"

[dependencies]
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
rust_decimal = { version = "1.42.1", default-features = false, features = ["std", "serde"] }
time = { version = "0.3.55", default-features = false, features = ["std", "formatting", "parsing", "macros"] }
thiserror = "2.0.20"
bon = "3.10.0"

# transport
base64 = { version = "0.23.1", optional = true }
http = { version = "1.5.0", optional = true }
subtle = { version = "2.6.1", optional = true }
url = "2.5.8"
uuid = { version = "1.26.0", optional = true, features = ["v4"] }
zeroize = { version = "1.9.0", optional = true, features = ["derive"] }

# client
futures-core = { version = "0.3.34", optional = true }
futures-util = { version = "0.3.34", optional = true, default-features = false, features = ["std"] }
reqwest = { version = "0.13.4", optional = true, default-features = false, features = [
    "rustls",
    "json",
    "http2",
    "charset",
] }
serde_path_to_error = { version = "0.1.20", optional = true }
tokio = { version = "1.53.1", optional = true, features = ["time", "rt", "sync"] }
tracing = { version = "0.1.44", optional = true }

# server
axum = { version = "0.8.9", optional = true, default-features = false, features = [
    "http1",
    "json",
    "query",
    "tokio",
] }
tower = { version = "0.5.3", optional = true }

# tariffs
tzdb = { version = "0.7.3", optional = true, default-features = false, features = ["std"] }
tz-rs = { version = "0.7.0", optional = true, default-features = false, features = ["std"] }

# schema export
schemars = { version = "1.2.2", optional = true }

# testkit
rand = { version = "0.10.2", optional = true }

# cli
clap = { version = "4.6.6", optional = true, features = ["derive", "env"] }

[dev-dependencies]
criterion = { version = "0.8.0", default-features = false, features = ["cargo_bench_support"] }
insta = { version = "1.48.0", features = ["json"] }
proptest = "1.11.0"
serde_json = { version = "1.0.151", features = ["preserve_order"] }
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
wiremock = "0.6.5"

[features]
default = ["v2_3_0", "v2_2_1", "transport"]

# ---- wire models -------------------------------------------------------
## OCPI 2.3.0 wire model (the canonical model of this crate).
v2_3_0 = []
## OCPI 2.2.1 wire model, defined as a delta from the 2.3.0 model.
v2_2_1 = ["v2_3_0"]
## OCPI 2.1.1 wire model (legacy), defined as a delta from the 2.3.0 model.
v2_1_1 = ["v2_3_0"]
## Bookings extension module of the OCPI 2.3.0 `bookings` release branch.
bookings = ["v2_3_0"]
## Invoice Reconciliation extension module of the OCPI 2.3.0 `payments` release branch.
invoice-reconciliation = ["v2_3_0"]

# ---- layers ------------------------------------------------------------
## Envelope, headers, credentials tokens, pagination, routing, URL builders, PATCH.
transport = ["dep:base64", "dep:http", "dep:subtle", "dep:uuid", "dep:zeroize"]
## Async OCPI client on top of `reqwest`.
##
## The layers above `transport` all speak the 2.3.0 model — it is the canonical one, and the
## other versions are defined as deltas from it — so they enable `v2_3_0` rather than failing to
## compile without it. They also enable `convert`, because a peer on an older version is normal
## rather than exceptional: bridging it onto the canonical model is how they stay usable.
client = [
    "transport",
    "v2_3_0",
    "convert",
    "dep:futures-core",
    "dep:futures-util",
    "dep:reqwest",
    "dep:serde_path_to_error",
    "dep:tokio",
    "dep:tracing",
]
## Async OCPI server on top of `axum`.
server = [
    "transport",
    "v2_3_0",
    "convert",
    "dep:axum",
    "dep:tower",
    "dep:tokio",
    "dep:tracing",
    "dep:serde_path_to_error",
]
## Hub: routing table, forwarder, broadcast push, GET-All merge, version bridging.
hub = ["client", "server"]
## Conversions between OCPI versions with loss accounting.
##
## It converts *between* two models, so it declares both: a feature that only compiles when some
## other feature happens to be on is not a feature.
convert = ["v2_2_1", "v2_3_0"]
## Auditable tariff/pricing engine.
tariffs = ["v2_3_0", "dep:tzdb", "dep:tz-rs"]
## Sample objects and in-memory stores for tests; with `server`, a complete conformant OCPI
## party (`MockPeer`) to point a partner's client at.
testkit = ["transport", "v2_3_0", "dep:rand"]
## `JsonSchema` derives for every wire type.
schema = ["dep:schemars"]
## The `ocpi` command line tool.
cli = ["full", "dep:clap"]

## Everything except the CLI binary.
full = [
    "v2_1_1",
    "v2_2_1",
    "v2_3_0",
    "bookings",
    "invoice-reconciliation",
    "transport",
    "client",
    "server",
    "hub",
    "convert",
    "tariffs",
    "testkit",
    "schema",
]

[[bin]]
name = "ocpi"
path = "src/bin/ocpi.rs"
required-features = ["cli"]

# Integration tests each need the feature set they exercise; without these they would fail to
# compile under `--no-default-features` and under partial feature selections.
[[test]]
name = "fixtures"
required-features = ["v2_2_1", "transport"]

[[test]]
name = "end_to_end"
required-features = ["client", "server", "testkit"]

[[test]]
name = "modules"
required-features = ["client", "server", "testkit"]

[[test]]
name = "hub"
required-features = ["hub", "testkit"]

[[test]]
name = "version_bridging"
required-features = ["convert", "v2_2_1", "tariffs"]

[[test]]
name = "versions_on_the_wire"
required-features = ["client", "server", "hub", "testkit"]

[[test]]
name = "snapshots"
required-features = ["tariffs", "transport", "v2_3_0"]

[[test]]
name = "properties"
required-features = ["transport", "tariffs", "v2_3_0"]

[[test]]
name = "mock_peer"
required-features = ["client", "testkit"]

[[test]]
name = "mock_peer_server"
required-features = ["client", "server", "testkit"]

[[bench]]
name = "wire"
harness = false
required-features = ["full"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lints.rust]
missing_docs = "deny"
unsafe_code = "forbid"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs)'] }

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
# A trait method declared `async fn` must be implemented as one even when the body does not
# await, which is most of the server's extractors and handler impls.
unused_async = "allow"
unused_async_trait_impl = "allow"
# The per-version modules mirror one another's signatures; taking a `Copy` enum by value in one
# version and by reference in another because that version models it as an open enum would be a
# worse API than the small clone.
needless_pass_by_value = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
struct_excessive_bools = "allow"
too_many_lines = "allow"
doc_markdown = "allow"