grillon/assertion/
mod.rs

1//! Functionnality for asserting.
2//!
3//! This module contains a set of structures, types and implementations to
4//! create expressive assertions decoupled from the DSL. This is ideal for
5//! external implementations.
6//!
7//! This is not generally used by end users, instead the [`dsl`] module should
8//! provide the built-in functions served as part of the library.
9//!
10//! The left and right hands of an [`Assertion`] enforce the implementation of
11//! [`Debug`] and [`Serialize`]. This is because the library can produce
12//! different types of logs to standard output : human-readable
13//! (debuggable) and json formats.
14//!
15//! [`dsl`]: crate::dsl
16
17mod impls;
18#[allow(clippy::wrong_self_convention)]
19pub mod traits;
20
21use crate::{
22    dsl::{Part, Predicate},
23    grillon::LogSettings,
24};
25use serde::Serialize;
26use serde_json::{json, Value};
27use std::any::Any;
28use std::fmt::Debug;
29use strum::Display;
30
31/// Short-hand types and aliases used for assertions.
32pub mod types {
33    use http::{header::HeaderName, HeaderValue};
34
35    /// An alias to manipulate an internal representation of headers as tuples
36    /// of strings.
37    pub type Headers = Vec<(String, String)>;
38    /// An alias to manipulate an internal representation of headers as tuples
39    /// of [`HeaderName`] and [`HeaderValue`].
40    pub type HeaderTupleVec = Vec<(HeaderName, HeaderValue)>;
41    /// An alias to manipulate an internal representation of headers as tuples
42    /// of str.
43    pub type HeaderStrTupleVec = Vec<(&'static str, &'static str)>;
44    /// An alias to manipulate an internal representation of a header as a
45    /// `String`.
46    pub type Header = String;
47}
48
49/// Represents left or right hands in an [`Assertion`].
50#[derive(Serialize, Debug)]
51#[serde(untagged)]
52pub enum Hand<T>
53where
54    T: Debug,
55{
56    /// The left hand of the assertion.
57    Left(T),
58    /// The right hand of the assertion.
59    Right(T),
60    /// A hand composed of two elements.
61    Compound(T, T),
62    /// An empty hand
63    Empty,
64}
65
66/// The assertion encapsulating information about the [`Part`] under
67/// test, the [`Predicate`] used, the [`AssertionResult`] and the right and left
68/// [`Hand`]s.
69#[derive(Serialize, Debug)]
70pub struct Assertion<T>
71where
72    T: Debug + Serialize,
73{
74    /// The part under test.
75    pub part: Part,
76    /// The predicate applied in the test.
77    pub predicate: Predicate,
78    /// The left hand of the assertion.
79    pub left: Hand<T>,
80    /// The right hand of the assertion.
81    pub right: Hand<T>,
82    /// The assertion result.
83    pub result: AssertionResult,
84}
85
86/// Unprocessable event reason. This enum should
87/// be used when the assertion syntax is correct
88/// but the implementor is unable to process the
89/// assertion due to an unexpected event.
90///
91/// For example, when an implementation asserts
92/// that a word exists in a file but there is no
93/// read access. In this case, the assertion
94/// fails not because the word is missing, but
95/// because the file content cannot be
96/// processed.
97#[derive(Serialize, Debug)]
98#[serde(rename_all = "snake_case")]
99pub enum UnprocessableReason {
100    /// Unprocessable json path with the string representation of the path.
101    InvalidJsonPath(String),
102    /// Unprocessable json body because it's missing.
103    MissingJsonBody,
104    /// Unprocessable header value because the correspond header key is missing.
105    MissingHeader,
106    /// Unprocessable json schema.
107    InvalidJsonSchema(String, String),
108    /// Serialization failure.
109    SerializationFailure(String),
110    /// Invalid HTTP request headers.
111    InvalidHttpRequestHeaders(String),
112    /// Invalid HTTP header value.
113    InvalidHeaderValue(String),
114    /// Invalid regex pattern.
115    InvalidRegex(String),
116    /// If the HTTP request results in an error while sending request, redirect
117    /// loop was detected or redirect limit was exhausted.
118    HttpRequestFailure(String),
119    /// Unprocessable entity.
120    Other(String),
121}
122
123// Strum cannot be used here since sum type fields are
124// not supported yet just like positional arguments for
125// tuple variants.
126impl std::fmt::Display for UnprocessableReason {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            UnprocessableReason::InvalidJsonPath(message) => {
130                write!(f, "Unprocessable json path: {message}")
131            }
132            UnprocessableReason::MissingJsonBody => {
133                write!(f, "Unprocessable json body: missing")
134            }
135            UnprocessableReason::MissingHeader => {
136                write!(f, "Unprocessable header: header key is missing")
137            }
138            UnprocessableReason::InvalidJsonSchema(schema, instance) => {
139                write!(f, "Invalid json schema: {schema} => {instance}")
140            }
141            UnprocessableReason::SerializationFailure(message) => {
142                write!(f, "Serialization failure: {message}")
143            }
144            UnprocessableReason::InvalidHttpRequestHeaders(details) => {
145                write!(f, "Invalid HTTP request headers: {details}")
146            }
147            UnprocessableReason::InvalidHeaderValue(details) => {
148                write!(f, "Invalid HTTP response header value: {details}")
149            }
150            UnprocessableReason::InvalidRegex(regex) => {
151                write!(f, "Invalid regex pattern: {regex}")
152            }
153            UnprocessableReason::HttpRequestFailure(details) => {
154                write!(f, "Http request failure: {details}")
155            }
156            UnprocessableReason::Other(message) => write!(f, "{message}"),
157        }
158    }
159}
160
161/// The assertion's result.
162#[derive(Serialize, Display, Debug)]
163#[serde(rename_all = "snake_case")]
164#[strum(serialize_all = "snake_case")]
165pub enum AssertionResult {
166    /// When the assertion passed.
167    Passed,
168    /// When the assertion failed.
169    Failed,
170    /// When the assertion didn't start.
171    NotYetStarted,
172    /// When the assertion is correct but cannot be processed
173    /// due to an unexpected reason.
174    Unprocessable(UnprocessableReason),
175}
176
177/// Represents an assertion log.
178///
179/// A log is built according to this scheme:
180/// - part: \<part\> \[compound_hand_part]
181/// - \<predicate\>: \<expected_value\>
182/// - was: \<found_value\> (only in case of failure)
183///
184/// The log will be displayed for both [`LogSettings::StdOutput`] and
185/// [`LogSettings::StdAssert`]
186pub struct AssertionLog(String);
187
188impl AssertionLog {
189    /// Builds the assertion message based on the [`Predicate`], the [`Part`]
190    /// and the [`AssertionResult`].
191    pub fn new<T: Any + Debug + Serialize>(assertion: &Assertion<T>) -> Self {
192        if let AssertionResult::Unprocessable(reason) = &assertion.result {
193            return Self(format!("{reason}"));
194        }
195
196        match assertion.part {
197            Part::JsonPath => Self::jsonpath_log(assertion),
198            _ => Self::log(assertion),
199        }
200    }
201
202    fn log<T: Debug + Serialize>(assertion: &Assertion<T>) -> Self {
203        let predicate = &assertion.predicate;
204        let part = &assertion.part;
205
206        let left = match &assertion.left {
207            Hand::Left(left) => format!("{left:#?}"),
208            Hand::Compound(left, right) if part == &Part::StatusCode => {
209                format!("{left:#?} and {right:#?}")
210            }
211            _ => "Unexpected left hand in right hand".to_string(),
212        };
213        let right = match &assertion.right {
214            Hand::Right(right) => format!("{right:#?}"),
215            Hand::Compound(left, right) if part == &Part::StatusCode => {
216                format!("{left:#?} and {right:#?}")
217            }
218            _ => "Unexpected left hand in right hand".to_string(),
219        };
220
221        let result = &assertion.result;
222        let part = format!("part: {part}");
223        let message = match result {
224            AssertionResult::Passed => format!(
225                "result: {result}
226{part}
227{predicate}: {right}"
228            ),
229            AssertionResult::Failed => format!(
230                "result: {result}
231{part}
232{predicate}: {right}
233was: {left}"
234            ),
235            AssertionResult::NotYetStarted => format!("Not yet started : {part}"),
236            AssertionResult::Unprocessable(reason) => format!("{reason}"),
237        };
238
239        Self(message)
240    }
241
242    fn jsonpath_log<T: Any + Debug + Serialize>(assertion: &Assertion<T>) -> Self {
243        let predicate = &assertion.predicate;
244        let part = &assertion.part;
245
246        let left_hand = match &assertion.left {
247            Hand::Compound(left, right) if part == &Part::JsonPath => (left, right),
248            _ => return Self("<unexpected left hand>".to_string()),
249        };
250        let right_hand = match &assertion.right {
251            Hand::Right(right) if part == &Part::JsonPath => right,
252            _ => return Self("<unexpected right hand>".to_string()),
253        };
254
255        let jsonpath = left_hand.0;
256        #[allow(trivial_casts)]
257        let jsonpath = match (jsonpath as &dyn Any).downcast_ref::<Value>() {
258            Some(Value::String(jsonpath_string)) => jsonpath_string.to_string(),
259            _ => format!("{jsonpath:?}"),
260        };
261
262        let jsonpath_value = left_hand.1;
263
264        let result = &assertion.result;
265        let part = format!("part: {part} '{jsonpath}'");
266        let message = match result {
267            AssertionResult::Passed => format!(
268                "result: {result}
269{part}
270{predicate}: {right_hand:#?}"
271            ),
272            AssertionResult::Failed => format!(
273                "result: {result}
274{part}
275{predicate}: {right_hand:#?}
276was: {jsonpath_value:#?}"
277            ),
278            AssertionResult::NotYetStarted => format!("[Not yet started] {part}"),
279            AssertionResult::Unprocessable(reason) => format!("{reason}"),
280        };
281
282        Self(message)
283    }
284}
285
286impl<T> Assertion<T>
287where
288    T: Debug + Serialize + 'static,
289{
290    /// Returns if the assertion passed.
291    pub fn passed(&self) -> bool {
292        matches!(self.result, AssertionResult::Passed)
293    }
294
295    /// Returns if the assertion failed.
296    pub fn failed(&self) -> bool {
297        matches!(
298            self.result,
299            AssertionResult::Failed | AssertionResult::Unprocessable(_)
300        )
301    }
302
303    /// Runs the assertion and produce the the result results with the given
304    /// [`LogSettings`].
305    pub fn assert(self, log_settings: &LogSettings) -> Assertion<T> {
306        let message = self.log();
307        match log_settings {
308            LogSettings::StdOutput => println!("\n{message}"),
309            LogSettings::StdAssert => assert!(self.passed(), "\n\n{message}"),
310            LogSettings::JsonOutput => {
311                let json = serde_json::to_string(&json!(self))
312                    .expect("Unexpected json failure: failed to serialize assertion");
313                println!("{json}");
314            }
315        }
316
317        self
318    }
319
320    fn log(&self) -> String {
321        AssertionLog::new(self).0
322    }
323}
324
325impl From<bool> for AssertionResult {
326    fn from(val: bool) -> Self {
327        if val {
328            return AssertionResult::Passed;
329        }
330
331        AssertionResult::Failed
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::{AssertionResult, Hand};
338    use crate::dsl::Predicate::{Between, LessThan};
339    use crate::{assertion::Assertion, dsl::Part};
340    use serde_json::json;
341
342    #[test]
343    fn it_should_serialize_status_code() {
344        let assertion: Assertion<u16> = Assertion {
345            part: Part::StatusCode,
346            predicate: Between,
347            left: Hand::Left(200),
348            right: Hand::Compound(200, 299),
349            result: AssertionResult::Passed,
350        };
351
352        let expected_json = json!({
353            "part": "status code",
354            "predicate": "should be between",
355            "left": 200,
356            "right": [200, 299],
357            "result": "passed"
358        });
359
360        assert_eq!(json!(assertion), expected_json);
361    }
362
363    #[test]
364    fn it_should_serialize_failed_response_time() {
365        let assertion: Assertion<u64> = Assertion {
366            part: Part::ResponseTime,
367            predicate: LessThan,
368            left: Hand::Left(300),
369            right: Hand::Right(248),
370            result: AssertionResult::Failed,
371        };
372
373        let expected_json = json!({
374            "part": "response time",
375            "predicate": "should be less than",
376            "left": 300,
377            "right": 248,
378            "result": "failed"
379        });
380
381        assert_eq!(json!(assertion), expected_json);
382    }
383}