openbim_ifcx_binding_core/
error.rs

1//! Errors raised by every binding.
2//!
3//! Each error carries a stable machine-readable [`BindingError::code`] and a
4//! human message. The codes are shared by all hosts, so a JavaScript
5//! `err.code` and a Python `err.code` name the same failure the same way.
6//!
7//! Every code is therefore part of the public contract of both bindings: a
8//! code may be added, never renamed or reused for a different failure. The
9//! `tests` module below pins the released set.
10
11use std::fmt;
12
13/// Why a binding call failed.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum BindingError {
16    /// The input is not an IFCX file: invalid JSON, or JSON that does not
17    /// have the IFCX file shape.
18    Read(String),
19    /// The file could not be written as JSON.
20    Write(String),
21    /// The imports of a layer could not be resolved: a missing layer, an
22    /// import cycle, an unreadable imported layer, or an `integrity`
23    /// mismatch.
24    Layer(String),
25    /// The layers do not compose: a reference cycle, or a reference to a
26    /// node that no layer defines.
27    Compose(String),
28    /// A host argument was not what the call expects, such as an empty layer
29    /// list or a value of the wrong type.
30    InvalidArgument(String),
31    /// The render scene could not be written as GLB, for example because
32    /// the file would exceed 4 GiB.
33    Glb(String),
34}
35
36impl BindingError {
37    /// Stable code for programmatic handling.
38    pub const fn code(&self) -> &'static str {
39        match self {
40            Self::Read(_) => "read",
41            Self::Write(_) => "write",
42            Self::Layer(_) => "layer",
43            Self::Compose(_) => "compose",
44            Self::InvalidArgument(_) => "invalid-argument",
45            Self::Glb(_) => "glb",
46        }
47    }
48}
49
50impl fmt::Display for BindingError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Read(detail) | Self::Layer(detail) | Self::Compose(detail) => f.write_str(detail),
54            Self::Write(detail) => write!(f, "cannot write IFCX: {detail}"),
55            Self::InvalidArgument(detail) => write!(f, "invalid argument: {detail}"),
56            Self::Glb(detail) => write!(f, "cannot write GLB: {detail}"),
57        }
58    }
59}
60
61impl std::error::Error for BindingError {}
62
63#[cfg(test)]
64mod tests {
65    use std::collections::BTreeSet;
66
67    use super::BindingError;
68
69    /// Released codes in declaration order. Append a new code; never edit or
70    /// remove one, because hosts match on these strings.
71    const RELEASED_CODES: &[&str] = &[
72        "read",
73        "write",
74        "layer",
75        "compose",
76        "invalid-argument",
77        "glb",
78    ];
79
80    /// One value of every variant, in declaration order.
81    fn one_of_each() -> Vec<BindingError> {
82        let all = vec![
83            BindingError::Read(String::new()),
84            BindingError::Write(String::new()),
85            BindingError::Layer(String::new()),
86            BindingError::Compose(String::new()),
87            BindingError::InvalidArgument(String::new()),
88            BindingError::Glb(String::new()),
89        ];
90        // Exhaustive on purpose: a new variant does not compile until it is
91        // listed above, so its code cannot escape the snapshot.
92        for error in &all {
93            match error {
94                BindingError::Read(_)
95                | BindingError::Write(_)
96                | BindingError::Layer(_)
97                | BindingError::Compose(_)
98                | BindingError::InvalidArgument(_)
99                | BindingError::Glb(_) => {}
100            }
101        }
102        all
103    }
104
105    #[test]
106    fn released_codes_are_never_renamed_or_reused() {
107        let codes: Vec<&str> = one_of_each().iter().map(BindingError::code).collect();
108        assert!(
109            codes.len() >= RELEASED_CODES.len(),
110            "a BindingError variant was removed: {codes:?}"
111        );
112        assert_eq!(
113            &codes[..RELEASED_CODES.len()],
114            RELEASED_CODES,
115            "a released BindingError code changed; add a new code instead"
116        );
117        let unique: BTreeSet<&str> = codes.iter().copied().collect();
118        assert_eq!(
119            unique.len(),
120            codes.len(),
121            "two variants share a code: {codes:?}"
122        );
123    }
124}