1use std::{error::Error, fmt, sync::Arc};
4
5use bible_io_references::ParseError;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum BibleDataFormatErrorCode {
13 InvalidJson,
15 InvalidType,
17 MissingField,
19 InvalidValue,
21 DuplicateId,
23 ReservedField,
25 NonJsonValue,
27}
28
29impl BibleDataFormatErrorCode {
30 #[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#[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 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 #[must_use]
106 pub fn with_value(mut self, value: Value) -> Self {
107 self.value = Some(Box::new(value));
108 self
109 }
110
111 #[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 #[must_use]
126 pub const fn code(&self) -> BibleDataFormatErrorCode {
127 self.code
128 }
129
130 #[must_use]
132 pub fn path(&self) -> &str {
133 &self.path
134 }
135
136 #[must_use]
138 pub fn message(&self) -> &str {
139 &self.message
140 }
141
142 #[must_use]
144 pub fn value(&self) -> Option<&Value> {
145 self.value.as_deref()
146 }
147
148 #[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#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct ModelError {
183 field: &'static str,
184 message: String,
185}
186
187impl ModelError {
188 pub fn new(field: &'static str, message: impl Into<String>) -> Self {
190 Self {
191 field,
192 message: message.into(),
193 }
194 }
195
196 #[must_use]
198 pub const fn field(&self) -> &'static str {
199 self.field
200 }
201
202 #[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#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum BibleError {
220 BookNotFound {
222 book_abbrev: String,
224 book_name: String,
226 translation: String,
228 },
229 ChapterOutOfBounds {
231 book_abbrev: String,
233 book_name: String,
235 chapter: usize,
237 max_chapter: usize,
239 },
240 VerseOutOfBounds {
242 book_abbrev: String,
244 book_name: String,
246 chapter: usize,
248 verse: usize,
250 max_verse: usize,
252 },
253 InvalidReference {
255 input: String,
257 },
258 ReferenceParse {
260 input: String,
262 cause: ParseError,
264 },
265 InvalidRange {
267 message: String,
269 },
270 MissingEditionId,
272 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}