use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Copy)]
pub struct SchemaRule<'a> {
pub name: &'a str,
pub rationale: bool,
pub relevance: bool,
}
pub fn build(rules: &[SchemaRule]) -> Value {
let mut properties = Map::new();
let mut required = Vec::with_capacity(rules.len());
for rule in rules {
properties.insert(rule.name.to_string(), rule_schema(rule));
required.push(rule.name);
}
json!({
"type": "object",
"additionalProperties": false,
"required": required,
"properties": properties,
})
}
fn rule_schema(rule: &SchemaRule) -> Value {
let mut properties = Map::new();
let mut required: Vec<Value> = Vec::new();
properties.insert(
"name".to_string(),
json!({ "type": "string", "const": rule.name }),
);
required.push(json!("name"));
if rule.rationale {
let description = if rule.relevance {
"Terse, evidence-citing justification, written before `relevant`/`holds`: why the rule is (or isn't) relevant, and — when relevant — the verdict."
} else {
"Terse, evidence-citing justification for `holds`, written before the verdict."
};
properties.insert(
"rationale".to_string(),
json!({ "type": "string", "minLength": 1, "description": description }),
);
required.push(json!("rationale"));
}
if rule.relevance {
properties.insert(
"relevant".to_string(),
json!({
"type": "boolean",
"description": "Whether this rule applies to the change. Decide before the verdict; when false, omit `holds`/`violations`.",
}),
);
required.push(json!("relevant"));
}
properties.insert("holds".to_string(), json!({ "type": "boolean" }));
if !rule.relevance {
required.push(json!("holds"));
}
properties.insert(
"violations".to_string(),
json!({
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"file": { "type": "string" },
"line": { "type": "integer", "minimum": 1 },
"end_line": { "type": "integer", "minimum": 1 },
"message": { "type": "string" }
}
}
}),
);
let mut obj = Map::new();
obj.insert("type".to_string(), json!("object"));
obj.insert("additionalProperties".to_string(), json!(false));
obj.insert("required".to_string(), Value::Array(required));
obj.insert("properties".to_string(), Value::Object(properties));
if rule.relevance {
obj.insert(
"if".to_string(),
json!({ "properties": { "relevant": { "const": true } }, "required": ["relevant"] }),
);
obj.insert("then".to_string(), json!({ "required": ["holds"] }));
}
Value::Object(obj)
}
#[cfg(test)]
mod tests {
use super::*;
fn with(name: &str, rationale: bool) -> SchemaRule<'_> {
SchemaRule {
name,
rationale,
relevance: false,
}
}
fn with_relevance(name: &str, rationale: bool) -> SchemaRule<'_> {
SchemaRule {
name,
rationale,
relevance: true,
}
}
fn key_order(props: &Value) -> Vec<String> {
props
.as_object()
.unwrap()
.keys()
.map(String::from)
.collect()
}
#[test]
fn schema_keys_each_rule_and_requires_holds() {
let s = build(&[with("a_rule", true), with("b_rule", true)]);
assert_eq!(s["type"], "object");
assert_eq!(s["additionalProperties"], false);
assert_eq!(s["required"], json!(["a_rule", "b_rule"]));
let a = &s["properties"]["a_rule"];
assert_eq!(a["properties"]["holds"]["type"], "boolean");
assert_eq!(a["properties"]["violations"]["type"], "array");
}
#[test]
fn rule_fields_are_ordered_name_rationale_holds() {
let s = build(&[with("only", true)]);
let rule = &s["properties"]["only"];
assert_eq!(
key_order(&rule["properties"]),
["name", "rationale", "holds", "violations"]
);
assert_eq!(rule["required"], json!(["name", "rationale", "holds"]));
assert_eq!(rule["properties"]["name"]["const"], "only");
assert_eq!(rule["properties"]["rationale"]["minLength"], 1);
}
#[test]
fn rationale_off_drops_the_field_but_keeps_order() {
let s = build(&[with("only", false)]);
let rule = &s["properties"]["only"];
assert_eq!(
key_order(&rule["properties"]),
["name", "holds", "violations"]
);
assert_eq!(rule["required"], json!(["name", "holds"]));
assert!(rule["properties"].get("rationale").is_none());
}
#[test]
fn rationale_is_decided_per_rule() {
let s = build(&[with("on", true), with("off", false)]);
assert_eq!(
s["properties"]["on"]["required"],
json!(["name", "rationale", "holds"])
);
assert_eq!(s["properties"]["off"]["required"], json!(["name", "holds"]));
}
#[test]
fn rule_order_is_preserved_in_top_level_properties() {
let s = build(&[with("zed", true), with("alpha", true)]);
assert_eq!(key_order(&s["properties"]), ["zed", "alpha"]);
}
#[test]
fn relevance_inserts_a_gate_before_holds_and_requires_holds_only_when_relevant() {
let s = build(&[with_relevance("gated", true)]);
let rule = &s["properties"]["gated"];
assert_eq!(
key_order(&rule["properties"]),
["name", "rationale", "relevant", "holds", "violations"]
);
assert_eq!(rule["required"], json!(["name", "rationale", "relevant"]));
assert_eq!(rule["properties"]["relevant"]["type"], "boolean");
assert_eq!(rule["if"]["properties"]["relevant"]["const"], true);
assert_eq!(rule["then"]["required"], json!(["holds"]));
}
#[test]
fn relevance_without_rationale_still_gates_holds() {
let s = build(&[with_relevance("gated", false)]);
let rule = &s["properties"]["gated"];
assert_eq!(
key_order(&rule["properties"]),
["name", "relevant", "holds", "violations"]
);
assert_eq!(rule["required"], json!(["name", "relevant"]));
assert_eq!(rule["then"]["required"], json!(["holds"]));
}
#[test]
fn non_relevance_rules_have_no_if_then_gate() {
let s = build(&[with("plain", true)]);
let rule = &s["properties"]["plain"];
assert!(rule.get("if").is_none());
assert!(rule.get("then").is_none());
assert!(rule["properties"].get("relevant").is_none());
}
#[test]
fn empty_rule_set_is_a_well_formed_object_schema() {
let s = build(&[]);
assert_eq!(s["type"], "object");
assert_eq!(s["required"], json!([]));
assert!(s["properties"].as_object().unwrap().is_empty());
}
}