///|
pub(all) enum ValueType {
Text
Integer
Boolean
} derive(Eq, Debug)
///|
/// Per-key INI constraint, not JSON Schema/OpenAPI or cross-format configuration.
pub(all) struct Rule {
section : String
key : String
required : Bool
kind : ValueType
minimum : Int?
maximum : Int?
choices : Array[String]
} derive(Eq, Debug)
///|
pub fn rule(
section : String,
key : String,
kind : ValueType,
required? : Bool = false,
minimum? : Int? = None,
maximum? : Int? = None,
choices? : Array[String] = [],
) -> Rule {
{ section, key, kind, required, minimum, maximum, choices: choices.copy() }
}
///|
/// Validate effective values. Syntax diagnostics remain on Document separately.
/// Output order: rule order, then unknown keys in physical order.
pub fn Document::validate_schema(
self : Document,
rules : Array[Rule],
allow_unknown? : Bool = true,
) -> Array[Diagnostic] {
let ds : Array[Diagnostic] = []
let seen : Array[Rule] = []
for r in rules {
if !valid_names(r.section, r.key, self.dialect) ||
(r.kind != Integer && (r.minimum != None || r.maximum != None)) ||
(match (r.minimum, r.maximum) {
(Some(a), Some(b)) => a > b
_ => false
}) ||
seen.any(x => {
same_name(x.section, r.section, self.dialect) &&
same_name(x.key, r.key, self.dialect)
}) {
ds.push(edit_problem("SCHEMA001", "invalid or duplicate schema rule"))
continue
}
seen.push(r)
if self.entries(r.section, r.key).is_empty() {
if r.required {
ds.push(
edit_problem(
"SCHEMA002",
"required key is missing: [" + r.section + "] " + r.key,
),
)
}
continue
}
match selected_node(self, r.section, r.key) {
Err(e) => ds.push(e)
Ok(n) => {
match r.kind {
Text => ()
Boolean =>
if boolean(n.value) == None {
ds.push(diagnostic("SCHEMA003", "expected boolean", n.value_span))
}
Integer =>
match decimal_int(n.value) {
None =>
ds.push(
diagnostic(
"SCHEMA003",
"expected decimal signed 32-bit integer",
n.value_span,
),
)
Some(v) => {
let below = match r.minimum {
Some(min) => v < min
None => false
}
let above = match r.maximum {
Some(max) => v > max
None => false
}
if below || above {
ds.push(
diagnostic(
"SCHEMA004",
"integer outside allowed range",
n.value_span,
),
)
}
}
}
}
if !r.choices.is_empty() && !r.choices.contains(n.value) {
ds.push(
diagnostic(
"SCHEMA005",
"value is not an allowed choice",
n.value_span,
),
)
}
}
}
}
if !allow_unknown {
for n in self.nodes {
if n.kind == Entry &&
!rules.any(r => {
same_name(r.section, n.section, self.dialect) &&
same_name(r.key, n.key, self.dialect)
}) {
ds.push(diagnostic("SCHEMA006", "unknown key", n.key_span))
}
}
}
ds
}