checkmate-cli 0.4.1

Checkmate - API Testing Framework CLI
//! Assertion evaluator using Clove queries

use clove_lang::{Evaluator, Lexer, Parser};
use regex::Regex;

use super::spec::{Assertion, AssertionValue};
use super::result::AssertionResult;
use super::RunnerError;

/// Evaluator for test assertions
pub struct AssertionEvaluator {
    current_response: Option<serde_json::Value>,
    previous_response: Option<serde_json::Value>,
}

impl AssertionEvaluator {
    pub fn new() -> Self {
        Self {
            current_response: None,
            previous_response: None,
        }
    }

    /// Set the current response for evaluation
    pub fn set_current(&mut self, response: serde_json::Value) {
        // Shift current to previous before setting new current
        self.previous_response = self.current_response.take();
        self.current_response = Some(response);
    }

    /// Check if previous response is available
    pub fn has_previous(&self) -> bool {
        self.previous_response.is_some()
    }

    /// Evaluate an assertion against the current response
    pub fn evaluate(&self, assertion: &Assertion) -> AssertionResult {
        let Some(ref current) = self.current_response else {
            return AssertionResult::error("No current response to evaluate");
        };

        // If no query specified, can't evaluate
        let Some(ref query) = assertion.query else {
            return AssertionResult::error("Assertion has no query");
        };

        // Evaluate the main query
        let actual = match self.evaluate_query(query, current) {
            Ok(v) => v,
            Err(e) => return AssertionResult::error(&format!("Query evaluation failed: {}", e)),
        };

        // Handle different assertion types
        if let Some(ref expected) = assertion.expect {
            return self.check_exact_match(&actual, expected, assertion);
        }

        if let Some(ref expected) = assertion.expect_gt {
            return self.check_comparison(&actual, expected, ">", assertion);
        }

        if let Some(ref expected) = assertion.expect_lt {
            return self.check_comparison(&actual, expected, "<", assertion);
        }

        if let Some(ref expected) = assertion.expect_gte {
            return self.check_comparison(&actual, expected, ">=", assertion);
        }

        if let Some(ref expected) = assertion.expect_lte {
            return self.check_comparison(&actual, expected, "<=", assertion);
        }

        if let Some(ref expected_type) = assertion.expect_type {
            return self.check_type(&actual, expected_type, assertion);
        }

        if let Some(ref pattern) = assertion.expect_match {
            return self.check_regex(&actual, pattern, assertion);
        }

        AssertionResult::error("Assertion has no expectation specified")
    }

    /// Evaluate a clove query, handling @prev scope references
    fn evaluate_query(&self, query: &str, data: &serde_json::Value) -> Result<serde_json::Value, RunnerError> {
        // Check if query references @prev
        if query.contains("@prev") {
            return self.evaluate_with_prev(query, data);
        }

        // Standard query evaluation
        let lexer = Lexer::new(query);
        let mut parser = Parser::new(lexer)
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;
        let parsed = parser.parse()
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

        let clove_data = clove_lang::json_to_clove(data.clone());

        let result = Evaluator::new()
            .eval_expression(&parsed, clove_data)
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

        Ok(clove_lang::clove_to_json(result))
    }

    /// Evaluate a query that references @prev
    fn evaluate_with_prev(&self, query: &str, current_data: &serde_json::Value) -> Result<serde_json::Value, RunnerError> {
        let Some(ref prev_data) = self.previous_response else {
            return Err(RunnerError::CloveError("@prev referenced but no previous response available".to_string()));
        };

        // Replace @prev[...] with a temporary marker, evaluate separately
        // This is a simplified approach - for complex cases we'd need proper scope injection

        // If the query is purely @prev[...], evaluate against previous response
        if query.starts_with("@prev") {
            let adjusted_query = query.replacen("@prev", "$", 1);
            let lexer = Lexer::new(&adjusted_query);
            let mut parser = Parser::new(lexer)
                .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;
            let parsed = parser.parse()
                .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

            let clove_data = clove_lang::json_to_clove(prev_data.clone());

            let result = Evaluator::new()
                .eval_expression(&parsed, clove_data)
                .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

            return Ok(clove_lang::clove_to_json(result));
        }

        // For queries like "$[x] > @prev[x]", we need to handle comparison separately
        // This will be handled by the comparison check methods
        let lexer = Lexer::new(query);
        let mut parser = Parser::new(lexer)
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;
        let parsed = parser.parse()
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

        let clove_data = clove_lang::json_to_clove(current_data.clone());

        let result = Evaluator::new()
            .eval_expression(&parsed, clove_data)
            .map_err(|e| RunnerError::CloveError(format!("{}", e)))?;

        Ok(clove_lang::clove_to_json(result))
    }

    fn check_exact_match(&self, actual: &serde_json::Value, expected: &serde_json::Value, assertion: &Assertion) -> AssertionResult {
        if actual == expected {
            AssertionResult::passed()
        } else {
            AssertionResult::failed(
                assertion.message.as_deref().unwrap_or("Value mismatch"),
                format!("{}", expected),
                format!("{}", actual),
            )
        }
    }

    fn check_comparison(&self, actual: &serde_json::Value, expected: &AssertionValue, op: &str, assertion: &Assertion) -> AssertionResult {
        let expected_val = match self.resolve_assertion_value(expected) {
            Ok(v) => v,
            Err(e) => return AssertionResult::error(&e.to_string()),
        };

        let actual_num = match self.to_number(actual) {
            Some(n) => n,
            None => return AssertionResult::error(&format!("Cannot compare non-numeric value: {}", actual)),
        };

        let expected_num = match self.to_number(&expected_val) {
            Some(n) => n,
            None => return AssertionResult::error(&format!("Cannot compare to non-numeric value: {}", expected_val)),
        };

        let passed = match op {
            ">" => actual_num > expected_num,
            "<" => actual_num < expected_num,
            ">=" => actual_num >= expected_num,
            "<=" => actual_num <= expected_num,
            _ => return AssertionResult::error(&format!("Unknown comparison operator: {}", op)),
        };

        if passed {
            AssertionResult::passed()
        } else {
            AssertionResult::failed(
                assertion.message.as_deref().unwrap_or(&format!("Expected {} {}", op, expected_num)),
                format!("{} {}", op, expected_num),
                format!("{}", actual_num),
            )
        }
    }

    fn check_type(&self, actual: &serde_json::Value, expected_type: &str, assertion: &Assertion) -> AssertionResult {
        let actual_type = match actual {
            serde_json::Value::Null => "null",
            serde_json::Value::Bool(_) => "boolean",
            serde_json::Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
            serde_json::Value::Number(_) => "number",
            serde_json::Value::String(_) => "string",
            serde_json::Value::Array(_) => "array",
            serde_json::Value::Object(_) => "object",
        };

        // Handle "number" matching both integer and float
        let matches = actual_type == expected_type
            || (expected_type == "number" && actual_type == "integer");

        if matches {
            AssertionResult::passed()
        } else {
            AssertionResult::failed(
                assertion.message.as_deref().unwrap_or("Type mismatch"),
                expected_type.to_string(),
                actual_type.to_string(),
            )
        }
    }

    fn check_regex(&self, actual: &serde_json::Value, pattern: &str, assertion: &Assertion) -> AssertionResult {
        let actual_str = match actual {
            serde_json::Value::String(s) => s.as_str(),
            _ => return AssertionResult::error("Cannot apply regex to non-string value"),
        };

        let re = match Regex::new(pattern) {
            Ok(r) => r,
            Err(e) => return AssertionResult::error(&format!("Invalid regex: {}", e)),
        };

        if re.is_match(actual_str) {
            AssertionResult::passed()
        } else {
            AssertionResult::failed(
                assertion.message.as_deref().unwrap_or("Pattern did not match"),
                format!("matches /{}/", pattern),
                actual_str.to_string(),
            )
        }
    }

    fn resolve_assertion_value(&self, value: &AssertionValue) -> Result<serde_json::Value, RunnerError> {
        match value {
            AssertionValue::Number(n) => Ok(serde_json::json!(n)),
            AssertionValue::Integer(n) => Ok(serde_json::json!(n)),
            AssertionValue::String(s) => {
                // Check if it's a query reference
                if s.starts_with('$') || s.starts_with('@') {
                    let data = if s.starts_with("@prev") {
                        self.previous_response.as_ref()
                            .ok_or_else(|| RunnerError::CloveError("@prev referenced but no previous response".to_string()))?
                    } else {
                        self.current_response.as_ref()
                            .ok_or_else(|| RunnerError::CloveError("No current response".to_string()))?
                    };

                    // For @prev queries, replace @prev with $ before parsing
                    let query = if s.starts_with("@prev") {
                        s.replacen("@prev", "$", 1)
                    } else {
                        s.clone()
                    };

                    self.evaluate_query(&query, data)
                } else {
                    Ok(serde_json::json!(s))
                }
            }
        }
    }

    fn to_number(&self, value: &serde_json::Value) -> Option<f64> {
        match value {
            serde_json::Value::Number(n) => n.as_f64(),
            _ => None,
        }
    }
}

impl Default for AssertionEvaluator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_exact_match() {
        let mut eval = AssertionEvaluator::new();
        eval.set_current(serde_json::json!({"status": "ok"}));

        let assertion = Assertion {
            query: Some("$[status]".to_string()),
            expect: Some(serde_json::json!("ok")),
            ..Default::default()
        };

        let result = eval.evaluate(&assertion);
        assert!(result.passed);
    }

    #[test]
    fn test_type_check() {
        let mut eval = AssertionEvaluator::new();
        eval.set_current(serde_json::json!({"count": 42}));

        let assertion = Assertion {
            query: Some("$[count]".to_string()),
            expect_type: Some("integer".to_string()),
            ..Default::default()
        };

        let result = eval.evaluate(&assertion);
        assert!(result.passed);
    }

    #[test]
    fn test_comparison_gt() {
        let mut eval = AssertionEvaluator::new();
        eval.set_current(serde_json::json!({"count": 10}));

        let assertion = Assertion {
            query: Some("$[count]".to_string()),
            expect_gt: Some(AssertionValue::Integer(5)),
            ..Default::default()
        };

        let result = eval.evaluate(&assertion);
        assert!(result.passed);
    }

    #[test]
    fn test_prev_comparison() {
        let mut eval = AssertionEvaluator::new();
        eval.set_current(serde_json::json!({"count": 5}));
        eval.set_current(serde_json::json!({"count": 10}));

        let assertion = Assertion {
            query: Some("$[count]".to_string()),
            expect_gt: Some(AssertionValue::String("@prev[count]".to_string())),
            ..Default::default()
        };

        let result = eval.evaluate(&assertion);
        assert!(result.passed, "Expected 10 > 5, but got: {:?}", result);
    }
}

impl Default for Assertion {
    fn default() -> Self {
        Self {
            query: None,
            expect: None,
            expect_gt: None,
            expect_lt: None,
            expect_gte: None,
            expect_lte: None,
            expect_type: None,
            expect_match: None,
            message: None,
            scope: None,
        }
    }
}