tirea_state/
error.rs

1//! Error types for tirea-state operations.
2
3use crate::Path;
4use thiserror::Error;
5
6/// Result type alias for tirea-state operations.
7pub type TireaResult<T> = Result<T, TireaError>;
8
9/// Errors that can occur during tirea-state operations.
10#[derive(Debug, Error)]
11pub enum TireaError {
12    /// Path does not exist in the document.
13    #[error("path not found: {path}")]
14    PathNotFound {
15        /// The path that was not found.
16        path: Path,
17    },
18
19    /// Array index is out of bounds.
20    #[error("index {index} out of bounds (len: {len}) at path {path}")]
21    IndexOutOfBounds {
22        /// The path to the array.
23        path: Path,
24        /// The index that was accessed.
25        index: usize,
26        /// The actual length of the array.
27        len: usize,
28    },
29
30    /// Type mismatch when accessing a value.
31    #[error("type mismatch at {path}: expected {expected}, found {found}")]
32    TypeMismatch {
33        /// The path where the mismatch occurred.
34        path: Path,
35        /// The expected type.
36        expected: &'static str,
37        /// The actual type found.
38        found: &'static str,
39    },
40
41    /// Numeric operation on a non-numeric value.
42    #[error("numeric operation requires number at {path}")]
43    NumericOperationOnNonNumber {
44        /// The path where the non-numeric value was found.
45        path: Path,
46    },
47
48    /// Merge operation requires an object value.
49    #[error("merge requires object value at {path}")]
50    MergeRequiresObject {
51        /// The path where a non-object was found.
52        path: Path,
53    },
54
55    /// Append operation requires an array value.
56    #[error("append requires array value at {path}")]
57    AppendRequiresArray {
58        /// The path where a non-array was found.
59        path: Path,
60    },
61
62    /// Invalid operation error.
63    #[error("invalid operation: {message}")]
64    InvalidOperation {
65        /// Description of what went wrong.
66        message: String,
67    },
68
69    /// JSON serialization/deserialization error.
70    #[error("serialization error: {0}")]
71    Serialization(#[from] serde_json::Error),
72}
73
74impl TireaError {
75    /// Create a path not found error.
76    #[inline]
77    pub fn path_not_found(path: Path) -> Self {
78        TireaError::PathNotFound { path }
79    }
80
81    /// Create an index out of bounds error.
82    #[inline]
83    pub fn index_out_of_bounds(path: Path, index: usize, len: usize) -> Self {
84        TireaError::IndexOutOfBounds { path, index, len }
85    }
86
87    /// Create a type mismatch error.
88    #[inline]
89    pub fn type_mismatch(path: Path, expected: &'static str, found: &'static str) -> Self {
90        TireaError::TypeMismatch {
91            path,
92            expected,
93            found,
94        }
95    }
96
97    /// Create a numeric operation on non-number error.
98    #[inline]
99    pub fn numeric_on_non_number(path: Path) -> Self {
100        TireaError::NumericOperationOnNonNumber { path }
101    }
102
103    /// Create a merge requires object error.
104    #[inline]
105    pub fn merge_requires_object(path: Path) -> Self {
106        TireaError::MergeRequiresObject { path }
107    }
108
109    /// Create an append requires array error.
110    #[inline]
111    pub fn append_requires_array(path: Path) -> Self {
112        TireaError::AppendRequiresArray { path }
113    }
114
115    /// Create an invalid operation error.
116    #[inline]
117    pub fn invalid_operation(message: impl Into<String>) -> Self {
118        TireaError::InvalidOperation {
119            message: message.into(),
120        }
121    }
122
123    /// Add a path prefix to this error.
124    ///
125    /// This is used when deserializing nested structures to maintain
126    /// the full path context. For example, if a nested struct at path
127    /// "address" has an error at "city", this will combine them into
128    /// "address.city".
129    pub fn with_prefix(self, prefix: &Path) -> Self {
130        match self {
131            TireaError::PathNotFound { path } => {
132                let mut new_path = prefix.clone();
133                for seg in path.iter() {
134                    new_path.push(seg.clone());
135                }
136                TireaError::PathNotFound { path: new_path }
137            }
138            TireaError::TypeMismatch {
139                path,
140                expected,
141                found,
142            } => {
143                let mut new_path = prefix.clone();
144                for seg in path.iter() {
145                    new_path.push(seg.clone());
146                }
147                TireaError::TypeMismatch {
148                    path: new_path,
149                    expected,
150                    found,
151                }
152            }
153            TireaError::IndexOutOfBounds { path, index, len } => {
154                let mut new_path = prefix.clone();
155                for seg in path.iter() {
156                    new_path.push(seg.clone());
157                }
158                TireaError::IndexOutOfBounds {
159                    path: new_path,
160                    index,
161                    len,
162                }
163            }
164            // For other error types, path prefix doesn't apply
165            other => other,
166        }
167    }
168}
169
170/// Get the type name of a JSON value.
171#[inline]
172pub fn value_type_name(v: &serde_json::Value) -> &'static str {
173    match v {
174        serde_json::Value::Null => "null",
175        serde_json::Value::Bool(_) => "boolean",
176        serde_json::Value::Number(_) => "number",
177        serde_json::Value::String(_) => "string",
178        serde_json::Value::Array(_) => "array",
179        serde_json::Value::Object(_) => "object",
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::path;
187
188    #[test]
189    fn test_error_display() {
190        let err = TireaError::path_not_found(path!("users", 0, "name"));
191        assert!(err.to_string().contains("path not found"));
192    }
193
194    #[test]
195    fn test_value_type_name() {
196        use serde_json::json;
197
198        assert_eq!(value_type_name(&json!(null)), "null");
199        assert_eq!(value_type_name(&json!(true)), "boolean");
200        assert_eq!(value_type_name(&json!(42)), "number");
201        assert_eq!(value_type_name(&json!("hello")), "string");
202        assert_eq!(value_type_name(&json!([1, 2, 3])), "array");
203        assert_eq!(value_type_name(&json!({"a": 1})), "object");
204    }
205}