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
185impl Error {
186 /// Names the file this error came from, for the variants that have
187 /// somewhere to put it ([`Self::Parse`] and [`Self::Write`]).
188 ///
189 /// Lets `xml::read_document_at`/`write_document_at` add the path on the
190 /// way out without every layer below them having to thread an
191 /// `Option<&Path>` through just in case. A no-op for the rest: the
192 /// value-level variants ([`Self::NonFiniteNumber`], ...) deliberately
193 /// don't carry a path — they already name the offending attribute *and*
194 /// its value, and a caller who passed the path to `read_network` in the
195 /// first place knows which file it was.
196 pub(crate) fn with_path(mut self, path: &std::path::Path) -> Self {
197 if let Self::Parse { path: slot, .. } | Self::Write { path: slot, .. } = &mut self {
198 *slot = Some(path.to_path_buf());
199 }
200
201 self
202 }
203}
204
205/// Renders `" in <path>"` for the variants that may or may not have a file
206/// to name, and nothing when they don't.
207///
208/// A [`Display`](std::fmt::Display) adapter rather than two near-identical
209/// variants per case (one with a path, one without): what went wrong is the
210/// same either way, and whether this crate opened the file or the caller
211/// handed over a sink isn't a different kind of failure.
212struct OptionalPath<'a>(&'a Option<PathBuf>);
213
214impl std::fmt::Display for OptionalPath<'_> {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self.0 {
217 Some(path) => write!(f, " in {}", path.display()),
218 None => Ok(()),
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn names_the_file_only_when_there_is_one() {
229 let with_path = Error::Parse {
230 what: "SUMO network",
231 path: Some(PathBuf::from("city.net.xml")),
232 source: "boom".into(),
233 };
234 assert_eq!(
235 with_path.to_string(),
236 "failed to parse SUMO network in city.net.xml"
237 );
238
239 let without_path = Error::Parse {
240 what: "SUMO network",
241 path: None,
242 source: "boom".into(),
243 };
244 assert_eq!(without_path.to_string(), "failed to parse SUMO network");
245 }
246
247 #[test]
248 fn keeps_the_underlying_error_reachable_as_a_source() {
249 use std::error::Error as _;
250
251 let error = Error::Parse {
252 what: "SUMO network",
253 path: None,
254 source: "the real detail".into(),
255 };
256 assert_eq!(error.source().unwrap().to_string(), "the real detail");
257 }
258}