///|
/// Rule-set templates.
///
/// The template operation renders a parameterized rule set into a
/// concrete one: expressions may reference `{{name}}` placeholders and
/// string cells may embed them, every placeholder is substituted from
/// the params map before compilation, and the result compiles through
/// the standard pipeline so a template error is a compile error with
/// position. Unknown placeholders and unused params are reported —
/// templates must be closed over their parameters to be reusable.
///|
fn split_all(text : String, separator : String) -> Array[String] {
let parts : Array[String] = []
for view in text.split(separator) {
parts.push(view.to_owned())
}
parts
}
///|
fn expand_template(
text : String,
params : Map[String, Value],
json_values : Bool,
) -> (String, Array[String], Array[String]) {
// Placeholder scanning via segment splits (no char-by-char walking):
// text between "{{" and "}}" names a parameter; values render as JSON.
let used : Map[String, Bool] = Map([])
let missing : Array[String] = []
let mut out = ""
let mut rest = text
let mut loops = 0
while loops < 1000 {
loops += 1
let parts = split_all(rest, "{{")
if parts.length() < 2 {
out += rest
break
}
out += parts[0]
let tail = parts[1]
let tail_parts = split_all(tail, "}}")
if tail_parts.length() < 2 {
// unterminated placeholder: keep literally along with the rest
out += "{{" + tail
break
}
let key = tail_parts[0].trim().to_owned()
match params.get(key) {
Some(value) => {
// Expressions need JSON literals (strings quoted); plain text
// fields like IDs take the bare string form.
if json_values {
out += Value::stringify(value)
} else {
out += match value {
String(s) => s
other => Value::stringify(other)
}
}
used[key] = true
}
None => {
missing.push(key)
out += "{{" + key + "}}"
}
}
// Re-join any further "{{" occurrences split apart above so the
// next loop iteration sees them intact.
let mut remainder = tail_parts[1]
for k = 2; k < parts.length(); k = k + 1 {
remainder += "{{" + parts[k]
}
rest = remainder
}
let missing_set : Map[String, Bool] = Map([])
for name in missing {
missing_set[name] = true
}
let missing_dedup : Array[String] = []
for name, _ in missing_set {
missing_dedup.push(name)
}
let used_list : Array[String] = []
for name, _ in used {
used_list.push(name)
}
used_list.sort_by(fn(a, b) { stable_string_compare(a, b) })
missing_dedup.sort_by(fn(a, b) { stable_string_compare(a, b) })
(out, used_list, missing_dedup)
}
///|
fn template_request(request : Value) -> Value raise {
let params : Map[String, Value] = Map([])
match get(request, "params") {
Null => ()
Object(fields) =>
for key, value in fields {
params[key] = value
}
_ => raise input_error("E3009", "params must be an object")
}
let rules = arr(get(request, "rules"))
let rendered : Array[Value] = []
let all_used : Map[String, Bool] = Map([])
let all_missing : Map[String, Bool] = Map([])
let mut index = 0
for item in rules {
let id = str(get(item, "id"))
let id_expanded = expand_template(id, params, false)
let expanded_id = id_expanded.0
for name in id_expanded.1 {
all_used[name] = true
}
for name in id_expanded.2 {
all_missing[name] = true
}
let source = str(get(item, "when"))
let expanded = expand_template(source, params, true)
let expanded_when = expanded.0
for name in expanded.1 {
all_used[name] = true
}
for name in expanded.2 {
all_missing[name] = true
}
let rule : Map[String, Value] = Map([])
rule["id"] = String(expanded_id)
rule["when"] = String(expanded_when)
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(compute_source) => {
let expanded_compute = expand_template(compute_source, params, true)
rule["compute"] = String(expanded_compute.0)
for name in expanded_compute.1 {
all_used[name] = true
}
for name in expanded_compute.2 {
all_missing[name] = true
}
}
_ => raise input_error("E4007", "compute must be a string")
}
ignore(index)
index += 1
rendered.push(Object(rule))
}
let missing_names : Array[String] = []
for name, _ in all_missing {
missing_names.push(name)
}
missing_names.sort_by(fn(a, b) { stable_string_compare(a, b) })
let unused_names : Array[String] = []
for name, _ in params {
if !all_used.contains(name) {
unused_names.push(name)
}
}
unused_names.sort_by(fn(a, b) { stable_string_compare(a, b) })
record([
("rules", Array(rendered)),
("rule_count", Number(rules.length().to_double())),
("missing_params", strings(missing_names)),
("unused_params", strings(unused_names)),
("ok", Bool(missing_names.is_empty())),
])
}