Skip to main content

asbuilt_core/
error.rs

1//! Errors that name the offending input, so a message reads without a
2//! rerun.
3
4use std::path::PathBuf;
5
6/// Anything the core, a front-end, or the config can get wrong.
7#[derive(Debug, thiserror::Error)]
8#[non_exhaustive]
9pub enum Error {
10    #[error("reading {path}: {source}")]
11    Io {
12        path: PathBuf,
13        #[source]
14        source: std::io::Error,
15    },
16
17    /// `asbuilt.toml` did not parse or had a key the schema rejects.
18    #[error("{path}: {source}")]
19    Config {
20        path: PathBuf,
21        #[source]
22        source: toml::de::Error,
23    },
24
25    /// An `[[externals.relations]]` `from` that names no generated
26    /// element and no sibling external. A typo here is an error, not a
27    /// diagram that quietly omits an edge.
28    #[error(
29        "externals.relations: \"{from}\" (under external \"{external}\") names no element in the surveyed model"
30    )]
31    UnknownRelationSource { external: String, from: String },
32
33    /// An `[[externals]]` `id` that is not a LikeC4 identifier as written.
34    #[error(
35        "externals: id \"{id}\" is not an identifier (letters, digits and _, not starting with a digit, not a keyword)"
36    )]
37    InvalidExternalId { id: String },
38
39    /// An `[[externals]]` `kind` that is one the survey generates;
40    /// `surveyed` lists them.
41    #[error(
42        "externals: \"{id}\" declares kind \"{kind}\", which the survey generates ({surveyed}); give an external its own kind"
43    )]
44    ExternalKindIsSurveyed {
45        id: String,
46        kind: String,
47        surveyed: String,
48    },
49
50    /// Two model ids that become the same LikeC4 identifier once `-`
51    /// maps to `_` and a leading digit is prefixed.
52    #[error("element ids \"{first}\" and \"{second}\" both become the LikeC4 id \"{id}\"")]
53    DuplicateId {
54        id: String,
55        first: String,
56        second: String,
57    },
58
59    /// A `[theme]` key naming an element kind the surveyed model does not
60    /// have; a typo would otherwise style nothing, silently.
61    #[error("theme: \"{kind}\" is not an element kind of the surveyed model (have {have})")]
62    UnknownThemeKind { kind: String, have: String },
63
64    /// A top-level `asbuilt.toml` entry that is neither one of asbuilt's
65    /// own tables nor a front-end's: `[rsut]`, `[[external]]` for
66    /// `[[externals]]`, or a key such as `title = ...` that belongs under
67    /// `[docs]`. It would otherwise be ignored, silently. `entry` is
68    /// written as it appears in the file.
69    #[error("{path}: {entry} is not a top-level entry asbuilt reads (those are {known})")]
70    UnknownTopLevel {
71        path: PathBuf,
72        entry: String,
73        known: String,
74    },
75
76    /// `asbuilt = "X.Y.Z"` pins a release other than the one running. A
77    /// survey is only meaningful against the release that wrote the
78    /// model, so every command stops here rather than report the other
79    /// release's output as drift.
80    #[error(
81        "{path} pins asbuilt {pinned}, but this is asbuilt {running}: install the pinned release with `cargo install asbuilt --version {pinned} --locked`"
82    )]
83    PinMismatch {
84        path: PathBuf,
85        pinned: String,
86        running: String,
87    },
88
89    /// No front-end recognized the root.
90    #[error("no supported stack detected at {root}")]
91    NoFrontend { root: PathBuf },
92
93    /// A front-end failed; the source says which file or module.
94    #[error("{frontend} front-end: {source}")]
95    Frontend {
96        frontend: String,
97        #[source]
98        source: Box<dyn std::error::Error + Send + Sync + 'static>,
99    },
100}
101
102pub type Result<T> = std::result::Result<T, Error>;
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn an_unknown_relation_source_names_the_external_and_the_from() {
110        let msg = Error::UnknownRelationSource {
111            external: "node_driver".into(),
112            from: "playwright_rs.nope".into(),
113        }
114        .to_string();
115        assert!(msg.contains("node_driver"), "{msg}");
116        assert!(msg.contains("playwright_rs.nope"), "{msg}");
117    }
118
119    #[test]
120    fn a_pin_mismatch_gives_the_install_command_for_the_pinned_release() {
121        let msg = Error::PinMismatch {
122            path: "asbuilt.toml".into(),
123            pinned: "0.3.0".into(),
124            running: "0.2.0".into(),
125        }
126        .to_string();
127        assert!(
128            msg.contains("`cargo install asbuilt --version 0.3.0 --locked`"),
129            "{msg}"
130        );
131    }
132
133    #[test]
134    fn a_duplicate_id_names_both_originals_and_the_collision() {
135        let msg = Error::DuplicateId {
136            id: "foo_bar".into(),
137            first: "foo-bar".into(),
138            second: "foo_bar".into(),
139        }
140        .to_string();
141        assert!(
142            msg.contains("\"foo-bar\"") && msg.contains("\"foo_bar\""),
143            "{msg}"
144        );
145    }
146}