Skip to main content

energyml_storage/
error.rs

1//! What can go wrong, and what merely looks odd.
2//!
3//! Two distinct notions live here and must not be confused. An [`Error`] stops
4//! one operation. A [`Diagnostic`] does not stop anything: it records that the
5//! store was read anyway, despite something the specification does not allow.
6//!
7//! Files in the wild are not conformant - the reference package published with
8//! fesapi declares one object twice in `[Content_Types].xml`, with a content
9//! type that belongs to a different part. A reader that refuses such a package
10//! is useless; a reader that swallows the problem is worse. So: read it, and
11//! say so.
12
13use std::fmt;
14
15/// Result of a storage operation.
16pub type Result<T> = std::result::Result<T, Error>;
17
18/// Something that stopped an operation.
19#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum Error {
22    /// No object with this URI in the store.
23    #[error("no object at `{0}`")]
24    NotFound(String),
25
26    /// A short URI that names more than one object: the same uuid and type
27    /// held under several `objectVersion`s. Identifier Specification v5.0
28    /// ยง3.4.2.1 (p. 15) leaves "the most recent" to the server and ETP 1.2 says
29    /// nothing of how to tell; this store does not guess, it names the
30    /// candidates - each spelled with its version, so any one of them resolves.
31    #[error("`{uri}` names {} objects; name the version: {}", candidates.len(), candidates.join(", "))]
32    Ambiguous {
33        /// The URI as given.
34        uri: String,
35        /// Every object it could mean, each by its full URI.
36        candidates: Vec<String>,
37    },
38
39    /// The URI could not be parsed, or names something this store cannot hold.
40    #[error("`{uri}` is not usable here: {reason}")]
41    BadUri {
42        /// The URI as given.
43        uri: String,
44        /// Why it was rejected.
45        reason: String,
46    },
47
48    /// A part of something asked for that is not inside it: a window of an
49    /// array starting or reaching past an axis, a start or count whose rank is
50    /// not the array's. The request is wrong, not the data.
51    #[error("`{location}`: {reason}")]
52    OutOfBounds {
53        /// What was asked into, by its address (an array: `uri` plus path).
54        location: String,
55        /// The figures: which axis, which start, which length.
56        reason: String,
57    },
58
59    /// A request larger than the bound its caller set: answered by naming the
60    /// size, never by returning a part of it as if it were the whole.
61    #[error("`{location}`: {reason}")]
62    TooLarge {
63        /// What was asked for, by its address.
64        location: String,
65        /// The size found and the bound, so the caller knows what to narrow.
66        reason: String,
67    },
68
69    /// The content type names a type no registered crate declares.
70    #[error("unknown type `{0}` - is the generated crate registered?")]
71    UnknownType(String),
72
73    /// The package itself is malformed beyond what tolerance can absorb.
74    #[error("package error: {0}")]
75    Package(String),
76
77    /// An operation this implementation does not offer.
78    ///
79    /// Not a failure of the data: a capability the transport does not have.
80    #[error("{operation} is not supported by {store}")]
81    Unsupported {
82        /// What was attempted.
83        operation: &'static str,
84        /// Which implementation refused.
85        store: &'static str,
86    },
87
88    /// A request by relationship kind met an edge whose kind nobody states.
89    ///
90    /// ETP 1.2 section 8.1.1.1.2 (p. 157) leaves the kind of a relationship to
91    /// the ML of the object holding it, and the guide of an ML that has none
92    /// says nothing: the edge has no kind, and the request that needs one is
93    /// refused **when it meets that edge**, not before
94    /// ([`crate::context::discover`]).
95    #[error("`{source_type}`: the kind of its edges is not stated: {why}")]
96    UnstatedKind {
97        /// The qualified type of the object holding the reference
98        /// (`prodml23.FlowTestActivity`), or the URI when it has none.
99        source_type: String,
100        /// Why no rule applies ([`crate::kinds::KindBasis::Unknown`]).
101        why: &'static str,
102    },
103
104    /// A transaction was misused: a second one opened while one is, or one
105    /// ended that is not the open one (ETP protocol 18).
106    #[error("{store}: {reason}")]
107    Transaction {
108        /// Which implementation refused.
109        store: &'static str,
110        /// What was wrong.
111        reason: String,
112    },
113
114    /// An external data file could not be reached or read.
115    #[error("external data `{location}`: {reason}")]
116    ExternalData {
117        /// File the array lives in, as named by the object or its relationship.
118        location: String,
119        /// Why it could not be read.
120        reason: ExternalDataReason,
121    },
122
123    /// Reading or writing the underlying bytes failed.
124    #[error("I/O error: {0}")]
125    Io(#[from] std::io::Error),
126
127    /// A remote store failed as a whole - the session fell, the server
128    /// refused the request. What the server said of **one** object or array
129    /// comes back as that object's own error instead.
130    #[error("{store}: {reason}")]
131    Remote {
132        /// Which kind of store, as [`crate::storage::EnergymlStorage::store_kind`] says.
133        store: &'static str,
134        /// What went wrong, in the server's words when it gave some.
135        reason: String,
136    },
137
138    /// The XML of a part could not be read.
139    #[error("XML error: {0}")]
140    Xml(String),
141
142    /// Anything raised by `energyml-core` while reading or writing an object.
143    #[error(transparent)]
144    Core(#[from] energyml_core::error::Error),
145}
146
147/// Why an array could not be read.
148#[derive(Debug, Clone, PartialEq, Eq)]
149#[non_exhaustive]
150pub enum ExternalDataReason {
151    /// The support for this format is not compiled in.
152    ///
153    /// A capability that is missing, **not** data that is absent - the caller
154    /// must be able to tell the two apart, which is why this is not a plain
155    /// "not found". Enable the matching feature, or supply the file yourself.
156    FeatureNotEnabled {
157        /// The Cargo feature that would have handled it, e.g. `hdf5`.
158        feature: &'static str,
159        /// MIME type the object declared, when it declared one.
160        mime: Option<String>,
161    },
162    /// The file is named by the package but nothing was found under that name.
163    FileMissing,
164    /// The file was found but the dataset path inside it was not.
165    PathMissing(String),
166    /// The file identifies itself with a different uuid than the package says.
167    ///
168    /// The EPC specification requires an HDF5 file to carry, at its root, a
169    /// `uuid` attribute equal to that of its `EpcExternalPartReference`. A
170    /// mismatch means this is somebody else's file.
171    UuidMismatch {
172        /// What the package expected.
173        expected: String,
174        /// What the file claims.
175        found: String,
176    },
177    /// The backend failed for a reason of its own.
178    Backend(String),
179    /// The shape and the values of an array being written disagree.
180    ///
181    /// Checked before the backend is asked, because a shape that promises more
182    /// numbers than there are **writes without error** in some formats and
183    /// reads back with zeros at the end - plausible values that nothing flags.
184    /// Both counts are named: it is their difference that says what to fix.
185    ShapeDisagrees {
186        /// How many the shape promises.
187        promised: i64,
188        /// How many the block holds.
189        held: usize,
190    },
191}
192
193impl fmt::Display for ExternalDataReason {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            Self::FeatureNotEnabled { feature, mime } => match mime {
197                Some(mime) => write!(f, "no support for `{mime}` (build with the `{feature}` feature)"),
198                None => write!(f, "no support compiled in (build with the `{feature}` feature)"),
199            },
200            Self::FileMissing => write!(f, "file not found"),
201            Self::PathMissing(path) => write!(f, "no dataset at `{path}` in the file"),
202            Self::UuidMismatch { expected, found } => {
203                write!(
204                    f,
205                    "the file carries uuid `{found}`, the package expects `{expected}`"
206                )
207            }
208            Self::Backend(reason) => write!(f, "{reason}"),
209            Self::ShapeDisagrees { promised, held } => write!(
210                f,
211                "the shape promises {promised} value(s) and the block holds {held}"
212            ),
213        }
214    }
215}
216
217/// How much a diagnostic matters.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
219pub enum Severity {
220    /// The package departs from the specification; the reader coped.
221    Warning,
222    /// Worth knowing, no departure. A fallback was taken, a default applied.
223    Note,
224}
225
226/// Something noticed while reading, which did not stop anything.
227///
228/// Every departure from the specification produces one. The caller decides
229/// whether to ignore them, show them, or treat them as fatal - but they are
230/// never dropped in silence, which is the same rule the code generator follows
231/// for the schemas it cannot map.
232#[derive(Debug, Clone)]
233pub struct Diagnostic {
234    /// How much it matters.
235    pub severity: Severity,
236    /// Which part of the package it concerns, empty for the package itself.
237    pub part: String,
238    /// What was noticed, in one sentence.
239    pub message: String,
240}
241
242impl Diagnostic {
243    /// A departure from the specification.
244    pub fn warning(part: impl Into<String>, message: impl Into<String>) -> Self {
245        Self {
246            severity: Severity::Warning,
247            part: part.into(),
248            message: message.into(),
249        }
250    }
251
252    /// An object whose uuid is the nil UUID: the one rule, spelled once, for
253    /// a part (`path` is `None`) and for an object held by value (`path` is
254    /// its path in the container). Detected and said, never refused.
255    pub(crate) fn nil_uuid(part: impl Into<String>, path: Option<&str>) -> Self {
256        let prefix = path.map(|p| format!("`{p}`: ")).unwrap_or_default();
257        Self::warning(
258            part,
259            format!(
260                "{prefix}uuid `00000000-0000-0000-0000-000000000000` is the nil UUID (RFC 4122 section 4.1.7): \
261                 it identifies no instance, so objects that carry it cannot be told apart \
262                 (Energistics Identifier Specification v5.0 section 2.3.1)"
263            ),
264        )
265    }
266
267    /// A choice worth recording.
268    pub fn note(part: impl Into<String>, message: impl Into<String>) -> Self {
269        Self {
270            severity: Severity::Note,
271            part: part.into(),
272            message: message.into(),
273        }
274    }
275}
276
277impl fmt::Display for Diagnostic {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        let tag = match self.severity {
280            Severity::Warning => "warning",
281            Severity::Note => "note",
282        };
283        if self.part.is_empty() {
284            write!(f, "[{tag}] {}", self.message)
285        } else {
286            write!(f, "[{tag}] {}: {}", self.part, self.message)
287        }
288    }
289}