Skip to main content

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}