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}