expect_json/expect_core/
expect_op.rs

1use crate::__private::ExpectOpExt;
2use crate::expect_core::Context;
3use crate::expect_core::ExpectOpError;
4use crate::expect_core::ExpectOpResult;
5use crate::internals::objects::IntegerObject;
6use crate::internals::objects::NullObject;
7use crate::internals::objects::ValueObject;
8use crate::JsonType;
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        match received {
78            Value::Null => self.on_null(context),
79            Value::Number(received_number) => {
80                let value_num = ValueObject::from(received_number.clone());
81                match value_num {
82                    ValueObject::Float(received_float) => self.on_f64(context, received_float.into()),
83                    ValueObject::Integer(IntegerObject::Positive(received_integer)) => self.on_u64(context, received_integer),
84                    ValueObject::Integer(IntegerObject::Negative(received_integer)) => self.on_i64(context, received_integer),
85                    _ => 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)"),
86                }
87            }
88            Value::String(received_string) => self.on_string(context, received_string),
89            Value::Bool(received_boolean) => self.on_boolean(context, *received_boolean),
90            Value::Array(received_array) => self.on_array(context, received_array),
91            Value::Object(received_object) => self.on_object(context, received_object),
92        }
93    }
94
95    #[allow(unused_variables)]
96    fn on_null(&self, context: &mut Context<'_>) -> ExpectOpResult<()> {
97        Err(ExpectOpError::unsupported_operation_type(
98            context, self, NullObject,
99        ))
100    }
101
102    #[allow(unused_variables)]
103    fn on_f64(&self, context: &mut Context<'_>, received: f64) -> ExpectOpResult<()> {
104        Err(ExpectOpError::unsupported_operation_type(
105            context, self, received,
106        ))
107    }
108
109    #[allow(unused_variables)]
110    fn on_u64(&self, context: &mut Context<'_>, received: u64) -> ExpectOpResult<()> {
111        Err(ExpectOpError::unsupported_operation_type(
112            context, self, received,
113        ))
114    }
115
116    #[allow(unused_variables)]
117    fn on_i64(&self, context: &mut Context<'_>, received: i64) -> ExpectOpResult<()> {
118        Err(ExpectOpError::unsupported_operation_type(
119            context, self, received,
120        ))
121    }
122
123    #[allow(unused_variables)]
124    fn on_boolean(&self, context: &mut Context<'_>, received: bool) -> ExpectOpResult<()> {
125        Err(ExpectOpError::unsupported_operation_type(
126            context, self, received,
127        ))
128    }
129
130    #[allow(unused_variables)]
131    fn on_string(&self, context: &mut Context<'_>, received: &str) -> ExpectOpResult<()> {
132        Err(ExpectOpError::unsupported_operation_type(
133            context,
134            self,
135            received.to_owned(),
136        ))
137    }
138
139    #[allow(unused_variables)]
140    fn on_array(&self, context: &mut Context<'_>, received: &[Value]) -> ExpectOpResult<()> {
141        Err(ExpectOpError::unsupported_operation_type(
142            context,
143            self,
144            received.to_owned(),
145        ))
146    }
147
148    #[allow(unused_variables)]
149    fn on_object(
150        &self,
151        context: &mut Context<'_>,
152        received: &Map<String, Value>,
153    ) -> ExpectOpResult<()> {
154        Err(ExpectOpError::unsupported_operation_type(
155            context,
156            self,
157            received.to_owned(),
158        ))
159    }
160
161    /// This is optional to implement. This method returns a list of types this is targeting.
162    ///
163    /// This is used for debug messages for the user, when the type doesn't match up.
164    fn debug_supported_types(&self) -> &'static [JsonType] {
165        &[]
166    }
167}
168
169#[cfg(test)]
170mod test_on_any {
171    use super::*;
172    use crate::internals::objects::ArrayObject;
173    use crate::internals::objects::BooleanObject;
174    use crate::internals::objects::FloatObject;
175    use crate::internals::objects::ObjectObject;
176    use crate::internals::objects::StringObject;
177    use crate::internals::objects::ValueTypeObject;
178    use crate::internals::ExpectOpMeta;
179    use serde_json::json;
180
181    // An empty implementation which will hit the errors by default.
182    #[crate::expect_core::expect_op(internal)]
183    #[derive(Debug, Clone)]
184    struct TestJsonExpectOp;
185
186    impl ExpectOp for TestJsonExpectOp {}
187
188    #[test]
189    fn it_should_error_by_default_against_json_null() {
190        let mut outer_context = Context::new();
191        let received = json!(null);
192        let output = TestJsonExpectOp
193            .on_any(&mut outer_context, &received)
194            .unwrap_err();
195        assert!(matches!(
196            output,
197            ExpectOpError::UnsupportedOperation {
198                context,
199                received: ValueTypeObject(ValueObject::Null(NullObject)),
200                expected_operation: ExpectOpMeta {
201                    name: "TestJsonExpectOp",
202                    types: &[],
203                },
204            } if context == outer_context.to_static()
205        ));
206    }
207
208    #[test]
209    fn it_should_error_by_default_against_json_boolean() {
210        let mut outer_context = Context::new();
211        let received = json!(true);
212        let output = TestJsonExpectOp
213            .on_any(&mut outer_context, &received)
214            .unwrap_err();
215        assert!(matches!(
216            output,
217            ExpectOpError::UnsupportedOperation {
218                context,
219                received: ValueTypeObject(ValueObject::Boolean(BooleanObject(true))),
220                expected_operation: ExpectOpMeta {
221                    name: "TestJsonExpectOp",
222                    types: &[],
223                },
224            } if context == outer_context.to_static()
225        ));
226    }
227
228    #[test]
229    fn it_should_error_by_default_against_json_positive_integer() {
230        let mut outer_context = Context::new();
231        let received = json!(123);
232        let output = TestJsonExpectOp
233            .on_any(&mut outer_context, &received)
234            .unwrap_err();
235        assert!(matches!(
236            output,
237            ExpectOpError::UnsupportedOperation {
238                context,
239                received: ValueTypeObject(ValueObject::Integer(IntegerObject::Positive(123))),
240                expected_operation: ExpectOpMeta {
241                    name: "TestJsonExpectOp",
242                    types: &[],
243                },
244            } if context == outer_context.to_static()
245        ));
246    }
247
248    #[test]
249    fn it_should_error_by_default_against_json_negative_integer() {
250        let mut outer_context = Context::new();
251        let received = json!(-123);
252        let output = TestJsonExpectOp
253            .on_any(&mut outer_context, &received)
254            .unwrap_err();
255        assert!(matches!(
256            output,
257            ExpectOpError::UnsupportedOperation {
258                context,
259                received: ValueTypeObject(ValueObject::Integer(IntegerObject::Negative(-123))),
260                expected_operation: ExpectOpMeta {
261                    name: "TestJsonExpectOp",
262                    types: &[],
263                },
264            } if context == outer_context.to_static()
265        ));
266    }
267
268    #[test]
269    fn it_should_error_by_default_against_json_float() {
270        let mut outer_context = Context::new();
271        let received = json!(123.456);
272        let output = TestJsonExpectOp
273            .on_any(&mut outer_context, &received)
274            .unwrap_err();
275        assert!(matches!(
276            output,
277            ExpectOpError::UnsupportedOperation {
278                context,
279                received: ValueTypeObject(ValueObject::Float(FloatObject(123.456))),
280                expected_operation: ExpectOpMeta {
281                    name: "TestJsonExpectOp",
282                    types: &[],
283                },
284            } if context == outer_context.to_static()
285        ));
286    }
287
288    #[test]
289    fn it_should_error_by_default_against_json_string() {
290        let mut outer_context = Context::new();
291        let received = json!("🦊");
292        let output = TestJsonExpectOp
293            .on_any(&mut outer_context, &received)
294            .unwrap_err();
295        assert!(matches!(
296            output,
297            ExpectOpError::UnsupportedOperation {
298                context,
299                received,
300                expected_operation: ExpectOpMeta {
301                    name: "TestJsonExpectOp",
302                    types: &[],
303                },
304            } if context == outer_context.to_static()
305                && received == ValueTypeObject(ValueObject::String(StringObject("🦊".to_string())))
306        ));
307    }
308
309    #[test]
310    fn it_should_error_by_default_against_json_array() {
311        let mut outer_context = Context::new();
312        let received = json!([]);
313        let output = TestJsonExpectOp
314            .on_any(&mut outer_context, &received)
315            .unwrap_err();
316        assert!(matches!(
317            output,
318            ExpectOpError::UnsupportedOperation {
319                context,
320                received,
321                expected_operation: ExpectOpMeta {
322                    name: "TestJsonExpectOp",
323                    types: &[],
324                },
325            } if context == outer_context.to_static()
326                && received == ValueTypeObject(ValueObject::Array(ArrayObject(vec![])))
327        ));
328    }
329
330    #[test]
331    fn it_should_error_by_default_against_json_object() {
332        let mut outer_context = Context::new();
333        let received = json!({});
334        let output = TestJsonExpectOp
335            .on_any(&mut outer_context, &received)
336            .unwrap_err();
337        assert!(matches!(
338            output,
339            ExpectOpError::UnsupportedOperation {
340                context,
341                received,
342                expected_operation: ExpectOpMeta {
343                    name: "TestJsonExpectOp",
344                    types: &[],
345                },
346            } if context == outer_context.to_static()
347                && received == ValueTypeObject(ValueObject::Object(ObjectObject(Map::new())))
348        ));
349    }
350}