Skip to main content

bible_io/
errors.rs

1//! Error values returned by Bible IO operations.
2
3use std::{error::Error, fmt, sync::Arc};
4
5use bible_io_references::ParseError;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Stable machine-readable codes for malformed Bible and catalog data.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum BibleDataFormatErrorCode {
13    /// Input was not valid JSON or UTF-8 JSON.
14    InvalidJson,
15    /// A JSON value had the wrong type.
16    InvalidType,
17    /// A required field was absent.
18    MissingField,
19    /// A value violated the content contract.
20    InvalidValue,
21    /// An identifier appeared more than once.
22    DuplicateId,
23    /// Extension data used a structural field name.
24    ReservedField,
25    /// A value could not be represented as JSON.
26    NonJsonValue,
27}
28
29impl BibleDataFormatErrorCode {
30    /// Return the stable snake-case representation of this error code.
31    #[must_use]
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            Self::InvalidJson => "invalid_json",
35            Self::InvalidType => "invalid_type",
36            Self::MissingField => "missing_field",
37            Self::InvalidValue => "invalid_value",
38            Self::DuplicateId => "duplicate_id",
39            Self::ReservedField => "reserved_field",
40            Self::NonJsonValue => "non_json_value",
41        }
42    }
43}
44
45impl fmt::Display for BibleDataFormatErrorCode {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter.write_str(self.as_str())
48    }
49}
50
51/// A path-aware violation of the serialized Bible content contract.
52#[derive(Debug, Clone, PartialEq)]
53pub struct BibleDataFormatError {
54    code: BibleDataFormatErrorCode,
55    path: String,
56    message: String,
57    value: Option<Box<Value>>,
58    cause: Option<StoredCause>,
59}
60
61#[derive(Clone)]
62struct StoredCause {
63    message: String,
64    error: Arc<dyn Error + Send + Sync>,
65}
66
67impl fmt::Debug for StoredCause {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter
70            .debug_tuple("StoredCause")
71            .field(&self.message)
72            .finish()
73    }
74}
75
76impl PartialEq for StoredCause {
77    fn eq(&self, other: &Self) -> bool {
78        self.message == other.message
79    }
80}
81
82impl fmt::Display for StoredCause {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        formatter.write_str(&self.message)
85    }
86}
87
88impl BibleDataFormatError {
89    /// Construct an error without an offending value.
90    pub fn new(
91        code: BibleDataFormatErrorCode,
92        path: impl Into<String>,
93        message: impl Into<String>,
94    ) -> Self {
95        Self {
96            code,
97            path: path.into(),
98            message: message.into(),
99            value: None,
100            cause: None,
101        }
102    }
103
104    /// Attach the offending JSON value.
105    #[must_use]
106    pub fn with_value(mut self, value: Value) -> Self {
107        self.value = Some(Box::new(value));
108        self
109    }
110
111    /// Attach the underlying error while preserving its concrete type.
112    #[must_use]
113    pub fn with_cause<E>(mut self, cause: E) -> Self
114    where
115        E: Error + Send + Sync + 'static,
116    {
117        self.cause = Some(StoredCause {
118            message: cause.to_string(),
119            error: Arc::new(cause),
120        });
121        self
122    }
123
124    /// Return the stable machine-readable code.
125    #[must_use]
126    pub const fn code(&self) -> BibleDataFormatErrorCode {
127        self.code
128    }
129
130    /// Return the JSONPath-like location of the invalid value.
131    #[must_use]
132    pub fn path(&self) -> &str {
133        &self.path
134    }
135
136    /// Return the human-readable explanation.
137    #[must_use]
138    pub fn message(&self) -> &str {
139        &self.message
140    }
141
142    /// Return the offending JSON value, when one was captured.
143    #[must_use]
144    pub fn value(&self) -> Option<&Value> {
145        self.value.as_deref()
146    }
147
148    /// Return the underlying error text, when one was captured.
149    #[must_use]
150    pub fn cause(&self) -> Option<&str> {
151        self.cause.as_ref().map(|cause| cause.message.as_str())
152    }
153}
154
155impl fmt::Display for BibleDataFormatError {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(
158            formatter,
159            "BibleDataFormatError({}) at {}: {}",
160            self.code, self.path, self.message
161        )?;
162        if let Some(value) = &self.value {
163            write!(formatter, "\nValue: {value}")?;
164        }
165        if let Some(cause) = &self.cause {
166            write!(formatter, "\nCause: {cause}")?;
167        }
168        Ok(())
169    }
170}
171
172impl Error for BibleDataFormatError {
173    fn source(&self) -> Option<&(dyn Error + 'static)> {
174        self.cause
175            .as_ref()
176            .map(|cause| cause.error.as_ref() as &(dyn Error + 'static))
177    }
178}
179
180/// A validated in-memory model could not be constructed.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct ModelError {
183    field: &'static str,
184    message: String,
185}
186
187impl ModelError {
188    /// Construct a model validation error.
189    pub fn new(field: &'static str, message: impl Into<String>) -> Self {
190        Self {
191            field,
192            message: message.into(),
193        }
194    }
195
196    /// Return the invalid field name.
197    #[must_use]
198    pub const fn field(&self) -> &'static str {
199        self.field
200    }
201
202    /// Return the validation explanation.
203    #[must_use]
204    pub fn message(&self) -> &str {
205        &self.message
206    }
207}
208
209impl fmt::Display for ModelError {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(formatter, "{}: {}", self.field, self.message)
212    }
213}
214
215impl Error for ModelError {}
216
217/// Errors that can occur when accessing Bible content.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum BibleError {
220    /// The requested book is not present in the specified Bible translation.
221    BookNotFound {
222        /// Requested compact book identifier.
223        book_abbrev: String,
224        /// Human-readable book name.
225        book_name: String,
226        /// Human-readable edition name.
227        translation: String,
228    },
229    /// The requested chapter number does not exist in the specified book.
230    ChapterOutOfBounds {
231        /// Compact book identifier.
232        book_abbrev: String,
233        /// Human-readable book name.
234        book_name: String,
235        /// Requested chapter number.
236        chapter: usize,
237        /// Greatest declared chapter number, or zero for an empty book.
238        max_chapter: usize,
239    },
240    /// The requested verse number does not exist in the specified chapter.
241    VerseOutOfBounds {
242        /// Compact book identifier.
243        book_abbrev: String,
244        /// Human-readable book name.
245        book_name: String,
246        /// Requested chapter number.
247        chapter: usize,
248        /// Requested verse number.
249        verse: usize,
250        /// Greatest declared verse number, or zero for an empty chapter.
251        max_verse: usize,
252    },
253    /// The provided reference string could not be parsed.
254    InvalidReference {
255        /// Original reference input after surrounding whitespace was removed.
256        input: String,
257    },
258    /// The reference package rejected a human-readable reference.
259    ReferenceParse {
260        /// Original input after surrounding whitespace was removed.
261        input: String,
262        /// Structured dependency parse failure.
263        cause: ParseError,
264    },
265    /// A range is descending in the loaded edition's declared order.
266    InvalidRange {
267        /// Human-readable explanation.
268        message: String,
269    },
270    /// A persisted-state key was requested for an edition without an ID.
271    MissingEditionId,
272    /// A chapter-only location was used where a verse location was required.
273    VerseRequired,
274}
275
276impl fmt::Display for BibleError {
277    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
278        match self {
279            Self::BookNotFound {
280                book_abbrev,
281                book_name,
282                translation,
283            } => write!(
284                formatter,
285                "Book {book_name} ('{book_abbrev}') not found in the '{translation}' Bible translation"
286            ),
287            Self::ChapterOutOfBounds {
288                book_abbrev,
289                book_name,
290                chapter,
291                max_chapter,
292            } => write!(
293                formatter,
294                "Chapter {chapter} is out of bounds for book {book_name} ('{book_abbrev}') (max {max_chapter})"
295            ),
296            Self::VerseOutOfBounds {
297                book_abbrev,
298                book_name,
299                chapter,
300                verse,
301                max_verse,
302            } => write!(
303                formatter,
304                "Verse {verse} is out of bounds for book {book_name} ('{book_abbrev}') chapter {chapter} (max {max_verse})"
305            ),
306            Self::InvalidReference { input } => write!(formatter, "Invalid reference: '{input}'"),
307            Self::ReferenceParse { input, cause } => {
308                write!(formatter, "Invalid reference '{input}': {cause}")
309            }
310            Self::InvalidRange { message } => formatter.write_str(message),
311            Self::MissingEditionId => formatter.write_str(
312                "Bible metadata must define an id before creating persisted keys",
313            ),
314            Self::VerseRequired => {
315                formatter.write_str("the Bible location must identify a verse")
316            }
317        }
318    }
319}
320
321impl Error for BibleError {
322    fn source(&self) -> Option<&(dyn Error + 'static)> {
323        match self {
324            Self::ReferenceParse { cause, .. } => Some(cause),
325            _ => None,
326        }
327    }
328}