1mod 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
31pub mod types {
33 use http::{header::HeaderName, HeaderValue};
34
35 pub type Headers = Vec<(String, String)>;
38 pub type HeaderTupleVec = Vec<(HeaderName, HeaderValue)>;
41 pub type HeaderStrTupleVec = Vec<(&'static str, &'static str)>;
44 pub type Header = String;
47}
48
49#[derive(Serialize, Debug)]
51#[serde(untagged)]
52pub enum Hand<T>
53where
54 T: Debug,
55{
56 Left(T),
58 Right(T),
60 Compound(T, T),
62 Empty,
64}
65
66#[derive(Serialize, Debug)]
70pub struct Assertion<T>
71where
72 T: Debug + Serialize,
73{
74 pub part: Part,
76 pub predicate: Predicate,
78 pub left: Hand<T>,
80 pub right: Hand<T>,
82 pub result: AssertionResult,
84}
85
86#[derive(Serialize, Debug)]
98#[serde(rename_all = "snake_case")]
99pub enum UnprocessableReason {
100 InvalidJsonPath(String),
102 MissingJsonBody,
104 MissingHeader,
106 InvalidJsonSchema(String, String),
108 SerializationFailure(String),
110 InvalidHttpRequestHeaders(String),
112 InvalidHeaderValue(String),
114 InvalidRegex(String),
116 HttpRequestFailure(String),
119 Other(String),
121}
122
123impl 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#[derive(Serialize, Display, Debug)]
163#[serde(rename_all = "snake_case")]
164#[strum(serialize_all = "snake_case")]
165pub enum AssertionResult {
166 Passed,
168 Failed,
170 NotYetStarted,
172 Unprocessable(UnprocessableReason),
175}
176
177pub struct AssertionLog(String);
187
188impl AssertionLog {
189 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 pub fn passed(&self) -> bool {
292 matches!(self.result, AssertionResult::Passed)
293 }
294
295 pub fn failed(&self) -> bool {
297 matches!(
298 self.result,
299 AssertionResult::Failed | AssertionResult::Unprocessable(_)
300 )
301 }
302
303 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}