lib_efo/
error.rs

1//! Error types that arise when interacting with the storage layer.
2//!
3//! These are the only errors emitted by the `Storage` abstraction. They are
4//! lightweight, strongly-typed, and can be converted to the top-level
5//! `AppError` via `thiserror`.
6//!
7//! The public items below are all lightweight error types that can be
8//! produced by the storage layer or the epoch handling subsystem.
9
10#![warn(missing_docs, clippy::missing_docs_in_private_items)]
11
12use derive_more::Display;
13use std::{num::ParseIntError, path::PathBuf};
14use uuid::Uuid;
15
16use thiserror::Error;
17
18use crate::{
19    entity::EntityIdentifier,
20    epoch::EpochId,
21    stats::{EntityType, Symbols},
22};
23use miette::Diagnostic;
24
25/// Errors that can be returned by the storage layer.
26///
27/// The variants are intentionally small - each one carries all the
28/// information required to surface a helpful message to the user.
29// TODO(2): Remove or rethink these errors
30#[derive(Debug, Error, Diagnostic)]
31pub enum StorageError {
32    /// Problems with serialisation / deserialisation.
33    #[error("Data error: {0}")]
34    Serialisation(#[from] serde_json::Error),
35
36    /// Database connection failed for the provided reason.
37    #[error("Database connection failed: {0}")]
38    Connection(String),
39
40    /// An error occurred while executing a database query.
41    #[error("Query error: {0}")]
42    Query(String),
43
44    /// The requested record could not be found.
45    #[error("Record not found")]
46    NotFound,
47}
48
49/// All errors that can occur while working with epochs.
50///
51/// The enum is deliberately _small_ - each variant is self-contained and
52/// carries the information you need to surface a friendly message
53/// to the user (and to the test-suite).
54// TODO(2): Split effectively user errors to avoid the usage of advice and such
55#[derive(Error, Debug, Diagnostic)]
56pub enum EpochError {
57    /// Problems with the `.epochconfig` file.
58    #[error("Failed to read epoch configuration")]
59    #[diagnostic(code(EpochError::ConfigRead))]
60    ConfigRead(#[source] std::io::Error),
61
62    /// Failed to write the epoch configuration.
63    #[error("Failed to write epoch configuration")]
64    #[diagnostic(code(EpochError::ConfigWrite))]
65    ConfigWrite(#[source] std::io::Error),
66
67    /// Failed to create the epoch configuration.
68    #[error("Failed to create the epoch configuration")]
69    #[diagnostic(help("Make sure the path is writable"))]
70    #[diagnostic(code(EpochError::ConfigCreation))]
71    ConfigCreation(String, #[source] std::io::Error),
72
73    /// This directory is already an epoch folder and thus cannot be initialized.
74    #[error("This directory is already an epoch folder and thus cannot be initialized")]
75    #[diagnostic(code(EpochError::AlreadyInitialized))]
76    AlreadyInitialized {
77        #[help]
78        /// User given advice
79        advice: Option<String>,
80    },
81
82    /// This directory isn't empty and thus cannot be initialized.
83    #[error("This directory {name} isn't empty and thus cannot be initialized")]
84    #[diagnostic(code(EpochError::FolderNotEmpty))]
85    #[diagnostic(help("Empty the folder contents first"))]
86    FolderNotEmpty {
87        #[source_code]
88        /// Name of the file causing problem
89        name: String,
90        /// Trick used to have a cute line showing the following error message
91        #[label("This file is present and forbid the command to execute properly")]
92        show_label: Option<(usize, usize)>,
93    },
94
95    /// Problems while serialising / deserialising an `EpochInfo`.
96    #[error("Failed to parse `.epochinfo` in epoch {0} ({1}): {2}")]
97    #[diagnostic(code(EpochError::EpochInfoParse))]
98    EpochInfoParse(EpochId, PathBuf, #[source] serde_json::Error),
99
100    /// Failed to write `.epochinfo` in epoch {id} ({path}).
101    #[error("Failed to write `.epochinfo` in epoch {id} ({path})")]
102    #[diagnostic(code(EpochError::EpochInfoWrite))]
103    EpochInfoWrite {
104        /// The id of the epoch
105        id: EpochId,
106        /// The path of the epoch
107        path: PathBuf,
108        /// An optionnal help message
109        #[help]
110        help: Option<String>,
111        /// The source error
112        #[source]
113        src: Option<std::io::Error>,
114    },
115
116    /// The user supplied an epoch id or tag that does not exist.
117    #[error("No epoch found for id/tag `{0}`")]
118    #[diagnostic(code(EpochError::EpochNotFound))]
119    EpochNotFound(String),
120
121    /// The user attempted to resolve an epoch that is ambiguous.
122    #[error("Tag `{0}` resolves to either multiple epochs or invalid epoch")]
123    #[diagnostic(code(EpochError::AmbiguousTag))]
124    AmbiguousTag(String),
125
126    /// The epoch folder for the target id is missing.
127    #[error("Missing directory for epoch {0} at `{1}`")]
128    #[diagnostic(code(EpochError::EpochFolderMissing))]
129    #[diagnostic(help(
130        "Ensure that the epoch was initialized and {0} refers to an existing epoch"
131    ))]
132    EpochFolderMissing(EpochId, PathBuf),
133
134    /// A future epoch exists, forbidding you to compute `next` and overriding them.
135    #[diagnostic(help("Use epoch reset if you want to erase all future epochs first"))]
136    #[error("Cannot compute `next` because future epoch exists")]
137    #[diagnostic(code(EpochError::FutureEpochExist))]
138    FutureEpochExist,
139
140    /// A specific epoch exists, forbidding the further execution of the command.
141    #[diagnostic(help("Use epoch reset or manually delete the epoch {0}"))]
142    #[error("The epoch {0} exists forbidding the execution of the command")]
143    #[diagnostic(code(EpochError::EpochAlreadyExists))]
144    EpochAlreadyExists(EpochId),
145
146    /// Attempt to switch to an epoch that has already been advanced past.
147    #[error("Cannot switch to epoch `{0}` - the world has already advanced beyond it")]
148    #[diagnostic(code(EpochError::SwitchPastEpoch))]
149    SwitchPastEpoch(EpochId),
150
151    /// The supplied epoch id is out of bounds (negative, zero or too large).
152    #[error("Invalid epoch id `{0}` - must be a positive, non-overflowing integer")]
153    #[diagnostic(code(EpochError::InvalidEpochId))]
154    InvalidEpochId(EpochId),
155
156    /// A tag that the user tried to assign already exists.
157    #[error("Tag `{1}` already exists for epoch {0}")]
158    #[diagnostic(code(EpochError::InvalidEpochId))]
159    TagAlreadyExists(EpochId, String),
160
161    /// The parent epoch referenced in an `EpochInfo` cannot be found.
162    #[error("Parent epoch {} referenced by epoch {} does not exist", .0, .1)]
163    #[diagnostic(code(EpochError::ParentEpochMissing))]
164    ParentEpochMissing(EpochId, EpochId),
165
166    /// Generic I/O errors that bubble up from other sub-tasks.
167    #[error("I/O error")]
168    #[diagnostic(code(EpochError::GenericIO))]
169    Io {
170        /// The source error
171        #[source]
172        source: std::io::Error,
173        /// An optionnal help message
174        #[help]
175        help: Option<String>,
176        /// Where the error originated from
177        error_line: u32,
178    },
179
180    /// Serialization / deserialization errors that are not caught above.
181    #[error("Serialization error from {file_name}")]
182    #[diagnostic(code(EpochError::SerdeError))]
183    #[diagnostic(help("You may have updated efo to an incompatible version.\n{version}"))]
184    Serde {
185        /// The source error
186        #[source]
187        source: serde_json::Error,
188        /// The file that was being written
189        file_name: String,
190        /// A version number vs file version number
191        version: String,
192    },
193
194    /// The epoch tree is in an incorrect state.
195    #[error("Invalid epoch tree at {0}")]
196    #[diagnostic(code(EpochError::TreeInvalid))]
197    TreeInvalid(EpochId, String),
198
199    /// The main folder is not an epoch folder.
200    #[error("The given directory ({0:?}) is not an epoch folder")]
201    #[diagnostic(code(EpochError::MainFolderInvalid))]
202    MainFolderInvalid(String),
203
204    /// Couldn't parse an `EpochId`.
205    #[error("The id ({0}) couldn't be converted to a valid EpochId")]
206    #[diagnostic(code(EpochError::ParsingEpochId))]
207    ParsingEpochId(#[from] ParseIntError),
208
209    /// Impossible to load the given epoch
210    #[error("The id ({0}) couldn't be loaded")]
211    #[diagnostic(code(EpochError::LoadingEpochId))]
212    LoadingEpochId(EpochId),
213
214    /// Any other error that didn't fit into the above categories.
215    #[error("An unknown error occurred: {0}")]
216    #[diagnostic(code(EpochError::Unknown))]
217    Unknown(String),
218
219    /// The user tried to parse an acronim that doen't match any stats known to the system.
220    #[error("Unknown stat acronym: {0}")]
221    #[diagnostic(code(EpochError::UnknownStatAcronim))]
222    UnknownStatAcronim(String),
223
224    /// The user gave a stat:identifier pair that was incorrect
225    #[error("Expected a stat and a number (e.g. fod:12 or mil=22), got: {0}")]
226    #[diagnostic(code(EpochError::IllFormedStatQualifier))]
227    IllFormedStatQualifier(String),
228
229    /// User error, meant to be very explicit over what happened and what to do.
230    #[error("An user facing error")]
231    #[diagnostic(code(EpochError::UserError))]
232    UserError(#[help] UserErrors),
233
234    /// An error happened when picking a rng event
235    #[error("An error happened when trying to pick a rng event for {cause}")]
236    #[diagnostic(code(EpochError::RNGPickError))]
237    RNGPickError {
238        /// The root cause originating the error
239        cause: String,
240        #[help]
241        /// Optionnal help
242        help: Option<String>,
243    },
244
245    /// A non-recoverable error while hashing an epoch identifier.
246    ///
247    /// This error should never happen in normal operation. It is surfaced only
248    /// when the hashing routine fails to produce a unique identifier for a
249    /// given epoch index.
250    #[error("Epoch hashing failed for `{0}` (index {1})")]
251    #[diagnostic(code(EpochError::EpochHashingImpossible))]
252    #[diagnostic(help("This is an internal error. Please file a bug report."))]
253    EpochHashingImpossible(&'static str, u32),
254
255    /// The entity with id **`{0}`** could not be found.
256    #[error("Entity of type {1:?} with id {0:?} not found")]
257    #[diagnostic(code(epoch::entity_not_found_by_id))]
258    #[diagnostic(help(
259        "Verify that the identifier is correct and that the entity exists in the current epoch."
260    ))]
261    EntityNotFoundById(EntityIdentifier, Option<EntityType>),
262
263    /// The entity could not be found
264    #[error("Entity of type {0:?} with name {1:?} not found")]
265    #[diagnostic(code(epoch::entity_not_found))]
266    #[diagnostic(help(
267        "Verify that the identifier is correct and that the entity exists in the current epoch."
268    ))]
269    EntityNotFound(EntityType, Option<EntityIdentifier>),
270
271    /// The requested uniform RNG could not be created because the supplied
272    /// distribution parameters were invalid.
273    #[error("Failed to create uniform RNG: {0}")]
274    #[diagnostic(code(EpochError::UniformRngCreation))]
275    #[diagnostic(help("Ensure that the supplied range bounds satisfy `min < max`."))]
276    UniformRngCreation(#[from] rand::distr::uniform::Error),
277
278    /// The string did not contain exactly two comma-separated values.
279    #[error("Invalid position format: expected \"x,y\" found {0}")]
280    #[diagnostic(code(EpochError::InvalidPositionFormat))]
281    #[diagnostic(help("Provide exactly two numbers separated by a comma."))]
282    InvalidPositionFormat(String),
283
284    /// One of the components failed to parse as an `isize`.
285    #[error("Failed to parse integer: {0}")]
286    #[diagnostic(code(EpochError::ParsePositionError))]
287    #[diagnostic(help("Ensure that the input is a valid integer."))]
288    ParsePositionError(String),
289
290    /// Failed to render a table into its string representation.
291    #[error("Failed to render table {name} [{id}] as string")]
292    #[diagnostic(code(EpochError::TableStringConversion))]
293    #[diagnostic(help(
294        "Verify that the table data and schema are compatible with string rendering."
295    ))]
296    TableStringConversionError {
297        /// The logical name of the table that failed to convert.
298        name: String,
299        /// The UUID that uniquely identifies the table instance.
300        id: Uuid,
301    },
302
303    /// Failed to parse a duration string.
304    #[error("Failed to parse duration: {0}:{1}")]
305    #[diagnostic(code(EpochError::ParsingDuration))]
306    #[diagnostic(help(
307        "Verify that the duration string follows the expected syntax, `permanent` or `duration:X` or `done`."
308    ))]
309    ParsingDuration(&'static str, String),
310
311    /// Failed to parse a bounds string.
312    #[error("Failed to parse bounds: {0}:{1}")]
313    #[diagnostic(code(EpochError::ParsingBounds))]
314    #[diagnostic(help(
315        "Verify that the bounds string follows the expected syntax,  `X-X` or `X:X`."
316    ))]
317    ParsingBounds(&'static str, String),
318
319    /// The value started with `@` but the remainder was not a valid id or the name was empty.
320    #[error("Failed to parse identifier or name")]
321    #[diagnostic(code(EntityIdentifierError::InvalidIdentifier))]
322    #[diagnostic(help("Verify that the UUID after `@` is a valid id."))]
323    EmptyIdentifier,
324
325    /// The entity already exists.
326    #[error("Failed to create entity: {0:?} already exists")]
327    #[diagnostic(code(EntityError::EntityAlreadyExist))]
328    #[diagnostic(help(
329        "Ensure that the entity name or ID is unique before creating a new one. \
330     You may need to delete or rename the existing entity."
331    ))]
332    EntityAlreadyExist(Option<EntityIdentifier>),
333    /// The argument could not be parsed.
334    #[error("Failed to parse argument '{argument_parsed}': {while_parsing}")]
335    #[diagnostic(code(EntityError::ParsingArgument))]
336    #[diagnostic(help(
337        "Verify that the value supplied for the argument '{argument_parsed}' matches the expected format. \
338     Refer to the command reference for the correct syntax and examples."
339    ))]
340    ParsingArgument {
341        /// The argument that was being parsed
342        argument_parsed: String,
343        /// The thing that was being parsed
344        while_parsing: &'static str,
345    },
346    /// Indicates that the user supplied **mutually‑exclusive** arguments.
347    ///  
348    /// The command line parser accepts a number of optional arguments that belong to
349    /// *argument groups* – e.g. `--force`, `--dry-run`, `--verbose`, etc.  
350    /// Some of these groups are **incompatible** with each other.  If the user
351    /// specifies an argument from one group together with an argument from a
352    /// conflicting group, this error is returned.
353    #[error("Can't use any {category} related argument on {}", e_type.to_symbol())]
354    #[diagnostic(code(EntityError::IncorrectArgumentCombinaison))]
355    IncorrectArgumentCombinaison {
356        /// The argument category that was involved in the conflict
357        category: &'static str,
358        /// The entity type for which the conflict was detected.
359        e_type: EntityType,
360        /// Optional, user‑friendly guidance on how to fix the issue.
361        #[help]
362        advice: Option<String>,
363    },
364}
365
366/// A small enum of explicit user-facing error messages.
367///
368/// The variants are deliberately simple; each one is rendered with a
369/// clear message via `derive_more::Display`.
370#[derive(Debug, Display, Diagnostic, Error)]
371pub enum UserErrors {
372    /// The classic mistake
373    #[display("The user did a very bad thing")]
374    #[diagnostic(code(UserErrors::DSItF))]
375    DSItF,
376}
377// macro_rules! err_with {
378//     ($expr:expr, $err_ty:ty {$($field:ident : $value:expr),* $(,)?}) => {{
379//         $expr.map_err(|e| { $err_ty { $($field: $value),* } })
380//     }};
381// }
382
383/// Helper macro to convert a `std::io::Error` into an `EpochError::Io`
384/// with an attached help message.
385///
386/// The macro captures the current line number for debugging purposes.
387#[macro_export]
388macro_rules! io_err_with_help {
389    ($expr:expr, $($help:expr),*) => {{
390        $expr.map_err(|e| EpochError::Io {
391            source: e,
392            help: Some(format!($($help),*)),
393            error_line: line!(),
394        })
395    }};
396}