Skip to main content

ifc_xml/
error.rs

1//! Why an ifcXML operation failed.
2
3use std::fmt;
4use thiserror::Error;
5
6/// Stable, codec-specific location of an ifcXML parse failure.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct XmlPath(String);
9
10impl XmlPath {
11    pub(crate) fn new(path: String) -> Self {
12        Self(path)
13    }
14
15    /// The XPath-like location string.
16    #[must_use]
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20}
21
22impl fmt::Display for XmlPath {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        formatter.write_str(&self.0)
25    }
26}
27
28/// Failures specific to reading or writing ifcXML.
29#[derive(Debug, Clone, Error)]
30#[non_exhaustive]
31pub enum XmlError {
32    /// The document is not well-formed XML.
33    #[error("malformed XML: {0}")]
34    Malformed(String),
35    /// An `id` attribute was not in the expected `i<number>` form.
36    #[error("unparseable entity id {0:?}")]
37    BadId(String),
38    /// A typed scalar did not contain a value of its declared kind.
39    #[error("invalid {kind} scalar {value:?}")]
40    InvalidScalar {
41        /// Declared scalar kind.
42        kind: String,
43        /// Invalid lexical value.
44        value: String,
45    },
46    /// A non-empty explicit `kind` is not part of this lossless dialect.
47    #[error("unknown value kind {0:?}")]
48    UnknownKind(String),
49    /// Two attributes or child elements of one entity name the same slot.
50    #[error("`{name}` names slot {slot}, which is already set")]
51    DuplicateSlot {
52        /// The second name resolving to the slot.
53        name: String,
54        /// The zero-based positional slot.
55        slot: usize,
56    },
57    /// A positional name skips slots that no schema declares as omittable.
58    #[error("`{name}` names slot {slot}, but slot {missing} is absent")]
59    MissingSlot {
60        /// The name that skipped ahead.
61        name: String,
62        /// The zero-based slot it names.
63        slot: usize,
64        /// The first slot left without a value.
65        missing: usize,
66    },
67    /// An element resolved outside the selected release namespace.
68    #[error("element `{element}` has namespace {found:?}; expected `{expected}`")]
69    Namespace {
70        /// Element local name.
71        element: String,
72        /// Required namespace URI.
73        expected: &'static str,
74        /// Resolved namespace URI, if any.
75        found: Option<String>,
76    },
77    /// Root schema metadata disagrees with the selected release profile.
78    #[error("ifcXML profile expects schema `{expected}`, found {found:?}")]
79    Profile {
80        /// Required schema token.
81        expected: &'static str,
82        /// Declared root schema token, if any.
83        found: Option<String>,
84    },
85    /// A strict document did not have the required root element.
86    #[error("strict ifcXML requires root `ifcXML`, found {found:?}")]
87    Root {
88        /// First element local name, if any.
89        found: Option<String>,
90    },
91    /// A parsing error with its entity/value location retained.
92    #[error("{path}: {source}")]
93    At {
94        /// XPath-like parser location.
95        path: XmlPath,
96        /// Underlying typed parse error.
97        #[source]
98        source: Box<XmlError>,
99    },
100    /// Writing to the output buffer failed.
101    #[error("write failed: {0}")]
102    Write(String),
103    /// An element names an entity type the schema does not declare.
104    #[error("`{name}` is not an entity the schema declares")]
105    UnknownEntity {
106        /// The element or type name as written.
107        name: String,
108    },
109    /// An abstract entity was instantiated.
110    #[error("`{name}` is abstract and cannot be instantiated")]
111    AbstractEntity {
112        /// The declared entity name.
113        name: String,
114    },
115    /// An XML attribute or child element that no attribute of the entity
116    /// declares. Never placed into a free slot.
117    #[error(
118        "{entity} has no attribute `{attribute}` (on element `{element}`); \
119         refusing to guess its slot"
120    )]
121    UnknownAttribute {
122        /// The entity type, as the schema declares it.
123        entity: String,
124        /// The XML element carrying it: the entity's own element, which is
125        /// an attribute name when the entity is defined in place.
126        element: String,
127        /// The XML attribute or child element name.
128        attribute: String,
129    },
130    /// A value whose kind the declared type does not admit: an integer in a
131    /// label, a reference where a measure is declared, an entity or typed
132    /// value outside its SELECT.
133    #[error("declared {declared}, found {found}")]
134    TypeMismatch {
135        /// The declared type, in EXPRESS terms.
136        declared: String,
137        /// What the document holds, in words.
138        found: String,
139    },
140    /// An attribute written in a form its declared type does not take in
141    /// this layout: a simple value as a child element, or an entity
142    /// reference as an XML attribute.
143    #[error("attribute `{attribute}` must be {expected}, found {found}")]
144    WrongForm {
145        /// The attribute name.
146        attribute: String,
147        /// The form the layout requires.
148        expected: &'static str,
149        /// The form the document used.
150        found: &'static str,
151    },
152    /// A reference to an id no element of the document defines.
153    #[error("reference to undefined id {id:?}")]
154    UnresolvedReference {
155        /// The referenced id as written.
156        id: String,
157    },
158    /// Two elements define the same id.
159    #[error("id {id:?} is defined twice")]
160    DuplicateId {
161        /// The repeated id.
162        id: String,
163    },
164    /// An inverse attribute implies a value its relationship contradicts.
165    #[error(
166        "inverse `{inverse}` places this entity in `{relationship}.{attribute}`, \
167         which {detail}"
168    )]
169    InverseConflict {
170        /// The inverse attribute's name.
171        inverse: String,
172        /// The relationship entity's type.
173        relationship: String,
174        /// The relationship's explicit attribute the inverse inverts.
175        attribute: String,
176        /// Why the implied value cannot be placed.
177        detail: String,
178    },
179    /// The schema given does not describe the selected release profile.
180    #[error("ifcXML profile {profile} needs the {profile} schema, given `{schema}`")]
181    SchemaMismatch {
182        /// The profile's schema token.
183        profile: &'static str,
184        /// The given schema's name.
185        schema: String,
186    },
187    /// Valid content this reader or writer does not implement. Refused,
188    /// never approximated.
189    #[error("unsupported: {construct}")]
190    Unsupported {
191        /// What was met, in words.
192        construct: String,
193    },
194}
195
196impl XmlError {
197    /// Location retained by the reader, when the failure occurred in document content.
198    #[must_use]
199    pub fn path(&self) -> Option<&XmlPath> {
200        match self {
201            Self::At { path, .. } => Some(path),
202            _ => None,
203        }
204    }
205
206    /// Innermost codec error, without discarding the inspectable path.
207    #[must_use]
208    pub fn root_cause(&self) -> &Self {
209        match self {
210            Self::At { source, .. } => source.root_cause(),
211            error => error,
212        }
213    }
214
215    pub(crate) fn at(self, path: String) -> Self {
216        match self {
217            Self::At { .. } => self,
218            source => Self::At {
219                path: XmlPath::new(path),
220                source: Box::new(source),
221            },
222        }
223    }
224}
225
226impl From<quick_xml::Error> for XmlError {
227    fn from(error: quick_xml::Error) -> Self {
228        Self::Malformed(error.to_string())
229    }
230}