///|
/// Canonical rule-set formatting.
///
/// The fmt operation rewrites every rule to its canonical textual
/// form: conditions through the expression renderer (minimal
/// parentheses, single spaces), IDs and outcomes untouched, optional
/// fields normalized to a stable key order. `changed` flags rules
/// whose source differed from canonical form, so CI can fail on
/// unformatted rule sets exactly like a code formatter — and `apply`
/// returns the formatted rules as a ready-to-use rule set that
/// compiles to identical trees.
///|
fn fmt_rules_request(request : Value) -> Value raise {
let rules = arr(get(request, "rules"))
let formatted : Array[Value] = []
let changed_ids : Array[String] = []
let mut index = 0
let mut changed_count = 0
for item in rules {
let id = str(get(item, "id"))
let condition = compile(str(get(item, "when")))
let canonical = condition.render()
let source = str(get(item, "when"))
let rule : Map[String, Value] = Map([])
rule["id"] = String(id)
rule["when"] = String(canonical)
match get(item, "priority") {
Null => rule["priority"] = Number(0.0)
other => rule["priority"] = other
}
match get(item, "output") {
Null => ()
output => rule["output"] = output
}
match get(item, "compute") {
Null => ()
String(source) => {
let compute = compile(source)
let canonical_compute = compute.render()
rule["compute"] = String(canonical_compute)
if canonical_compute != source {
changed_count += 1
changed_ids.push(id)
}
}
_ => raise input_error("E4007", "compute must be a string")
}
if canonical != source {
changed_count += 1
changed_ids.push(id)
}
ignore(index)
index += 1
formatted.push(Object(rule))
}
changed_ids.sort_by(fn(a, b) { stable_string_compare(a, b) })
let changed_flagged = match get(request, "changed_only") {
Bool(true) => {
let selected : Array[Value] = []
let wanted : Map[String, Bool] = Map([])
for id in changed_ids {
wanted[id] = true
}
for rule in formatted {
if wanted.contains(str(get(rule, "id"))) {
selected.push(rule)
}
}
Array(selected)
}
_ => Array(formatted)
}
record([
("rules", changed_flagged),
("rule_count", Number(rules.length().to_double())),
("changed_count", Number(changed_count.to_double())),
("changed_ids", strings(changed_ids)),
])
}