stubr 0.6.2

Wiremock implemented in Rust
Documentation
use itertools::Itertools;
use serde_json::{Map, Value};

use crate::model::response::{
    template::{
        data::{HandlebarsData, RequestData},
        utils::TemplateExt,
        verify::Predictable,
        HandlebarTemplatable,
    },
    ResponseStub,
};
use crate::{StubrError, StubrResult};

use super::{
    super::super::{StdResponse, Verifier},
    JsonBodyTemplatingVerifier,
};

pub struct JsonObjectVerifier<'a> {
    pub actual: &'a Map<String, Value>,
    pub expected: &'a Map<String, Value>,
}

impl JsonObjectVerifier<'_> {
    fn to_bytes(&self, value: &Value) -> Option<Vec<u8>> {
        match value {
            Value::String(s) => Some(s.as_bytes().to_vec()),
            Value::Number(n) => Some(n.to_string().as_bytes().to_vec()),
            Value::Null => Some("null".to_string().as_bytes().to_vec()),
            Value::Bool(b) => Some(b.to_string().as_bytes().to_vec()),
            Value::Array(_) => None,
            Value::Object(_) => None,
        }
    }

    fn cast_to_value(&self, raw: &str) -> Value {
        if let Ok(i) = raw.parse::<i32>() {
            Value::from(i)
        } else if let Ok(b) = raw.parse::<bool>() {
            Value::from(b)
        } else if let Ok(f) = raw.parse::<f64>() {
            Value::from(f)
        } else if raw == "null" {
            Value::Null
        } else {
            Value::from(raw)
        }
    }
}

impl<'a> TryFrom<&'a JsonBodyTemplatingVerifier> for JsonObjectVerifier<'a> {
    type Error = StubrError;

    fn try_from(parent: &'a JsonBodyTemplatingVerifier) -> StubrResult<Self> {
        parent
            .actual
            .as_object()
            .zip(parent.expected.as_object())
            .map(|(actual, expected)| Self { actual, expected })
            .ok_or_else(|| StubrError::QuietError)
    }
}

impl Verifier<'_> for JsonObjectVerifier<'_> {
    fn verify(self, stub: &'_ ResponseStub, name: &'_ str, req: &'_ RequestData, resp: &'_ mut StdResponse) {
        let key_diff = self.expected.keys().filter(|k| !self.actual.keys().contains(k)).collect_vec();
        assert!(
            key_diff.is_empty(),
            "Verification failed for stub '{}'. Expected json fields '{:?}' were absent from response body",
            name,
            self.expected
                .iter()
                .filter(|(k, _)| key_diff.contains(k))
                .map(|(k, v)| (k, v.as_str().unwrap_or_default()))
                .collect_vec()
        );
        let actual = self
            .actual
            .iter()
            .filter(|(k, _)| self.expected.keys().contains(k))
            .sorted_by_key(|(k, _)| k.as_str());
        actual
            .zip(self.expected.iter().sorted_by_key(|(k, _)| k.as_str()))
            .for_each(|((_, va), (ke, ve))| {
                if let Some(expected) = ve.as_str().filter(|v| v.has_template_expressions()) {
                    let response = self.to_bytes(va);
                    let data = HandlebarsData {
                        request: req,
                        response: response.as_deref(),
                        is_verify: true,
                        stub_name: Some(name),
                    };
                    stub.body.register(expected, expected);
                    let render = stub.body.render(expected, &data).unwrap_or_default();
                    if expected.is_predictable() {
                        assert_eq!(
                            va,
                            &self.cast_to_value(&render),
                            "Verification failed for stub '{name}'. Expected json response body for field '{ke}' to be '{render}' but was '{va}'"
                        )
                    }
                } else {
                    JsonBodyTemplatingVerifier {
                        actual: va.clone(),
                        expected: ve.clone(),
                    }
                    .verify(stub, name, req, resp)
                }
            })
    }
}