use std::path::{Path, PathBuf};
use std::process::exit;
use behavior_contracts::canonical::{
canonical_json, canonical_value, py_float_repr, CanonicalFailure,
};
use behavior_contracts::codec::{decode_value, encode_value};
use behavior_contracts::expr::{evaluate as evaluate_expression, ExprFailure};
use behavior_contracts::plan::{
run_plan, ExecOutcome, ExecutionPlanSpec, OpSpec, PlanFailure, RelationKind,
};
use behavior_contracts::template::{render_template, TemplateFailure};
use behavior_contracts::value::{deep_equals, Value};
use behavior_contracts::SpecVersions;
use serde_json::Value as J;
fn vectors_dir() -> PathBuf {
if let Ok(env) = std::env::var("DSL_CONTRACTS_VECTORS") {
return PathBuf::from(env);
}
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest)
.parent()
.unwrap()
.join("conformance")
.join("vectors")
}
fn load_json(dir: &Path, file: &str) -> J {
let raw = std::fs::read_to_string(dir.join(file))
.unwrap_or_else(|e| panic!("cannot read {file}: {e}"));
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("cannot parse {file}: {e}"))
}
struct Tally {
passed: u32,
failed: u32,
}
fn line(ok: bool, name: &str, detail: &str) {
if ok {
println!(" \u{2713} {name}");
} else {
println!(" \u{2717} {name}");
if !detail.is_empty() {
println!(" {detail}");
}
}
}
fn bump(t: &mut Tally, ok: bool) {
if ok {
t.passed += 1;
} else {
t.failed += 1;
}
}
fn preflight(dir: &Path) -> std::collections::HashMap<&'static str, J> {
let specs: [(&str, &str, &str, i64); 4] = [
(
"expression.json",
"expression",
"exprVersion",
SpecVersions::EXPRESSION,
),
(
"template.json",
"template",
"templateVersion",
SpecVersions::TEMPLATE,
),
("plan.json", "plan", "planVersion", SpecVersions::PLAN),
(
"canonical.json",
"canonical",
"canonicalVersion",
SpecVersions::CANONICAL,
),
];
let loaded: Vec<(&str, &str, i64, J)> = specs
.iter()
.map(|(f, suite, vk, want)| (*suite, *vk, *want, load_json(dir, f)))
.collect();
let mismatches: Vec<(&str, i64, i64)> = loaded
.iter()
.filter_map(|(suite, vk, want, doc)| {
let got = doc.get(*vk).and_then(|v| v.as_i64()).unwrap_or(-1);
if got != *want {
Some((*suite, got, *want))
} else {
None
}
})
.collect();
if !mismatches.is_empty() {
for (suite, got, want) in &mismatches {
eprintln!("FAIL-CLOSED: {suite} suite version {got} != supported {want}.");
}
eprintln!(
"Refusing to run: {} suite version mismatch(es). No vectors executed.",
mismatches.len()
);
exit(2);
}
loaded
.into_iter()
.map(|(suite, _, _, doc)| (suite, doc))
.collect()
}
fn as_scope(v: Option<&J>) -> Vec<(String, Value)> {
match v {
Some(J::Object(o)) => o
.iter()
.map(|(k, val)| (k.clone(), decode_value(val).expect("decode scope")))
.collect(),
_ => Vec::new(),
}
}
fn run_expression(t: &mut Tally, doc: &J) {
let vectors = doc["vectors"].as_array().unwrap();
println!(
"\nexpression.json (v{}) — {} vectors",
doc["exprVersion"],
vectors.len()
);
for v in vectors {
let name = v["name"].as_str().unwrap_or("?");
let scope = as_scope(v.get("scope"));
let expect = &v["expect"];
let (mut ok, mut detail) = (false, String::new());
match evaluate_expression(&v["expr"], &scope) {
Ok(result) => {
if let Some(exp) = expect.get("value") {
let want = decode_value(exp).expect("decode expect.value");
ok = deep_equals(&result, &want);
if !ok {
detail = format!("expected value {}, got {}", exp, encode_value(&result));
}
} else {
detail = format!("expected Failure({}), got a value", expect["failure"]);
}
}
Err(e) => {
let e: ExprFailure = e;
if let Some(f) = expect.get("failure").and_then(|f| f.as_str()) {
ok = e.code.as_str() == f;
if !ok {
detail = format!("expected Failure({f}), got Failure({})", e.code.as_str());
}
} else {
detail = format!("expected a value, got Failure({})", e.code.as_str());
}
}
}
line(ok, name, &detail);
bump(t, ok);
}
}
fn run_template(t: &mut Tally, doc: &J) {
let vectors = doc["vectors"].as_array().unwrap();
println!(
"\ntemplate.json (v{}) — {} vectors",
doc["templateVersion"],
vectors.len()
);
for v in vectors {
let name = v["name"].as_str().unwrap_or("?");
let tmpl = v["template"].as_str().unwrap();
let params: Vec<(String, Value)> = v["params"]
.as_object()
.unwrap()
.iter()
.map(|(k, val)| (k.clone(), decode_value(val).expect("decode param")))
.collect();
let expect = &v["expect"];
let (mut ok, mut detail) = (false, String::new());
match render_template(tmpl, ¶ms) {
Ok(out) => {
if let Some(exp) = expect.get("ok").and_then(|x| x.as_str()) {
ok = out == exp;
if !ok {
detail = format!("expected {exp:?}, got {out:?}");
}
} else {
detail = format!("expected Failure({}), got {out:?}", expect["failure"]);
}
}
Err(e) => {
let e: TemplateFailure = e;
if let Some(f) = expect.get("failure").and_then(|f| f.as_str()) {
ok = e.code.as_str() == f;
if !ok {
detail = format!("expected Failure({f}), got Failure({})", e.code.as_str());
}
} else {
detail = format!("expected value, got Failure({})", e.code.as_str());
}
}
}
line(ok, name, &detail);
bump(t, ok);
}
}
fn run_canonical(t: &mut Tally, doc: &J) {
let vectors = doc["vectors"].as_array().unwrap();
println!(
"\ncanonical.json (v{}) — {} vectors",
doc["canonicalVersion"],
vectors.len()
);
for v in vectors {
let name = v["name"].as_str().unwrap_or("?");
let kind = v["kind"].as_str().unwrap();
let expect = &v["expect"];
let val = decode_value(&v["value"]).expect("decode value");
let (mut ok, mut detail) = (false, String::new());
let result: Result<String, CanonicalFailure> = match kind {
"canonicalValue" => canonical_value(&val),
"canonicalJson" => canonical_json(&val),
"floatRepr" => match &val {
Value::Float(f) => py_float_repr(*f),
other => panic!("floatRepr expects a float, got {}", other.type_name()),
},
other => panic!("unknown kind: {other}"),
};
match result {
Ok(out) => {
if let Some(exp) = expect.get("ok").and_then(|x| x.as_str()) {
ok = out == exp;
if !ok {
detail = format!("expected {exp:?}, got {out:?}");
}
} else {
detail = format!("expected Failure({}), got {out:?}", expect["failure"]);
}
}
Err(e) => {
if let Some(f) = expect.get("failure").and_then(|f| f.as_str()) {
ok = e.code.as_str() == f;
if !ok {
detail = format!("expected Failure({f}), got Failure({})", e.code.as_str());
}
} else {
detail = format!("expected value, got Failure({})", e.code.as_str());
}
}
}
line(ok, name, &detail);
bump(t, ok);
}
}
fn parse_op(o: &J) -> OpSpec {
let relation_kind = o
.get("relationKind")
.and_then(|r| r.as_str())
.map(|s| match s {
"connection" => RelationKind::Connection,
_ => RelationKind::Single,
});
OpSpec {
id: o["id"].as_str().unwrap().to_string(),
parent: o.get("parent").and_then(|p| p.as_u64()).map(|p| p as usize),
bind_field: o
.get("bindField")
.and_then(|b| b.as_str())
.map(|s| s.to_string()),
relation_kind,
policy: o
.get("policy")
.and_then(|p| p.as_str())
.map(|s| s.to_string()),
}
}
fn parse_plan(p: &J) -> Option<ExecutionPlanSpec> {
if p.is_null() {
return None;
}
let groups = p["groups"]
.as_array()
.unwrap()
.iter()
.map(|g| {
g.as_array()
.unwrap()
.iter()
.map(|i| i.as_u64().unwrap() as usize)
.collect()
})
.collect();
let concurrency = p.get("concurrency").and_then(|c| c.as_i64()).unwrap_or(1);
Some(ExecutionPlanSpec {
groups,
concurrency,
})
}
fn same_set(a: &[String], b: &[J]) -> bool {
let mut sa: Vec<&str> = a.iter().map(|s| s.as_str()).collect();
let mut sb: Vec<&str> = b.iter().map(|s| s.as_str().unwrap()).collect();
sa.sort_unstable();
sb.sort_unstable();
sa == sb
}
fn run_plan_suite(t: &mut Tally, doc: &J) {
let vectors = doc["vectors"].as_array().unwrap();
println!(
"\nplan.json (v{}) — {} vectors",
doc["planVersion"],
vectors.len()
);
for v in vectors {
let name = v["name"].as_str().unwrap_or("?");
let plan = parse_plan(v.get("plan").unwrap_or(&J::Null));
let ops: Vec<OpSpec> = v["ops"].as_array().unwrap().iter().map(parse_op).collect();
let exec_map = v["exec"].as_object().unwrap();
let expect = &v["expect"];
let (mut ok, mut detail) = (false, String::new());
let exec = |op: &OpSpec, _bound: Option<&Value>| -> ExecOutcome {
let o = exec_map
.get(&op.id)
.unwrap_or_else(|| panic!("no mock outcome for op '{}'", op.id));
if let Some(okv) = o.get("ok") {
ExecOutcome::Ok(decode_value(okv).expect("decode exec.ok"))
} else {
ExecOutcome::Error(o["error"].as_str().unwrap_or("").to_string())
}
};
match run_plan(plan.as_ref(), &ops, exec) {
Ok(res) => {
if let Some(f) = expect.get("failure") {
detail = format!("expected Failure({f}), got a full run");
} else {
let tree = Value::Obj(res.final_tree());
let exp_tree_pairs: Vec<(String, Value)> = expect["tree"]
.as_object()
.unwrap()
.iter()
.map(|(k, val)| (k.clone(), decode_value(val).expect("decode expect.tree")))
.collect();
let exp_tree = Value::Obj(exp_tree_pairs);
let got_c = canonical_json(&tree).expect("canonical tree");
let want_c = canonical_json(&exp_tree).expect("canonical exp tree");
let tree_ok = got_c == want_c;
let exec_ok = same_set(&res.executed, expect["executed"].as_array().unwrap());
let skip_ok = same_set(&res.skipped, expect["skipped"].as_array().unwrap());
ok = tree_ok && exec_ok && skip_ok;
if !ok {
let mut parts = Vec::new();
if !tree_ok {
parts.push(format!("tree {got_c} != {want_c}"));
}
if !exec_ok {
parts.push(format!(
"executed {:?} != {}",
res.executed, expect["executed"]
));
}
if !skip_ok {
parts.push(format!(
"skipped {:?} != {}",
res.skipped, expect["skipped"]
));
}
detail = parts.join("; ");
}
}
}
Err(e) => {
let e: PlanFailure = e;
if let Some(f) = expect.get("failure").and_then(|f| f.as_str()) {
ok = e.code.as_str() == f;
if !ok {
detail = format!("expected Failure({f}), got Failure({})", e.code.as_str());
}
} else {
detail = format!("expected a full run, got Failure({})", e.code.as_str());
}
}
}
line(ok, name, &detail);
bump(t, ok);
}
}
fn main() {
println!("dsl-contracts conformance kit — Rust runner");
let dir = vectors_dir();
let docs = preflight(&dir);
let mut t = Tally {
passed: 0,
failed: 0,
};
run_expression(&mut t, &docs["expression"]);
run_template(&mut t, &docs["template"]);
run_plan_suite(&mut t, &docs["plan"]);
run_canonical(&mut t, &docs["canonical"]);
let total = t.passed + t.failed;
println!(
"\n{} passed, {} failed / {total} vectors across 4 suites",
t.passed, t.failed
);
exit(if t.failed > 0 { 1 } else { 0 });
}