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 = 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::() { 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>, timezone: Option, } struct Now; impl rets_expression::function::Function for Now { fn evaluate<'json>( &self, context: rets_expression::function::FunctionContext<'_, TimeContext>, _input: Vec>, ) -> Result, 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 for Today { fn evaluate<'json>( &self, context: rets_expression::function::FunctionContext<'_, TimeContext>, _input: Vec>, ) -> Result, 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, } 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, now: Option>, timezone: Option, } #[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, }