expect_json/
expect_json_error.rs

1use crate::JsonType;
2use crate::expect_core::Context;
3use crate::expect_core::ExpectOpError;
4use crate::internals::objects::ArrayObject;
5use crate::internals::objects::ValueObject;
6use crate::internals::objects::ValueTypeObject;
7use crate::internals::utils::is_unquotable_js_identifier;
8use serde_json::Error as SerdeJsonError;
9use serde_json::Value;
10use std::fmt::Write;
11use thiserror::Error;
12
13pub type ExpectJsonResult<V> = Result<V, ExpectJsonError>;
14
15#[derive(Debug, Error)]
16pub enum ExpectJsonError {
17    #[error("Failed to serialise expected value to Json")]
18    FailedToSerialiseExpected(#[source] SerdeJsonError),
19
20    #[error("Failed to serialise other value to Json")]
21    FailedToSerialiseReceived(#[source] SerdeJsonError),
22
23    #[error(
24        "Json {} at {context} are different types:
25    expected {expected}
26    received {received}",
27        value_or_number_type_name(received, expected)
28    )]
29    DifferentTypes {
30        context: Context<'static>,
31        received: ValueTypeObject,
32        expected: ValueTypeObject,
33    },
34
35    #[error(
36        "Json {json_type}s at {context} are not equal:
37    expected {expected}
38    received {received}"
39    )]
40    DifferentValues {
41        context: Context<'static>,
42        json_type: JsonType,
43        received: ValueObject,
44        expected: ValueObject,
45    },
46
47    #[error(
48        "Json is not null at {context}, expected null:
49    expected null
50    received {received}"
51    )]
52    ReceivedIsNotNull {
53        context: Context<'static>,
54        received: ValueTypeObject,
55    },
56
57    #[error(
58        "Json null received at {context}, expected not null:
59    expected {expected}
60    received null"
61    )]
62    ReceivedIsNull {
63        context: Context<'static>,
64        expected: ValueTypeObject,
65    },
66
67    #[error(
68        "Json objects at {context} are not equal:
69    expected field '{expected_key}',
70    but it was not found"
71    )]
72    ObjectKeyMissing {
73        context: Context<'static>,
74        expected_key: String,
75    },
76
77    #[error(
78        "Json arrays at {context} contain different types:
79    expected {expected}
80        full array {expected_full_array}
81    received {received}
82        full array {received_array}"
83    )]
84    ArrayContainsDifferentTypes {
85        context: Context<'static>,
86        received: Box<ValueTypeObject>,
87        received_array: Box<ArrayObject>,
88        expected: Box<ValueTypeObject>,
89        expected_full_array: Box<ArrayObject>,
90    },
91
92    #[error(
93        "Json {json_type}s at {context} are not equal:
94    expected {expected}
95        full array {expected_full_array}
96    received {received}
97        full array {received_array}"
98    )]
99    ArrayContainsDifferentValues {
100        context: Context<'static>,
101        json_type: JsonType,
102        received: Box<ValueObject>,
103        received_array: Box<ArrayObject>,
104        expected: Box<ValueObject>,
105        expected_full_array: Box<ArrayObject>,
106    },
107
108    #[error(
109        "Json arrays at {context} are not equal:
110    expected {expected_array}
111    received {received_array}"
112    )]
113    ArrayMissingInMiddle {
114        context: Context<'static>,
115        received_array: ArrayObject,
116        expected_array: ArrayObject,
117        // TODO, get this working.
118        // It should display all of the items found in Expected, and not in Received.
119        // Taking into account that some items might be missing as duplicates.
120        // missing_in_received: ArrayObject,
121    },
122
123    #[error(
124        "Json arrays at {context} are not equal:
125    expected {expected_array}
126    received {received_array}"
127    )]
128    ArrayValuesAreDifferent {
129        context: Context<'static>,
130        received_array: ArrayObject,
131        expected_array: ArrayObject,
132    },
133
134    #[error(
135        "Json arrays at {context} are not equal:
136    expected array index at '{expected_index}',
137    but it was not found"
138    )]
139    ArrayIndexMissing {
140        context: Context<'static>,
141        expected_index: usize,
142    },
143
144    #[error(
145        "Json arrays at {context} are not equal, missing {} {} at the end:
146    expected {expected_array}
147    received {received_array}
148     missing {missing_in_received}"
149     , missing_in_received.len(), pluralise_item_word(missing_in_received.len())
150    )]
151    ArrayMissingAtEnd {
152        context: Context<'static>,
153        expected_array: ArrayObject,
154        received_array: ArrayObject,
155        missing_in_received: ArrayObject,
156    },
157
158    #[error(
159        "Json arrays at {context} are not equal, missing {} {} from the start:
160    expected {expected_array}
161    received {received_array}
162     missing {missing_in_received}"
163     , missing_in_received.len(), pluralise_item_word(missing_in_received.len())
164    )]
165    ArrayMissingAtStart {
166        context: Context<'static>,
167        expected_array: ArrayObject,
168        received_array: ArrayObject,
169        missing_in_received: ArrayObject,
170    },
171
172    #[error(
173        "Json arrays at {context} are not equal, received {} extra {} at the end:
174    expected {expected_array}
175    received {received_array}
176       extra {extra_in_received}"
177     , extra_in_received.len(), pluralise_item_word(extra_in_received.len())
178    )]
179    ArrayExtraAtEnd {
180        context: Context<'static>,
181        expected_array: ArrayObject,
182        received_array: ArrayObject,
183        extra_in_received: ArrayObject,
184    },
185
186    #[error(
187        "Json arrays at {context} are not equal, received {} extra {} at the start:
188    expected {expected_array}
189    received {received_array}
190       extra {extra_in_received}"
191     , extra_in_received.len(), pluralise_item_word(extra_in_received.len())
192    )]
193    ArrayExtraAtStart {
194        context: Context<'static>,
195        expected_array: ArrayObject,
196        received_array: ArrayObject,
197        extra_in_received: ArrayObject,
198    },
199
200    #[error(
201        r#"Json object at {context} has extra field "{received_extra_field}":
202    expected {expected_obj}
203    received {received_obj}"#
204    )]
205    ObjectReceivedHasExtraKey {
206        context: Context<'static>,
207        received_extra_field: String,
208        received_obj: ValueObject,
209        expected_obj: ValueObject,
210    },
211
212    #[error(
213        r#"Json object at {context} has many extra fields over expected:
214    expected {expected_obj}
215    received {received_obj}
216
217    extra fields in received:
218{}"#,
219        format_extra_fields(received_extra_fields)
220    )]
221    ObjectReceivedHasExtraKeys {
222        context: Context<'static>,
223        received_extra_fields: Vec<String>,
224        received_obj: ValueObject,
225        expected_obj: ValueObject,
226    },
227
228    #[error("{source}")]
229    ExpectOpError {
230        #[from]
231        source: ExpectOpError,
232    },
233}
234
235impl ExpectJsonError {
236    pub(crate) fn array_index_missing<'a>(
237        context: &mut Context<'a>,
238        source_error: Self,
239        received_array: &'a [Value],
240        expected_array: &'a [Value],
241    ) -> Self {
242        match source_error {
243            Self::DifferentValues {
244                context: source_error_context,
245                json_type,
246                received,
247                expected,
248            } => {
249                // If the source is deeper, then it takes precedence.
250                if source_error_context != *context {
251                    return Self::DifferentValues {
252                        context: source_error_context,
253                        json_type,
254                        received,
255                        expected,
256                    };
257                }
258
259                Self::ArrayContainsDifferentValues {
260                    context: source_error_context,
261                    json_type,
262                    received: Box::new(received),
263                    received_array: Box::new(ArrayObject::from(received_array.to_owned())),
264                    expected: Box::new(expected),
265                    expected_full_array: Box::new(ArrayObject::from(expected_array.to_owned())),
266                }
267            }
268            Self::DifferentTypes {
269                context: source_error_context,
270                received,
271                expected,
272            } => {
273                // If the source is deeper, then it takes precedence.
274                if source_error_context != *context {
275                    return Self::DifferentTypes {
276                        context: source_error_context,
277                        received,
278                        expected,
279                    };
280                }
281
282                Self::ArrayContainsDifferentTypes {
283                    context: source_error_context,
284                    received: Box::new(received),
285                    received_array: Box::new(ArrayObject::from(received_array.to_owned())),
286                    expected: Box::new(expected),
287                    expected_full_array: Box::new(ArrayObject::from(expected_array.to_owned())),
288                }
289            }
290
291            // All other errors are left as is.
292            _ => source_error,
293        }
294    }
295}
296
297fn pluralise_item_word(len: usize) -> &'static str {
298    if len == 1 { "item" } else { "items" }
299}
300
301fn value_or_number_type_name(left: &ValueTypeObject, right: &ValueTypeObject) -> &'static str {
302    if left.is_number() && right.is_number() {
303        "numbers"
304    } else {
305        "values"
306    }
307}
308
309fn format_extra_fields(received_extra_fields: &[String]) -> String {
310    let mut output = String::new();
311
312    for field in received_extra_fields {
313        if is_unquotable_js_identifier(field) {
314            let _ = writeln!(output, "        {field},");
315        } else {
316            let _ = writeln!(output, r#"        "{field}","#);
317        }
318    }
319
320    output
321}