sumo_types/error.rs
1//! This crate's error type.
2//!
3//! A single typed enum rather than an erased one (`anyhow::Error`, which
4//! earlier versions returned): the same argument the `schema` module's docs
5//! in `lib.rs` make for keeping `xsd-parser-types` out of the public API
6//! applies to an error crate too. `anyhow::Error` in a `pub fn`'s signature
7//! makes `anyhow` part of this crate's API — a major bump there would be a
8//! breaking change here — and it gives consumers nothing to match on, so
9//! "the file was missing" and "a coordinate was `NaN`" are distinguishable
10//! only by string-matching the message. `thiserror` generates the
11//! [`std::error::Error`] plumbing for the enum below without appearing in
12//! any signature at all: it is a `derive`-only dependency, so it can be
13//! bumped freely.
14//!
15//! [`Error`] is [`non_exhaustive`](https://doc.rust-lang.org/reference/attributes/type_system.html)
16//! so that modelling more of SUMO's formats later — every one of which
17//! brings its own malformed-input cases — doesn't force a breaking release
18//! each time.
19
20use std::path::PathBuf;
21
22/// Convenience alias for a [`Result`](std::result::Result) with this
23/// crate's [`Error`].
24pub type Result<T, E = Error> = std::result::Result<T, E>;
25
26/// Anything that can go wrong reading or writing a SUMO file.
27///
28/// The variants split into three groups: I/O against the file itself,
29/// well-formedness of the XML and its root element, and the SUMO-specific
30/// value encodings each format's `schema_mapper` interprets (positions,
31/// shapes, booleans, ...). Only the last group is specific to this crate;
32/// the first two would apply to any XML reader.
33#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum Error {
36 /// The input file could not be opened.
37 #[error("could not open input file: {path}")]
38 Open {
39 path: PathBuf,
40 #[source]
41 source: std::io::Error,
42 },
43
44 /// The output file could not be created.
45 #[error("could not create output file: {path}")]
46 Create {
47 path: PathBuf,
48 #[source]
49 source: std::io::Error,
50 },
51
52 /// Writing to the output failed partway through.
53 ///
54 /// `path` is `None` when writing to a caller-supplied sink
55 /// (`write_network_to` and friends) rather than to a file this crate
56 /// opened itself. Code span, not an intra-doc link: this module is
57 /// compiled under every feature combination, and `write_network_to`
58 /// only exists under `net` + `write` — see the crate docs.
59 #[error("failed to write {what}{}", OptionalPath(path))]
60 Write {
61 what: &'static str,
62 path: Option<PathBuf>,
63 #[source]
64 source: std::io::Error,
65 },
66
67 /// The document isn't well-formed XML, or doesn't fit the format's
68 /// schema.
69 ///
70 /// The underlying error is boxed rather than named: it comes from
71 /// `xsd-parser-types`, which `lib.rs` deliberately keeps out of this
72 /// crate's public API. [`std::error::Error::source`] still reaches it
73 /// for anyone who wants the detail.
74 #[error("failed to parse {what}{}", OptionalPath(path))]
75 Parse {
76 what: &'static str,
77 path: Option<PathBuf>,
78 #[source]
79 source: Box<dyn std::error::Error + Send + Sync>,
80 },
81
82 /// The value could not be serialized to XML. Boxed for the same reason
83 /// as [`Self::Parse`].
84 #[error("failed to serialize {what}")]
85 Serialize {
86 what: &'static str,
87 #[source]
88 source: Box<dyn std::error::Error + Send + Sync>,
89 },
90
91 /// The document is well-formed but rooted at the wrong element — a
92 /// `.rou.xml` handed to `read_network`, say. (Code span rather than a
93 /// link, for the same reason as [`Self::Write`] above.)
94 ///
95 /// Worth its own variant because nothing else catches it: xsd-parser
96 /// generates deserializers for XSD *types*, not elements, and most SUMO
97 /// schemas make their content optional, so an unrelated document
98 /// otherwise parses into an empty, plausible-looking value. See
99 /// `xml::RootRecordingReader`.
100 #[error("not a {what}: expected a <{expected}> root element, found <{found}>")]
101 WrongRoot {
102 what: &'static str,
103 expected: &'static str,
104 found: String,
105 },
106
107 /// The document has no root element at all (it is empty, or contains
108 /// only a declaration and comments).
109 #[error("not a {what}: the document has no root element")]
110 MissingRoot { what: &'static str },
111
112 /// An attribute the format requires in practice was absent. Only raised
113 /// where a real SUMO writer always emits the attribute even though the
114 /// XSD marks it optional — see `net::domain::Location`.
115 #[error("<{element}> is missing {attribute}")]
116 MissingAttribute {
117 element: &'static str,
118 attribute: &'static str,
119 },
120
121 /// A number didn't parse. `context` names the attribute it came from
122 /// (`"junction x coordinate"`, ...).
123 #[error("invalid {context}: {value:?}")]
124 InvalidNumber {
125 context: &'static str,
126 value: String,
127 },
128
129 /// A number parsed but was `NaN` or an infinity, which SUMO's
130 /// `floatType` pattern excludes and which would poison every
131 /// computation downstream — see `sumo::parse_finite`.
132 #[error("non-finite {context}: {value:?}")]
133 NonFiniteNumber {
134 context: &'static str,
135 value: String,
136 },
137
138 /// A position had fewer than the two coordinates SUMO's `positionType`
139 /// requires.
140 #[error("incomplete {context}: {value:?}")]
141 IncompletePosition {
142 context: &'static str,
143 value: String,
144 },
145
146 /// A position had more than the three coordinates SUMO's `positionType`
147 /// allows.
148 #[error("{context} with too many coordinates: {value:?}")]
149 TooManyCoordinates {
150 context: &'static str,
151 value: String,
152 },
153
154 /// SUMO's `boolType` allows `"x"` and `"-"`, the "unspecified" marker
155 /// its configuration options use. Neither has a binary meaning, so they
156 /// are an error rather than a silent `false`.
157 #[error("SUMO boolean value with no binary meaning (\"x\"/\"-\")")]
158 AmbiguousBool,
159
160 /// A `locationType` boundary didn't have exactly the four components
161 /// `"minX,minY,maxX,maxY"` needs.
162 #[error("SUMO boundary must have 4 components: {value:?}")]
163 InvalidBoundary { value: String },
164
165 /// A `colorType` in its numeric form didn't have 3 (`"r,g,b"`) or 4
166 /// (`"r,g,b,a"`) components.
167 #[error("SUMO color must have 3 or 4 components: {value:?}")]
168 InvalidColor { value: String },
169
170 /// A `timeType` in its clock form didn't have 3 (`"H:M:S"`) or 4
171 /// (`"H:M:S:MS"`) components.
172 #[error("SUMO clock time must have 3 or 4 components: {value:?}")]
173 InvalidClockTime { value: String },
174
175 /// Two attributes SUMO treats as alternatives were both set — an
176 /// `e2Detector` with both `lane` and `lanes`, for instance.
177 #[error("{element} sets both `{first}` and `{second}`, which are alternatives")]
178 ConflictingAttributes {
179 element: &'static str,
180 first: &'static str,
181 second: &'static str,
182 },
183
184 /// A `vType`'s `vClass` attribute named a token this crate's `VClass`
185 /// has no variant for. Code span, not an intra-doc link: `VClass` lives
186 /// in the shared `sumo` module, but is only reachable at
187 /// `domain::VClass` under `net` and `routes::domain::VClass` under
188 /// `routes` — this module compiles under every feature combination, so
189 /// a link naming either path would be broken in a build that doesn't
190 /// enable it.
191 ///
192 /// Appended after every existing variant, not inserted where it reads
193 /// best: this enum has no `#[repr]`, so inserting ahead of
194 /// `ConflictingAttributes` silently shifted its discriminant from 16 to
195 /// 18 — a breaking change `cargo semver-checks` catches. New variants
196 /// go at the end.
197 #[error("unknown SUMO vehicle class: {value:?}")]
198 InvalidVehicleClass { value: String },
199
200 /// A detector's `detectPersons` attribute named a token this crate's
201 /// `PersonMode` (`additional::domain::PersonMode`, only reachable under
202 /// `additional` — code span, not a link, for the same reason
203 /// [`Self::InvalidVehicleClass`]'s own docs give) has no variant for.
204 #[error("unknown SUMO detectPersons token: {value:?}")]
205 InvalidPersonMode { value: String },
206}
207
208impl Error {
209 /// Names the file this error came from, for the variants that have
210 /// somewhere to put it ([`Self::Parse`] and [`Self::Write`]).
211 ///
212 /// Lets `xml::read_document_at`/`write_document_at` add the path on the
213 /// way out without every layer below them having to thread an
214 /// `Option<&Path>` through just in case. A no-op for the rest: the
215 /// value-level variants ([`Self::NonFiniteNumber`], ...) deliberately
216 /// don't carry a path — they already name the offending attribute *and*
217 /// its value, and a caller who passed the path to `read_network` in the
218 /// first place knows which file it was.
219 pub(crate) fn with_path(mut self, path: &std::path::Path) -> Self {
220 if let Self::Parse { path: slot, .. } | Self::Write { path: slot, .. } = &mut self {
221 *slot = Some(path.to_path_buf());
222 }
223
224 self
225 }
226}
227
228/// Renders `" in <path>"` for the variants that may or may not have a file
229/// to name, and nothing when they don't.
230///
231/// A [`Display`](std::fmt::Display) adapter rather than two near-identical
232/// variants per case (one with a path, one without): what went wrong is the
233/// same either way, and whether this crate opened the file or the caller
234/// handed over a sink isn't a different kind of failure.
235struct OptionalPath<'a>(&'a Option<PathBuf>);
236
237impl std::fmt::Display for OptionalPath<'_> {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 match self.0 {
240 Some(path) => write!(f, " in {}", path.display()),
241 None => Ok(()),
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn names_the_file_only_when_there_is_one() {
252 let with_path = Error::Parse {
253 what: "SUMO network",
254 path: Some(PathBuf::from("city.net.xml")),
255 source: "boom".into(),
256 };
257 assert_eq!(
258 with_path.to_string(),
259 "failed to parse SUMO network in city.net.xml"
260 );
261
262 let without_path = Error::Parse {
263 what: "SUMO network",
264 path: None,
265 source: "boom".into(),
266 };
267 assert_eq!(without_path.to_string(), "failed to parse SUMO network");
268 }
269
270 #[test]
271 fn keeps_the_underlying_error_reachable_as_a_source() {
272 use std::error::Error as _;
273
274 let error = Error::Parse {
275 what: "SUMO network",
276 path: None,
277 source: "the real detail".into(),
278 };
279 assert_eq!(error.source().unwrap().to_string(), "the real detail");
280 }
281}