mod impls;
#[allow(clippy::wrong_self_convention)]
pub mod traits;
use crate::{
dsl::{Part, Predicate},
grillon::LogSettings,
};
use serde::Serialize;
use serde_json::{json, Value};
use std::any::Any;
use std::fmt::Debug;
use strum::Display;
pub mod types {
use http::{header::HeaderName, HeaderValue};
pub type Headers = Vec<(String, String)>;
pub type HeaderTupleVec = Vec<(HeaderName, HeaderValue)>;
pub type HeaderStrTupleVec = Vec<(&'static str, &'static str)>;
pub type Header = String;
}
#[derive(Serialize, Debug)]
#[serde(untagged)]
pub enum Hand<T>
where
T: Debug,
{
Left(T),
Right(T),
Compound(T, T),
Empty,
}
#[derive(Serialize, Debug)]
pub struct Assertion<T>
where
T: Debug + Serialize,
{
pub part: Part,
pub predicate: Predicate,
pub left: Hand<T>,
pub right: Hand<T>,
pub result: AssertionResult,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum UnprocessableReason {
InvalidJsonPath(String),
MissingJsonBody,
MissingHeader,
InvalidJsonSchema(String, String),
SerializationFailure(String),
InvalidHttpRequestHeaders(String),
InvalidHeaderValue(String),
InvalidRegex(String),
HttpRequestFailure(String),
Other(String),
}
impl std::fmt::Display for UnprocessableReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnprocessableReason::InvalidJsonPath(message) => {
write!(f, "Unprocessable json path: {message}")
}
UnprocessableReason::MissingJsonBody => {
write!(f, "Unprocessable json body: missing")
}
UnprocessableReason::MissingHeader => {
write!(f, "Unprocessable header: header key is missing")
}
UnprocessableReason::InvalidJsonSchema(schema, instance) => {
write!(f, "Invalid json schema: {schema} => {instance}")
}
UnprocessableReason::SerializationFailure(message) => {
write!(f, "Serialization failure: {message}")
}
UnprocessableReason::InvalidHttpRequestHeaders(details) => {
write!(f, "Invalid HTTP request headers: {details}")
}
UnprocessableReason::InvalidHeaderValue(details) => {
write!(f, "Invalid HTTP response header value: {details}")
}
UnprocessableReason::InvalidRegex(regex) => {
write!(f, "Invalid regex pattern: {regex}")
}
UnprocessableReason::HttpRequestFailure(details) => {
write!(f, "Http request failure: {details}")
}
UnprocessableReason::Other(message) => write!(f, "{message}"),
}
}
}
#[derive(Serialize, Display, Debug)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum AssertionResult {
Passed,
Failed,
NotYetStarted,
Unprocessable(UnprocessableReason),
}
pub struct AssertionLog(String);
impl AssertionLog {
pub fn new<T: Any + Debug + Serialize>(assertion: &Assertion<T>) -> Self {
if let AssertionResult::Unprocessable(reason) = &assertion.result {
return Self(format!("{reason}"));
}
match assertion.part {
Part::JsonPath => Self::jsonpath_log(assertion),
_ => Self::log(assertion),
}
}
fn log<T: Debug + Serialize>(assertion: &Assertion<T>) -> Self {
let predicate = &assertion.predicate;
let part = &assertion.part;
let left = match &assertion.left {
Hand::Left(left) => format!("{left:#?}"),
Hand::Compound(left, right) if part == &Part::StatusCode => {
format!("{left:#?} and {right:#?}")
}
_ => "Unexpected left hand in right hand".to_string(),
};
let right = match &assertion.right {
Hand::Right(right) => format!("{right:#?}"),
Hand::Compound(left, right) if part == &Part::StatusCode => {
format!("{left:#?} and {right:#?}")
}
_ => "Unexpected left hand in right hand".to_string(),
};
let result = &assertion.result;
let part = format!("part: {part}");
let message = match result {
AssertionResult::Passed => format!(
"result: {result}
{part}
{predicate}: {right}"
),
AssertionResult::Failed => format!(
"result: {result}
{part}
{predicate}: {right}
was: {left}"
),
AssertionResult::NotYetStarted => format!("Not yet started : {part}"),
AssertionResult::Unprocessable(reason) => format!("{reason}"),
};
Self(message)
}
fn jsonpath_log<T: Any + Debug + Serialize>(assertion: &Assertion<T>) -> Self {
let predicate = &assertion.predicate;
let part = &assertion.part;
let left_hand = match &assertion.left {
Hand::Compound(left, right) if part == &Part::JsonPath => (left, right),
_ => return Self("<unexpected left hand>".to_string()),
};
let right_hand = match &assertion.right {
Hand::Right(right) if part == &Part::JsonPath => right,
_ => return Self("<unexpected right hand>".to_string()),
};
let jsonpath = left_hand.0;
#[allow(trivial_casts)]
let jsonpath = match (jsonpath as &dyn Any).downcast_ref::<Value>() {
Some(Value::String(jsonpath_string)) => jsonpath_string.to_string(),
_ => format!("{jsonpath:?}"),
};
let jsonpath_value = left_hand.1;
let result = &assertion.result;
let part = format!("part: {part} '{jsonpath}'");
let message = match result {
AssertionResult::Passed => format!(
"result: {result}
{part}
{predicate}: {right_hand:#?}"
),
AssertionResult::Failed => format!(
"result: {result}
{part}
{predicate}: {right_hand:#?}
was: {jsonpath_value:#?}"
),
AssertionResult::NotYetStarted => format!("[Not yet started] {part}"),
AssertionResult::Unprocessable(reason) => format!("{reason}"),
};
Self(message)
}
}
impl<T> Assertion<T>
where
T: Debug + Serialize + 'static,
{
pub fn passed(&self) -> bool {
matches!(self.result, AssertionResult::Passed)
}
pub fn failed(&self) -> bool {
matches!(
self.result,
AssertionResult::Failed | AssertionResult::Unprocessable(_)
)
}
pub fn assert(self, log_settings: &LogSettings) -> Assertion<T> {
let message = self.log();
match log_settings {
LogSettings::StdOutput => println!("\n{message}"),
LogSettings::StdAssert => assert!(self.passed(), "\n\n{message}"),
LogSettings::JsonOutput => {
let json = serde_json::to_string(&json!(self))
.expect("Unexpected json failure: failed to serialize assertion");
println!("{json}");
}
}
self
}
fn log(&self) -> String {
AssertionLog::new(self).0
}
}
impl From<bool> for AssertionResult {
fn from(val: bool) -> Self {
if val {
return AssertionResult::Passed;
}
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::Compound(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);
}
}