use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Copy)]
pub struct SchemaRule<'a> {
pub name: &'a str,
pub rationale: 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 {
properties.insert(
"rationale".to_string(),
json!({
"type": "string",
"minLength": 1,
"description": "Terse, evidence-citing justification for `holds`, written before the verdict.",
}),
);
required.push(json!("rationale"));
}
properties.insert("holds".to_string(), json!({ "type": "boolean" }));
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" }
}
}
}),
);
json!({
"type": "object",
"additionalProperties": false,
"required": Value::Array(required),
"properties": Value::Object(properties),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn with(name: &str, rationale: bool) -> SchemaRule<'_> {
SchemaRule { name, rationale }
}
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 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());
}
}