rets_expression 0.2.3

Implementation of RETS Validation Expressions from RESO RCP19
Documentation
use std::borrow::Cow;

use chrono::{DateTime, Utc};
use rets_expression::{Engine, EvaluateContext, Expression};
use serde_json::json;

fn main() {
    let mut tests_found = 0;
    let mut tests_passed = 0;

    let engine = Engine::default()
        .with_function("NOW", Box::new(Now))
        .with_function("TODAY", Box::new(Today));

    let iter = std::fs::read_dir("compliance-tests/tests").unwrap();
    for result in iter {
        let file = result.unwrap();
        let filename = file
            .file_name()
            .into_string()
            .expect("Expected utf8 filename");

        if !filename.ends_with(".json") {
            continue;
        }

        let contents = std::fs::read(file.path()).unwrap();
        let expression_tests: Vec<ExpressionTest> = serde_json::from_slice(&contents).unwrap();

        for test in expression_tests {
            for check in &test.checks {
                tests_found += 1;
                let name = format!(
                    "tests/expression-tests/{} :: {} :: {}",
                    filename, test.name, check.expression
                );
                let expression = match check.expression.parse::<Expression>() {
                    Ok(expression) => expression,
                    Err(err) => {
                        if let Some(expected) = check.result.as_expected() {
                            println!("NOK {name}");
                            println!("  expected: {}", serde_json::to_string(expected).unwrap());
                            println!("  error: {err}");
                        } else {
                            tests_passed += 1;
                            println!("OK {name}");
                        }
                        continue;
                    }
                };
                let context = EvaluateContext::new_with_state(
                    &engine,
                    &test.context.value,
                    test.time_context(),
                )
                .set_previous(test.context.previous_value.as_ref());
                let actual = match expression.apply(context) {
                    Ok(result) => result,
                    Err(err) => {
                        if let Some(expected) = check.result.as_expected() {
                            println!("NOK {name}");
                            println!("  expected: {}", serde_json::to_string(expected).unwrap());
                            println!("  error: {err:?}");
                        } else {
                            tests_passed += 1;
                            println!("OK {name}");
                        }
                        continue;
                    }
                };

                if let Some(expected) = check.result.as_expected() {
                    if actual.as_ref() != expected {
                        println!("NOK {name}");
                        println!("  expected: {}", serde_json::to_string(expected).unwrap());
                        println!("  actual: {}", serde_json::to_string(&actual).unwrap());
                    } else {
                        tests_passed += 1;
                        println!("OK {name}");
                    }
                } else {
                    println!("NOK {name}");
                    println!("  expected an error");
                    println!("  actual: {}", serde_json::to_string(&actual).unwrap());
                }
            }
        }
    }

    println!("Passed {tests_passed} of {tests_found} tests");
    if tests_passed < tests_found {
        std::process::exit(1)
    }
}

struct TimeContext {
    now: Option<DateTime<Utc>>,
    timezone: Option<chrono_tz::Tz>,
}

struct Now;

impl rets_expression::function::Function<TimeContext> for Now {
    fn evaluate<'json>(
        &self,
        context: rets_expression::function::FunctionContext<'_, TimeContext>,
        _input: Vec<Cow<'json, serde_json::Value>>,
    ) -> Result<Cow<'json, serde_json::Value>, rets_expression::function::FunctionError> {
        let now = context
            .state()
            .now
            .ok_or(rets_expression::function::FunctionError::InvalidType)?;

        Ok(Cow::Owned(json!(
            now.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
        )))
    }
}

struct Today;

impl rets_expression::function::Function<TimeContext> for Today {
    fn evaluate<'json>(
        &self,
        context: rets_expression::function::FunctionContext<'_, TimeContext>,
        _input: Vec<Cow<'json, serde_json::Value>>,
    ) -> Result<Cow<'json, serde_json::Value>, rets_expression::function::FunctionError> {
        let now = context
            .state()
            .now
            .ok_or(rets_expression::function::FunctionError::InvalidType)?;
        let tz = context
            .state()
            .timezone
            .ok_or(rets_expression::function::FunctionError::InvalidType)?;

        let now = now.with_timezone(&tz);
        let value = now.format("%Y-%m-%d").to_string();

        Ok(Cow::Owned(json!(value)))
    }
}

#[derive(serde::Deserialize)]
struct ExpressionTest {
    name: String,
    context: Context,
    checks: Vec<Check>,
}

impl ExpressionTest {
    pub fn time_context(&self) -> TimeContext {
        TimeContext {
            now: self.context.now,
            timezone: self.context.timezone,
        }
    }
}

#[derive(serde::Deserialize)]
struct Context {
    value: serde_json::Value,
    #[serde(rename = "previousValue")]
    previous_value: Option<serde_json::Value>,
    now: Option<DateTime<Utc>>,
    timezone: Option<chrono_tz::Tz>,
}

#[derive(serde::Deserialize)]
struct Check {
    #[serde(rename = "expr")]
    expression: String,
    #[serde(flatten)]
    result: CheckResult,
}

#[derive(serde::Deserialize)]
#[serde(untagged)]
enum CheckResult {
    Expected(CheckResultExpected),
    Error(CheckResultError),
}

impl CheckResult {
    fn as_expected(&self) -> Option<&serde_json::Value> {
        match self {
            CheckResult::Expected(e) => Some(&e.expected),
            CheckResult::Error(_) => None,
        }
    }
}

#[derive(serde::Deserialize)]
struct CheckResultExpected {
    expected: serde_json::Value,
}

#[derive(serde::Deserialize)]
struct CheckResultError {
    #[allow(dead_code)]
    error: bool,
}