expect_json/expect_core/
expect_op.rs

1use crate::__private::ExpectOpExt;
2use crate::JsonType;
3use crate::expect_core::Context;
4use crate::expect_core::ExpectOpError;
5use crate::expect_core::ExpectOpResult;
6use crate::internals::objects::IntegerObject;
7use crate::internals::objects::NullObject;
8use crate::internals::objects::ValueObject;
9use serde_json::Map;
10use serde_json::Value;
11use std::fmt::Debug;
12
13/// The trait that represents an expectation. It needs to be used in
14/// conjunction with the [`super::expect_op`] macro.
15///
16/// # Example
17///
18/// Here is an example checking if the value returned is a string,
19/// and of a minimum length, using Axum Test.
20///
21/// ```rust
22/// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
23/// #
24/// # use axum::Router;
25/// # use axum::extract::Json;
26/// # use axum::routing::get;
27/// # use axum_test::TestServer;
28/// # use serde_json::json;
29/// #
30/// # let server = TestServer::new(Router::new())?;
31/// #
32/// use axum_test::expect_json;
33/// use axum_test::expect_json::expect_core::ExpectOp;
34/// use axum_test::expect_json::expect_core::ExpectOpResult;
35/// use axum_test::expect_json::expect_core::expect_op;
36/// use axum_test::expect_json::expect_core::Context;
37///
38/// // 1. Implement a struct representing your expectation.
39/// // This needs to include the `expect_op`, and the contents must be serializable.
40/// #[expect_op]
41/// #[derive(Clone, Debug)]
42/// struct ExpectStrMinLen {
43///     min: usize,
44/// }
45///
46/// // 2. Implement `ExpectOp`, and implement the types you want to check for. Here we check against strings.
47/// impl ExpectOp for ExpectStrMinLen {
48///     fn on_string(&self, _context: &mut Context<'_>, received: &str) -> ExpectOpResult<()> {
49///         if received.len() < self.min {
50///             panic!("String is too short, received: {received}");
51///         }
52///
53///         Ok(())
54///     }
55/// }
56///
57/// // 3. Build a router to test against.
58/// let app = Router::new().route(&"/user", get(|| async {
59///     Json(json!({
60///         "name": "Joe",
61///         "age": 20,
62///     }))
63/// }));
64/// let server = TestServer::new(app).unwrap();
65///
66/// // 4. Use the new expectation!
67/// server.get(&"/user").await.assert_json(&json!({
68///     "name": ExpectStrMinLen { min: 3 },
69///     "age": 20,
70/// }));
71/// #
72/// # Ok(()) }
73/// ```
74///
75pub trait ExpectOp: ExpectOpExt + Debug + Send + 'static {
76    fn on_any(&self, context: &mut Context<'_>, received: &Value) -> ExpectOpResult<()> {
77        context.without_propagated_contains().map(|context| {
78            match received {
79                Value::Null => self.on_null(context),
80                Value::Number(received_number) => {
81                    let value_num = ValueObject::from(received_number.clone());
82                    match value_num {
83                        ValueObject::Float(received_float) => self.on_f64(context, received_float.into()),
84                        ValueObject::Integer(IntegerObject::Positive(received_integer)) => self.on_u64(context, received_integer),
85                        ValueObject::Integer(IntegerObject::Negative(received_integer)) => self.on_i64(context, received_integer),
86                        _ => panic!("Unexpected non-number value, expected a float or an integer, found {value_num:?}. (This is a bug, please report at: https://github.com/JosephLenton/expect-json/issues)"),
87                    }
88                }
89                Value::String(received_string) => self.on_string(context, received_string),
90                Value::Bool(received_boolean) => self.on_boolean(context, *received_boolean),
91                Value::Array(received_array) => self.on_array(context, received_array),
92                Value::Object(received_object) => self.on_object(context, received_object),
93            }
94        })
95    }
96
97    #[allow(unused_variables)]
98    fn on_null(&self, context: &mut Context<'_>) -> ExpectOpResult<()> {
99        Err(ExpectOpError::unsupported_operation_type(
100            context, self, NullObject,
101        ))
102    }
103
104    #[allow(unused_variables)]
105    fn on_f64(&self, context: &mut Context<'_>, received: f64) -> ExpectOpResult<()> {
106        Err(ExpectOpError::unsupported_operation_type(
107            context, self, received,
108        ))
109    }
110
111    #[allow(unused_variables)]
112    fn on_u64(&self, context: &mut Context<'_>, received: u64) -> ExpectOpResult<()> {
113        Err(ExpectOpError::unsupported_operation_type(
114            context, self, received,
115        ))
116    }
117
118    #[allow(unused_variables)]
119    fn on_i64(&self, context: &mut Context<'_>, received: i64) -> ExpectOpResult<()> {
120        Err(ExpectOpError::unsupported_operation_type(
121            context, self, received,
122        ))
123    }
124
125    #[allow(unused_variables)]
126    fn on_boolean(&self, context: &mut Context<'_>, received: bool) -> ExpectOpResult<()> {
127        Err(ExpectOpError::unsupported_operation_type(
128            context, self, received,
129        ))
130    }
131
132    #[allow(unused_variables)]
133    fn on_string(&self, context: &mut Context<'_>, received: &str) -> ExpectOpResult<()> {
134        Err(ExpectOpError::unsupported_operation_type(
135            context,
136            self,
137            received.to_owned(),
138        ))
139    }
140
141    #[allow(unused_variables)]
142    fn on_array(&self, context: &mut Context<'_>, received: &[Value]) -> ExpectOpResult<()> {
143        Err(ExpectOpError::unsupported_operation_type(
144            context,
145            self,
146            received.to_owned(),
147        ))
148    }
149
150    #[allow(unused_variables)]
151    fn on_object(
152        &self,
153        context: &mut Context<'_>,
154        received: &Map<String, Value>,
155    ) -> ExpectOpResult<()> {
156        Err(ExpectOpError::unsupported_operation_type(
157            context,
158            self,
159            received.to_owned(),
160        ))
161    }
162
163    /// This is optional to implement. This method returns a list of types this is targeting.
164    ///
165    /// This is used for debug messages for the user, when the type doesn't match up.
166    fn debug_supported_types(&self) -> &'static [JsonType] {
167        &[]
168    }
169}
170
171#[cfg(test)]
172mod test_on_any {
173    use super::*;
174    use crate::internals::ExpectOpMeta;
175    use crate::internals::objects::ArrayObject;
176    use crate::internals::objects::BooleanObject;
177    use crate::internals::objects::FloatObject;
178    use crate::internals::objects::ObjectObject;
179    use crate::internals::objects::StringObject;
180    use crate::internals::objects::ValueTypeObject;
181    use serde_json::json;
182
183    // An empty implementation which will hit the errors by default.
184    #[crate::expect_core::expect_op(internal)]
185    #[derive(Debug, Clone)]
186    struct TestJsonExpectOp;
187
188    impl ExpectOp for TestJsonExpectOp {}
189
190    #[test]
191    fn it_should_error_by_default_against_json_null() {
192        let mut outer_context = Context::new();
193        let received = json!(null);
194        let output = TestJsonExpectOp
195            .on_any(&mut outer_context, &received)
196            .unwrap_err();
197        assert!(matches!(
198            output,
199            ExpectOpError::UnsupportedOperation {
200                context,
201                received: ValueTypeObject(ValueObject::Null(NullObject)),
202                expected_operation: ExpectOpMeta {
203                    name: "TestJsonExpectOp",
204                    types: &[],
205                },
206            } if context == outer_context.to_static()
207        ));
208    }
209
210    #[test]
211    fn it_should_error_by_default_against_json_boolean() {
212        let mut outer_context = Context::new();
213        let received = json!(true);
214        let output = TestJsonExpectOp
215            .on_any(&mut outer_context, &received)
216            .unwrap_err();
217        assert!(matches!(
218            output,
219            ExpectOpError::UnsupportedOperation {
220                context,
221                received: ValueTypeObject(ValueObject::Boolean(BooleanObject(true))),
222                expected_operation: ExpectOpMeta {
223                    name: "TestJsonExpectOp",
224                    types: &[],
225                },
226            } if context == outer_context.to_static()
227        ));
228    }
229
230    #[test]
231    fn it_should_error_by_default_against_json_positive_integer() {
232        let mut outer_context = Context::new();
233        let received = json!(123);
234        let output = TestJsonExpectOp
235            .on_any(&mut outer_context, &received)
236            .unwrap_err();
237        assert!(matches!(
238            output,
239            ExpectOpError::UnsupportedOperation {
240                context,
241                received: ValueTypeObject(ValueObject::Integer(IntegerObject::Positive(123))),
242                expected_operation: ExpectOpMeta {
243                    name: "TestJsonExpectOp",
244                    types: &[],
245                },
246            } if context == outer_context.to_static()
247        ));
248    }
249
250    #[test]
251    fn it_should_error_by_default_against_json_negative_integer() {
252        let mut outer_context = Context::new();
253        let received = json!(-123);
254        let output = TestJsonExpectOp
255            .on_any(&mut outer_context, &received)
256            .unwrap_err();
257        assert!(matches!(
258            output,
259            ExpectOpError::UnsupportedOperation {
260                context,
261                received: ValueTypeObject(ValueObject::Integer(IntegerObject::Negative(-123))),
262                expected_operation: ExpectOpMeta {
263                    name: "TestJsonExpectOp",
264                    types: &[],
265                },
266            } if context == outer_context.to_static()
267        ));
268    }
269
270    #[test]
271    fn it_should_error_by_default_against_json_float() {
272        let mut outer_context = Context::new();
273        let received = json!(123.456);
274        let output = TestJsonExpectOp
275            .on_any(&mut outer_context, &received)
276            .unwrap_err();
277        assert!(matches!(
278            output,
279            ExpectOpError::UnsupportedOperation {
280                context,
281                received: ValueTypeObject(ValueObject::Float(FloatObject(123.456))),
282                expected_operation: ExpectOpMeta {
283                    name: "TestJsonExpectOp",
284                    types: &[],
285                },
286            } if context == outer_context.to_static()
287        ));
288    }
289
290    #[test]
291    fn it_should_error_by_default_against_json_string() {
292        let mut outer_context = Context::new();
293        let received = json!("🦊");
294        let output = TestJsonExpectOp
295            .on_any(&mut outer_context, &received)
296            .unwrap_err();
297        assert!(matches!(
298            output,
299            ExpectOpError::UnsupportedOperation {
300                context,
301                received,
302                expected_operation: ExpectOpMeta {
303                    name: "TestJsonExpectOp",
304                    types: &[],
305                },
306            } if context == outer_context.to_static()
307                && received == ValueTypeObject(ValueObject::String(StringObject("🦊".to_string())))
308        ));
309    }
310
311    #[test]
312    fn it_should_error_by_default_against_json_array() {
313        let mut outer_context = Context::new();
314        let received = json!([]);
315        let output = TestJsonExpectOp
316            .on_any(&mut outer_context, &received)
317            .unwrap_err();
318        assert!(matches!(
319            output,
320            ExpectOpError::UnsupportedOperation {
321                context,
322                received,
323                expected_operation: ExpectOpMeta {
324                    name: "TestJsonExpectOp",
325                    types: &[],
326                },
327            } if context == outer_context.to_static()
328                && received == ValueTypeObject(ValueObject::Array(ArrayObject(vec![])))
329        ));
330    }
331
332    #[test]
333    fn it_should_error_by_default_against_json_object() {
334        let mut outer_context = Context::new();
335        let received = json!({});
336        let output = TestJsonExpectOp
337            .on_any(&mut outer_context, &received)
338            .unwrap_err();
339        assert!(matches!(
340            output,
341            ExpectOpError::UnsupportedOperation {
342                context,
343                received,
344                expected_operation: ExpectOpMeta {
345                    name: "TestJsonExpectOp",
346                    types: &[],
347                },
348            } if context == outer_context.to_static()
349                && received == ValueTypeObject(ValueObject::Object(ObjectObject(Map::new())))
350        ));
351    }
352}