use crate::ir_nodes::IRType;
use crate::pem::semantic_validator::{Clause, Constraint, ConstraintSet, Predicate, ValidatorError};
pub fn constraints_for_type(ty: &IRType) -> Result<ConstraintSet, ValidatorError> {
let mut clauses = Vec::new();
clauses.push(Clause::Checkable(Constraint {
id: "c1".to_string(),
source: format!("type {} — the response must be structured", ty.name),
predicate: Predicate::ParsesAsJson,
}));
for f in ty.fields.iter().filter(|f| !f.optional) {
let id = format!("c{}", clauses.len() + 1);
clauses.push(Clause::Checkable(Constraint {
id,
source: format!("type {} — field `{}: {}`", ty.name, f.name, f.type_name),
predicate: Predicate::JsonField {
field: f.name.clone(),
},
}));
}
ConstraintSet::new(clauses)
}
pub fn obligation_count(ty: &IRType) -> usize {
1 + ty.fields.iter().filter(|f| !f.optional).count()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir_nodes::IRTypeField;
fn field(name: &str, ty: &str, optional: bool) -> IRTypeField {
IRTypeField {
node_type: "type_field",
source_line: 0,
source_column: 0,
name: name.to_string(),
type_name: ty.to_string(),
generic_param: String::new(),
optional,
}
}
fn schema(fields: Vec<IRTypeField>) -> IRType {
IRType {
node_type: "type",
source_line: 0,
source_column: 0,
name: "ContractSchema".to_string(),
fields,
range_min: None,
range_max: None,
where_expression: String::new(),
compliance: Vec::new(),
}
}
#[test]
fn the_set_is_structure_plus_every_required_field() {
let t = schema(vec![
field("parties", "String", false),
field("obligations", "String", false),
field("notes", "String", true),
]);
assert_eq!(obligation_count(&t), 3);
let cs = constraints_for_type(&t).expect("lowers");
assert_eq!(cs.len(), 3, "1 structural + 2 required (the optional is NOT an obligation)");
}
#[test]
fn a_conforming_json_object_scores_one() {
let t = schema(vec![field("parties", "String", false)]);
let cs = constraints_for_type(&t).unwrap();
let v = cs.evaluate(r#"{"parties": "Acme and Beta"}"#);
assert_eq!(v.csr, 1.0, "violations: {:?}", v.violated);
assert!(v.is_satisfied());
}
#[test]
fn prose_that_mentions_the_field_does_not_satisfy_it() {
let t = schema(vec![field("amount", "Float", false)]);
let cs = constraints_for_type(&t).unwrap();
let v = cs.evaluate("I could not determine the amount from the document.");
assert_eq!(
v.csr, 0.0,
"a substring test would score this 1.0 — structure is NOT evidenced \
by the presence of text"
);
}
#[test]
fn a_null_member_does_not_satisfy_its_field() {
let t = schema(vec![field("amount", "Float", false)]);
let cs = constraints_for_type(&t).unwrap();
let v = cs.evaluate(r#"{"amount": null}"#);
assert_eq!(v.csr, 0.5, "structure held; the field did not: {:?}", v.violated);
}
#[test]
fn partial_conformance_lands_between_zero_and_one() {
let t = schema(vec![
field("a", "String", false),
field("b", "String", false),
field("c", "String", false),
]);
let cs = constraints_for_type(&t).unwrap();
let v = cs.evaluate(r#"{"a": 1, "b": 2}"#);
assert!((v.csr - 0.75).abs() < 1e-9, "1 structural + 2 of 3 fields = 3/4, got {}", v.csr);
assert!((v.error - 0.25).abs() < 1e-9, "e = 1 − CSR");
}
#[test]
fn a_json_array_names_its_own_failure() {
let t = schema(vec![field("a", "String", false)]);
let cs = constraints_for_type(&t).unwrap();
let v = cs.evaluate(r#"["a"]"#);
assert_eq!(v.csr, 0.5, "the structural obligation held — it IS valid JSON");
assert!(
v.violated[0].reason.contains("not an OBJECT"),
"got: {}",
v.violated[0].reason
);
}
#[test]
fn prose_feedback_names_the_root_cause_once() {
let t = schema(vec![
field("a", "String", false),
field("b", "String", false),
]);
let cs = constraints_for_type(&t).unwrap();
let fb = cs.evaluate("not json at all").feedback();
assert_eq!(
fb.matches("must be valid JSON").count(),
1,
"the structural obligation appears exactly once:\n{fb}"
);
}
#[test]
fn a_schema_with_no_required_fields_measures_structure_alone() {
let t = schema(vec![field("maybe", "String", true)]);
let cs = constraints_for_type(&t).expect("still lowers");
assert_eq!(cs.len(), 1);
assert_eq!(cs.evaluate(r#"{}"#).csr, 1.0);
assert_eq!(cs.evaluate("prose").csr, 0.0);
}
}