1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! Functionnality for asserting.
//!
//! This module contains a set of structures, types and implementations to
//! create expressive assertions decoupled from the DSL. This is ideal for
//! external implementations.
//!
//! This is not generally used by end users, instead the [`dsl`] module should
//! provide the built-in functions served as part of the library.
//!
//! The left and right hands of an [`Assertion`] enforce the implementation of
//! [`Debug`] and [`Serialize`]. This is because the library can produce
//! different types of logs to standard output : human-readable
//! (debuggable) and json formats.
//!
//! [`dsl`]: crate::dsl

mod impls;
#[allow(clippy::wrong_self_convention)]
pub mod traits;

use crate::{
    dsl::{Part, Predicate},
    grillon::LogSettings,
};
use serde::Serialize;
use serde_json::json;
use std::fmt::Debug;

/// Short-hand types and aliases used for assertions.
pub mod types {
    use http::{header::HeaderName, HeaderValue};

    /// An alias to manipulate an internal representation of headers as tuples
    /// of strings.
    pub type Headers = Vec<(String, String)>;
    /// An alias to manipulate an internal representation of headers as tuples
    /// of [`HeaderName`] and [`HeaderValue`].
    pub type HeaderTupleVec = Vec<(HeaderName, HeaderValue)>;
}

/// Represents left or right hands in an [`Assertion`].
#[derive(Serialize)]
#[serde(untagged)]
pub enum Hand<T>
where
    T: Debug,
{
    /// The left hand of the assertion.
    Left(T),
    /// The right hand of the assertion.
    Right(T),
    /// A more complex hand made of a range that can be left or right.
    Range(T, T),
}

/// The assertion encapsulating information about the [`Part`] under
/// test, the [`Predicate`] used, the [`AssertionResult`] and the right and left
/// [`Hand`]s.
#[derive(Serialize)]
pub struct Assertion<T>
where
    T: Debug + Serialize,
{
    /// The part under test.
    pub part: Part,
    /// The predicate applied in the test.
    pub predicate: Predicate,
    /// The left hand of the assertion.
    pub left: Hand<T>,
    /// The right hand of the assertion.
    pub right: Hand<T>,
    /// The assertion result.
    pub result: AssertionResult,
}

/// The assertion's result.
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssertionResult {
    /// When the assertion passed.
    Passed,
    /// When the assertion failed.
    Failed,
    /// When the assertion didn't start.
    NotYetStarted,
}

impl<T> Assertion<T>
where
    T: Debug + Serialize,
{
    /// Returns if the assertion passed.
    pub fn passed(&self) -> bool {
        match self.result {
            AssertionResult::Passed => true,
            AssertionResult::Failed | AssertionResult::NotYetStarted => false,
        }
    }

    /// Returns if the assertion failed.
    pub fn failed(&self) -> bool {
        match self.result {
            AssertionResult::Failed => true,
            AssertionResult::Passed | AssertionResult::NotYetStarted => false,
        }
    }

    /// Runs the assertion and produce the the result results with the given
    /// [`LogSettings`].
    pub fn assert(self, log_settings: &LogSettings) -> Assertion<T> {
        let message = self.message();
        match log_settings {
            LogSettings::StdOut => println!("{message}"),
            LogSettings::StdAssert => assert!(self.passed(), "{}", message),
            LogSettings::Json => {
                let json = serde_json::to_string(&json!(self))
                    .expect("Unexpected json failure: failed to serialize assertion");
                println!("{json}");
            }
        }

        self
    }

    /// Builds the assertion message based on the [`Predicate`], the [`Part`]
    /// and the [`AssertionResult`].
    fn message(&self) -> String {
        let result = &self.result;

        let predicate = &self.predicate;
        let part = &self.part;
        let left = match &self.left {
            Hand::Left(left) => format!("{left:#?}"),
            Hand::Range(min, max) => format!("{min:#?} and {max:#?}"),
            Hand::Right(_) => "".to_string(),
        };
        let right = match &self.right {
            Hand::Right(right) => format!("{right:#?}"),
            Hand::Range(min, max) => format!("{min:#?} and {max:#?}"),
            Hand::Left(_) => "".to_string(),
        };

        // The base message is built as a passing case.
        let message = match part {
            Part::Empty => format!("{left} {predicate} {right}"),
            _ => format!("{part} {predicate} {right}"),
        };

        match result {
            AssertionResult::Passed => message,
            AssertionResult::Failed => format!("{message}. Found {left}"),
            AssertionResult::NotYetStarted => format!("Not yet started : {message}"),
        }
    }
}

impl From<bool> for AssertionResult {
    fn from(val: bool) -> Self {
        if val {
            AssertionResult::Passed
        } else {
            AssertionResult::Failed
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{AssertionResult, Hand};
    use crate::dsl::Predicate::{Between, LessThan};
    use crate::{assertion::Assertion, dsl::Part};
    use serde_json::json;

    #[test]
    fn it_should_serialize_status_code() {
        let assertion: Assertion<u16> = Assertion {
            part: Part::StatusCode,
            predicate: Between,
            left: Hand::Left(200),
            right: Hand::Range(200, 299),
            result: AssertionResult::Passed,
        };

        let expected_json = json!({
            "part": "status code",
            "predicate": "should be between",
            "left": 200,
            "right": [200, 299],
            "result": "passed"
        });

        assert_eq!(json!(assertion), expected_json);
    }

    #[test]
    fn it_should_serialize_failed_response_time() {
        let assertion: Assertion<u64> = Assertion {
            part: Part::ResponseTime,
            predicate: LessThan,
            left: Hand::Left(300),
            right: Hand::Right(248),
            result: AssertionResult::Failed,
        };

        let expected_json = json!({
            "part": "response time",
            "predicate": "should be less than",
            "left": 300,
            "right": 248,
            "result": "failed"
        });

        assert_eq!(json!(assertion), expected_json);
    }
}