1#![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#[derive(Debug, Error, Diagnostic)]
31pub enum StorageError {
32 #[error("Data error: {0}")]
34 Serialisation(#[from] serde_json::Error),
35
36 #[error("Database connection failed: {0}")]
38 Connection(String),
39
40 #[error("Query error: {0}")]
42 Query(String),
43
44 #[error("Record not found")]
46 NotFound,
47}
48
49#[derive(Error, Debug, Diagnostic)]
56pub enum EpochError {
57 #[error("Failed to read epoch configuration")]
59 #[diagnostic(code(EpochError::ConfigRead))]
60 ConfigRead(#[source] std::io::Error),
61
62 #[error("Failed to write epoch configuration")]
64 #[diagnostic(code(EpochError::ConfigWrite))]
65 ConfigWrite(#[source] std::io::Error),
66
67 #[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 #[error("This directory is already an epoch folder and thus cannot be initialized")]
75 #[diagnostic(code(EpochError::AlreadyInitialized))]
76 AlreadyInitialized {
77 #[help]
78 advice: Option<String>,
80 },
81
82 #[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: String,
90 #[label("This file is present and forbid the command to execute properly")]
92 show_label: Option<(usize, usize)>,
93 },
94
95 #[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 #[error("Failed to write `.epochinfo` in epoch {id} ({path})")]
102 #[diagnostic(code(EpochError::EpochInfoWrite))]
103 EpochInfoWrite {
104 id: EpochId,
106 path: PathBuf,
108 #[help]
110 help: Option<String>,
111 #[source]
113 src: Option<std::io::Error>,
114 },
115
116 #[error("No epoch found for id/tag `{0}`")]
118 #[diagnostic(code(EpochError::EpochNotFound))]
119 EpochNotFound(String),
120
121 #[error("Tag `{0}` resolves to either multiple epochs or invalid epoch")]
123 #[diagnostic(code(EpochError::AmbiguousTag))]
124 AmbiguousTag(String),
125
126 #[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 #[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 #[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 #[error("Cannot switch to epoch `{0}` - the world has already advanced beyond it")]
148 #[diagnostic(code(EpochError::SwitchPastEpoch))]
149 SwitchPastEpoch(EpochId),
150
151 #[error("Invalid epoch id `{0}` - must be a positive, non-overflowing integer")]
153 #[diagnostic(code(EpochError::InvalidEpochId))]
154 InvalidEpochId(EpochId),
155
156 #[error("Tag `{1}` already exists for epoch {0}")]
158 #[diagnostic(code(EpochError::InvalidEpochId))]
159 TagAlreadyExists(EpochId, String),
160
161 #[error("Parent epoch {} referenced by epoch {} does not exist", .0, .1)]
163 #[diagnostic(code(EpochError::ParentEpochMissing))]
164 ParentEpochMissing(EpochId, EpochId),
165
166 #[error("I/O error")]
168 #[diagnostic(code(EpochError::GenericIO))]
169 Io {
170 #[source]
172 source: std::io::Error,
173 #[help]
175 help: Option<String>,
176 error_line: u32,
178 },
179
180 #[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 #[source]
187 source: serde_json::Error,
188 file_name: String,
190 version: String,
192 },
193
194 #[error("Invalid epoch tree at {0}")]
196 #[diagnostic(code(EpochError::TreeInvalid))]
197 TreeInvalid(EpochId, String),
198
199 #[error("The given directory ({0:?}) is not an epoch folder")]
201 #[diagnostic(code(EpochError::MainFolderInvalid))]
202 MainFolderInvalid(String),
203
204 #[error("The id ({0}) couldn't be converted to a valid EpochId")]
206 #[diagnostic(code(EpochError::ParsingEpochId))]
207 ParsingEpochId(#[from] ParseIntError),
208
209 #[error("The id ({0}) couldn't be loaded")]
211 #[diagnostic(code(EpochError::LoadingEpochId))]
212 LoadingEpochId(EpochId),
213
214 #[error("An unknown error occurred: {0}")]
216 #[diagnostic(code(EpochError::Unknown))]
217 Unknown(String),
218
219 #[error("Unknown stat acronym: {0}")]
221 #[diagnostic(code(EpochError::UnknownStatAcronim))]
222 UnknownStatAcronim(String),
223
224 #[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 #[error("An user facing error")]
231 #[diagnostic(code(EpochError::UserError))]
232 UserError(#[help] UserErrors),
233
234 #[error("An error happened when trying to pick a rng event for {cause}")]
236 #[diagnostic(code(EpochError::RNGPickError))]
237 RNGPickError {
238 cause: String,
240 #[help]
241 help: Option<String>,
243 },
244
245 #[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 #[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 #[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 #[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 #[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 #[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 #[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 name: String,
299 id: Uuid,
301 },
302
303 #[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 #[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 #[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 #[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 #[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 argument_parsed: String,
343 while_parsing: &'static str,
345 },
346 #[error("Can't use any {category} related argument on {}", e_type.to_symbol())]
354 #[diagnostic(code(EntityError::IncorrectArgumentCombinaison))]
355 IncorrectArgumentCombinaison {
356 category: &'static str,
358 e_type: EntityType,
360 #[help]
362 advice: Option<String>,
363 },
364}
365
366#[derive(Debug, Display, Diagnostic, Error)]
371pub enum UserErrors {
372 #[display("The user did a very bad thing")]
374 #[diagnostic(code(UserErrors::DSItF))]
375 DSItF,
376}
377#[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}