json_test/
error.rs

1use std::collections::HashMap;
2use serde_json::Value;
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum JsonPathError {
7    #[error("{message}\nPath: {path}\nActual Value: {actual}\n{}", context_string(.context, .expected))]
8    AssertionFailed {
9        message: String,
10        path: String,
11        actual: Value,
12        expected: Option<Value>,
13        context: HashMap<String, String>,
14    },
15
16    #[error("Invalid JSONPath expression: {0}")]
17    InvalidPath(String),
18}
19
20/// Helper function for formatting context in error messages
21fn context_string(context: &HashMap<String, String>, expected: &Option<Value>) -> String {
22    let mut parts = Vec::new();
23
24    // Add expected value if present
25    if let Some(exp) = expected {
26        parts.push(format!("Expected Value: {}", exp));
27    }
28
29    // Add all context key-value pairs
30    for (key, value) in context {
31        parts.push(format!("{}: {}", key, value));
32    }
33
34    parts.join("\n")
35}
36
37impl JsonPathError {
38    pub fn assertion_failed(
39        message: impl Into<String>,
40        path: impl Into<String>,
41        actual: Value,
42        expected: Option<Value>,
43        context: HashMap<String, String>,
44    ) -> Self {
45        JsonPathError::AssertionFailed {
46            message: message.into(),
47            path: path.into(),
48            actual,
49            expected,
50            context,
51        }
52    }
53
54    pub fn type_mismatch(path: String, actual: Value, expected_type: &str) -> Self {
55        let mut context = HashMap::new();
56        context.insert("Expected Type".to_string(), expected_type.to_string());
57        context.insert("Actual Type".to_string(), type_name(&actual));
58
59        JsonPathError::AssertionFailed {
60            message: format!("Expected value of type {}", expected_type),
61            path,
62            actual,
63            expected: None,
64            context,
65        }
66    }
67
68    pub fn value_mismatch(path: String, actual: Value, expected: Value) -> Self {
69        let mut context = HashMap::new();
70        context.insert("Operation".to_string(), "Equality".to_string());
71
72        JsonPathError::AssertionFailed {
73            message: "Value mismatch".to_string(),
74            path,
75            actual,
76            expected: Some(expected),
77            context,
78        }
79    }
80
81    pub fn comparison_failed(
82        path: String,
83        actual: Value,
84        operation: &str,
85        comparison_value: Value,
86    ) -> Self {
87        let mut context = HashMap::new();
88        context.insert("Operation".to_string(), operation.to_string());
89        context.insert("Comparison Value".to_string(), comparison_value.to_string());
90
91        JsonPathError::AssertionFailed {
92            message: format!("Value comparison failed for operation: {}", operation),
93            path,
94            actual,
95            expected: None,
96            context,
97        }
98    }
99
100    pub fn property_error(
101        path: String,
102        actual: Value,
103        message: String,
104        context: HashMap<String, String>,
105    ) -> Self {
106        JsonPathError::AssertionFailed {
107            message,
108            path,
109            actual,
110            expected: None,
111            context,
112        }
113    }
114}
115
116/// Helper function to get readable type names
117fn type_name(value: &Value) -> String {
118    match value {
119        Value::Null => "null",
120        Value::Bool(_) => "boolean",
121        Value::Number(_) => "number",
122        Value::String(_) => "string",
123        Value::Array(_) => "array",
124        Value::Object(_) => "object",
125    }.to_string()
126}
127
128/// Extension trait for adding context to errors
129pub trait ErrorContext<T> {
130    fn with_context<K, V>(self, key: K, value: V) -> Result<T, JsonPathError>
131    where
132        K: Into<String>,
133        V: Into<String>;
134
135    fn with_contexts<I, K, V>(self, contexts: I) -> Result<T, JsonPathError>
136    where
137        I: IntoIterator<Item = (K, V)>,
138        K: Into<String>,
139        V: Into<String>;
140}
141
142impl<T> ErrorContext<T> for Result<T, JsonPathError> {
143    fn with_context<K, V>(self, key: K, value: V) -> Result<T, JsonPathError>
144    where
145        K: Into<String>,
146        V: Into<String>,
147    {
148        self.map_err(|err| {
149            if let JsonPathError::AssertionFailed {
150                message,
151                path,
152                actual,
153                expected,
154                mut context,
155            } = err
156            {
157                context.insert(key.into(), value.into());
158                JsonPathError::AssertionFailed {
159                    message,
160                    path,
161                    actual,
162                    expected,
163                    context,
164                }
165            } else {
166                err
167            }
168        })
169    }
170
171    fn with_contexts<I, K, V>(self, contexts: I) -> Result<T, JsonPathError>
172    where
173        I: IntoIterator<Item = (K, V)>,
174        K: Into<String>,
175        V: Into<String>,
176    {
177        self.map_err(|err| {
178            if let JsonPathError::AssertionFailed {
179                message,
180                path,
181                actual,
182                expected,
183                mut context,
184            } = err
185            {
186                for (k, v) in contexts {
187                    context.insert(k.into(), v.into());
188                }
189                JsonPathError::AssertionFailed {
190                    message,
191                    path,
192                    actual,
193                    expected,
194                    context,
195                }
196            } else {
197                err
198            }
199        })
200    }
201}