sumo_types/lib.rs
1//! Reads — and, with the `write` feature, writes — [SUMO](https://sumo.dlr.de/)
2//! files as well-typed Rust structs.
3//!
4//! Each SUMO file format is its own Cargo feature, generated from that
5//! format's XSD; see `FEATURE_SCHEMAS` in `build.rs` for the current list.
6//! Three are implemented:
7//!
8//! | Feature | Format | Public API |
9//! |---|---|---|
10//! | `net` (default) | `.net.xml` (`netconvert` output) | `domain`, `read_network` |
11//! | `routes` | `.rou.xml` (traffic demand) | `routes` |
12//! | `additional` | `.add.xml` (E1/E2/E3 detectors) | `additional` |
13//!
14//! A fourth feature, `write`, is not a format but a direction: it adds a
15//! `write_*` counterpart to every enabled format's `read_*`, running the
16//! same pipeline backwards. It does nothing on its own.
17//!
18//! Those are written as plain code spans, not intra-doc links, throughout
19//! this crate-level comment: it can't be `#[cfg]`-gated (a `//!` comment
20//! isn't attached to a single item), so it is rendered under every feature
21//! combination, and a link to a feature-gated item is a broken link in every
22//! build that doesn't enable it. Everything named here is reachable from the
23//! module list below under the features that provide it.
24//!
25//! `net`'s API sits at the crate root (`sumo_types::domain::Network`,
26//! `sumo_types::read_network`) because it was this crate's first, only
27//! format; that path is kept stable even though `net`'s own files live
28//! under `src/net/` internally — a module's file location and its public
29//! path are independent in Rust, and `lib.rs` re-exports `net`'s public
30//! items at the root on purpose. `routes`, added later, didn't inherit that
31//! spot: it's namespaced at `sumo_types::routes` instead, both to avoid colliding
32//! with `net`'s type names (both formats have their own `Route`-shaped
33//! concepts: `net::domain` doesn't, but a third format might) and so the
34//! module a type is imported from tells you which SUMO format it belongs
35//! to.
36//!
37//! Every format has 2 layers of types and a conversion step between them.
38//! For `net`:
39//!
40//! ```text
41//! .net.xml --(xsd-parser, at build time)--> schema --(schema_mapper)--> domain
42//! ```
43//!
44//! - `schema` (layer 1, generated at build time from the XSDs in `xsd/`)
45//! — an almost literal mirror of the SUMO schema, with no domain
46//! semantics. **Private**: it is an implementation detail, and keeping
47//! it so is what keeps `xsd-parser-types` out of this crate's public
48//! API (see the module's own comment in `lib.rs`). Shared by every
49//! enabled format (see `FEATURE_SCHEMAS` in `build.rs`), so
50//! `schema::NetType` and `schema::RoutesType` both live there when both
51//! features are on.
52//! - `schema_mapper` converts layer 1 into layer 2, interpreting SUMO's
53//! text-encoded positions, shapes, boundaries, and enumerations along the
54//! way. For example, `schema::LaneType::shape` is a raw `String`
55//! (`"0.00,0.00 100.00,0.00"`); `net`'s `schema_mapper` turns it into a
56//! `domain::Shape` (a `Vec<domain::Point>`).
57//! - `domain` (layer 2) — hand-written types independent of SUMO/XSD. This
58//! is what consumers build on: `domain` for `net`, `routes::domain`
59//! for `routes`. The two are unrelated Rust types even where SUMO's
60//! concepts overlap (a route's edges and a network's edges are both just
61//! ids, `domain::EdgeId` vs. `routes::domain::EdgeRef`) — `routes` doesn't
62//! require `net` to be enabled, so it can't borrow its types.
63//!
64//! `read_network` runs the whole pipeline for `net`, so consumers only ever
65//! have to deal with layer 2 (see its own doc comment in `net/reader.rs`
66//! for a runnable example) — marked `ignore` here rather than `no_run`
67//! because this doc comment has no `#[cfg(feature = "net")]` to gate it
68//! (crate-level `//!` docs can't be attached to a single item), so it has
69//! to compile under every feature combination, including `routes` alone:
70//!
71//! ```ignore
72//! let network = sumo_types::read_network(std::path::Path::new("city.net.xml"))?;
73//! println!("{} edges", network.edges.len());
74//! # Ok::<(), sumo_types::Error>(())
75//! ```
76
77#[cfg(not(any(feature = "net", feature = "routes", feature = "additional")))]
78compile_error!(
79 "sumo-types needs at least one format feature enabled: `net` (reads .net.xml), \
80 `routes` (reads .rou.xml) or `additional` (reads .add.xml). With none of them \
81 the crate has no schema to generate and no API to offer."
82);
83
84/// Type-checks the examples in `README.md`, so they can't drift from the
85/// real API. Only exists while running doctests, and only when every format
86/// the README demonstrates is available — the file is one document, so it
87/// can't be `#[cfg]`-gated feature by feature.
88#[cfg(all(doctest, feature = "net", feature = "routes", feature = "additional"))]
89#[doc = include_str!("../README.md")]
90pub struct ReadmeExamples;
91
92/// Types generated from the active features' XSDs by `build.rs` (see
93/// `FEATURE_SCHEMAS` there). Layer 1: an (almost) literal mirror of the
94/// schema, with no domain semantics.
95///
96/// **Private on purpose.** The generated types implement `xsd-parser-types`'
97/// traits (`WithDeserializer`, `DeserializeBytes`, ...) and name its `Error`
98/// in 173 impls, so making this module `pub` would put `xsd-parser-types`
99/// into this crate's public API. Traits are identified by their defining
100/// crate *version*: `WithDeserializer` from 0.2 and from 0.3 are different
101/// traits, so bumping that dependency would break every consumer who built
102/// on layer 1 — a breaking release forced by a dependency bump that has
103/// nothing to do with this crate's own API. Layer 2 (`domain`) doesn't
104/// mention `xsd-parser-types` anywhere, so keeping this module private is
105/// what lets the dependency stay an implementation detail.
106///
107/// Contrast [`uom`], which *is* re-exported: `domain::Lane::length` is a
108/// `Length`, so that one is public API by design rather than by accident.
109///
110/// Enum variants preserve the exact case of the XSD value (see the custom
111/// `Naming` in `build.rs`) so information like `state="M"` vs. `state="m"`
112/// isn't lost; that's why `non_camel_case_types` is disabled for the whole
113/// module.
114///
115/// `clippy::all` is disabled here for the same reason: xsd-parser's output
116/// isn't hand-written, so its style lints (redundant field names, collapsible
117/// `if`s, ...) are noise nobody can act on — and drowned out the handful of
118/// real ones in the rest of the crate.
119///
120/// `dead_code` is disabled too: every enabled format's *entire* schema is
121/// generated here (see `FEATURE_SCHEMAS` in `build.rs`), but each format's
122/// `domain`/`schema_mapper` only maps the subset it actually models — e.g.
123/// `routes` doesn't map `vTypeType`'s car-following-model choice, so
124/// xsd-parser's generated deserializer state for that choice is never
125/// constructed. That's expected, not a bug to silence one warning at a time.
126#[allow(
127 non_camel_case_types,
128 unused_variables,
129 unused_mut,
130 dead_code,
131 clippy::all
132)]
133mod schema {
134 include!(concat!(env!("OUT_DIR"), "/generated_schema.rs"));
135}
136
137/// This crate's own error type, shared by every format. Unconditional (not
138/// gated on a format feature) so that [`Error`]'s shape doesn't shift with
139/// the consumer's feature selection.
140mod error;
141pub use error::{Error, Result};
142
143/// XML plumbing shared by every format's reader: checking that a document's
144/// root element is the one that format expects, which xsd-parser's generated
145/// deserializers don't do. Only needed when there is a reader to plumb.
146#[cfg(any(feature = "net", feature = "routes", feature = "additional"))]
147mod xml;
148
149/// SUMO primitives shared by every format's `schema_mapper` — how SUMO
150/// spells a boolean, encodes a number, writes a list of ids — plus the one
151/// domain type that is genuinely common to all of them, [`sumo::Point`],
152/// re-exported at each format's own path.
153///
154/// This module is what makes that sharing possible at all: it is compiled
155/// whenever *any* format feature is on, so unlike `net`'s or `additional`'s
156/// own modules it can hold something both must be able to name.
157///
158/// `dead_code` is allowed for the whole module because which helpers are
159/// live depends on the active feature set: `split_ids_opt` is only reached
160/// from `net`, the boolean table only from `net` and `additional`, and with
161/// `routes` alone most of the module is unused. Gating each helper on the
162/// features that happen to call it today would be noise that has to be
163/// re-derived every time a mapper changes.
164#[cfg(any(feature = "net", feature = "routes", feature = "additional"))]
165#[allow(dead_code)]
166mod sumo;
167
168/// `net` format's own files (`domain`, `reader`, `schema_mapper`); kept
169/// private and re-exported below so its public path (`sumo_types::domain`,
170/// `sumo_types::read_network`) doesn't have to match where the files live.
171/// See the crate docs for why `routes` isn't re-exported the same way.
172///
173/// `#[doc(hidden)]`: without it, rustdoc still generates full doc pages at
174/// `net`'s own (unreachable — `net` isn't `pub`) path, e.g.
175/// `sumo_types::net::domain::Network`, duplicating `sumo_types::domain::Network`
176/// with a URL that doesn't actually compile if used. Hiding the module
177/// stops that; the `pub use` below still inlines everything at the real
178/// public path.
179#[cfg(feature = "net")]
180#[doc(hidden)]
181mod net;
182#[cfg(feature = "net")]
183pub use net::domain;
184#[cfg(feature = "net")]
185pub use net::domain::Network;
186#[cfg(feature = "net")]
187pub use net::reader;
188#[cfg(feature = "net")]
189pub use net::reader::{read_network, read_network_from};
190#[cfg(feature = "net")]
191pub use net::schema_mapper;
192#[cfg(all(feature = "net", feature = "write"))]
193pub use net::schema_writer;
194#[cfg(all(feature = "net", feature = "write"))]
195pub use net::writer;
196#[cfg(all(feature = "net", feature = "write"))]
197pub use net::writer::{write_network, write_network_to};
198
199/// `routes` format: reads `.rou.xml`. Namespaced here (rather than
200/// re-exported at the crate root like `net`) — see the crate docs.
201#[cfg(feature = "routes")]
202pub mod routes;
203
204/// `additional` format: reads `.add.xml`, of which only the E1/E2/E3
205/// detector definitions are modelled. Namespaced for the same reason as
206/// `routes` — see the crate docs.
207#[cfg(feature = "additional")]
208pub mod additional;
209
210/// Re-exported because it appears in this crate's public API
211/// (`domain::Lane::length`, `domain::Lane::speed`, ... — code spans rather
212/// than links, because `domain` only exists under the `net` feature while
213/// this re-export is unconditional): consumers must
214/// build their `Length`/`Velocity` values with the same `uom` version this
215/// crate was compiled against, and going through `sumo_types::uom` guarantees
216/// that without them having to track the version themselves.
217pub use uom;
218
219// `xsd-parser-types` is deliberately *not* re-exported, unlike `uom`. It
220// appears nowhere in this crate's public API — only inside the private
221// `schema` module and the two readers' bodies — so re-exporting it would
222// hand back exactly the semver lock that keeping `schema` private buys:
223// a `pub use` of a dependency makes bumping that dependency a breaking
224// change for anyone who reached it through this crate.
225//
226// `thiserror` is in the same position for the same reason, and is why this
227// crate returns its own [`Error`] rather than an erased `anyhow::Error`:
228// `thiserror` is a `derive`-only dependency that appears in no signature,
229// whereas `anyhow::Error` in every `read_*`/`write_*` return type would
230// have made `anyhow` part of this crate's API — the exact leak the
231// paragraph above exists to prevent — while giving consumers nothing to
232// match on. See `src/error.rs`.