///|
priv struct NumericBound {
value : ExactDecimal
inclusive : Bool
}
///|
priv struct NumericFieldRule {
field : String
array : Bool
lower : NumericBound?
upper : NumericBound?
minimum_items : Int32
maximum_items : Int32?
increasing : Bool
document : Map[String, Json]
}
///|
priv struct NumericRelation {
left : String
op : String
right : String
}
///|
/// Project policy over inherited top-level numeric fields, with exact decimal
/// comparisons. This is not a JSON Schema implementation or unit converter.
pub struct NumericRules {
priv fields : Array[NumericFieldRule]
priv relations : Array[NumericRelation]
}
///|
fn NumericRules::rules_value(self : NumericRules) -> Json {
let fields : Map[String, Json] = Map([])
for rule in self.fields {
fields[rule.field] = Json::object(rule.document)
}
Json::object({
"fields": Json::object(fields),
"relations": Json::array(
self.relations.map(r => {
Json::object({
"left": Json::string(r.left),
"op": Json::string(r.op),
"right": Json::string(r.right),
})
}),
),
})
}
///|
pub fn NumericRules::to_json(self : NumericRules) -> String {
canonical_json(self.rules_value())
}
///|
fn numeric_bound(
input : Map[String, Json],
exclusive : String,
inclusive : String,
field : String,
) -> NumericBound? raise TraceError {
if input.contains(exclusive) && input.contains(inclusive) {
reject(
"conflicting_numeric_bounds",
field,
"Use only one of " + exclusive + " and " + inclusive + ".",
)
}
let control = if input.contains(exclusive) { exclusive } else { inclusive }
match input.get(control) {
None => None
Some(Number(_, ..) as n) =>
Some({
value: exact_decimal(canonical_json(n)),
inclusive: control == inclusive,
})
Some(_) => {
reject(
"invalid_numeric_bound",
field,
control +
" must be a JSON number; strings and null are not numeric bounds.",
)
None
}
}
}
///|
fn numeric_item_count(
input : Map[String, Json],
key : String,
field : String,
) -> Int32? raise TraceError {
match input.get(key) {
None => None
Some(Number(_, ..) as n) => {
// Configuration counts deliberately use plain decimal integer spelling.
let text = canonical_json(n)
if text.length() > 7 || !text.iter().all(c => c >= '0' && c <= '9') {
reject(
"invalid_numeric_item_count",
field,
key + " must be a plain nonnegative integer at most 2,097,152.",
)
}
let mut value = 0
for c in text.iter() {
value = value * 10 + c.to_int() - 48
}
if value > 2097152 {
reject(
"invalid_numeric_item_count",
field,
key + " must be at most 2,097,152.",
)
}
Some(value)
}
Some(_) => {
reject(
"invalid_numeric_item_count",
field,
key + " must be a plain nonnegative integer.",
)
None
}
}
}
///|
fn numeric_field_rule(
field : String,
value : Json,
) -> NumericFieldRule raise TraceError {
guard value is Object(input) else {
reject(
"invalid_numeric_field", field, "Expected an object declaring type number or array.",
)
abort("reject raises")
}
for key in sorted_keys(input) {
if ![
"type", "gt", "gte", "lt", "lte", "min_items", "max_items", "strictly_increasing",
].contains(key) {
reject(
"unknown_numeric_field_control",
field,
"Unsupported numeric control " + key + "; no rule was produced.",
)
}
}
let array = match input.get("type") {
Some(String("number")) => false
Some(String("array")) => true
_ => {
reject(
"invalid_numeric_type", field, "type must be number or array; no coercion is performed.",
)
false
}
}
if !array &&
["min_items", "max_items", "strictly_increasing"].any(key => {
input.contains(key)
}) {
reject("invalid_numeric_field", field, "Array controls require type array.")
}
let lower = numeric_bound(input, "gt", "gte", field)
let upper = numeric_bound(input, "lt", "lte", field)
if lower is Some(a) && upper is Some(b) {
let c = a.value.compare(b.value)
if c > 0 || (c == 0 && (!a.inclusive || !b.inclusive)) {
reject(
"empty_numeric_interval", field, "Lower and upper bounds define an empty interval; correct the bounds.",
)
}
}
let minimum_items = numeric_item_count(input, "min_items", field).unwrap_or(0)
let maximum_items = numeric_item_count(input, "max_items", field)
if maximum_items is Some(maximum) && minimum_items > maximum {
reject(
"empty_numeric_item_range", field, "min_items must not exceed max_items.",
)
}
let increasing = match input.get("strictly_increasing") {
None | Some(False) => false
Some(True) => true
_ => {
reject(
"invalid_numeric_field", field, "strictly_increasing must be a boolean.",
)
false
}
}
{
field,
array,
lower,
upper,
minimum_items,
maximum_items,
increasing,
document: input,
}
}
///|
/// Strict {fields:{name:{type,...}},relations:[{left,op,right}]}.
/// Every declared field is required. Relations reference declared scalar fields.
pub fn NumericRules::from_json(text : String) -> NumericRules raise TraceError {
guard parse_source("", text) is Object(input) else {
reject(
"invalid_numeric_rules", "", "Expected fields and relations controls.",
)
abort("reject raises")
}
if sorted_keys(input) != ["fields", "relations"] {
reject(
"invalid_numeric_rules", "", "Exactly fields and relations are required; unknown controls are rejected.",
)
}
guard input.get("fields") is Some(Object(fields)) &&
input.get("relations") is Some(Array(relations)) else {
reject(
"invalid_numeric_rules", "", "fields must be an object and relations an array.",
)
abort("reject raises")
}
let names = review_fields(sorted_keys(fields))
if names.is_empty() || relations.length() > 256 {
reject(
"numeric_rules_limit", "", "Declare 1–256 fields and at most 256 relations.",
)
}
let rules = names.map(name => numeric_field_rule(name, fields[name]))
let seen : Map[String, Bool] = Map([])
let parsed = relations.map(value => {
guard value is Object(r) &&
sorted_keys(r) == ["left", "op", "right"] &&
r.get("left") is Some(String(left)) &&
r.get("op") is Some(String(op)) &&
r.get("right") is Some(String(right)) else {
reject(
"invalid_numeric_relation", "/relations", "Each relation requires exactly string left, op and right.",
)
abort("reject raises")
}
if !["lt", "lte", "eq", "gte", "gt"].contains(op) {
reject(
"invalid_numeric_relation", left, "op must be lt, lte, eq, gte or gt.",
)
}
for field in [left, right] {
if !rules.any(rule => rule.field == field && !rule.array) {
reject(
"invalid_numeric_operand", field, "Relations require explicitly declared number fields; arrays and missing declarations are unsupported.",
)
}
}
let token = canonical_json(value)
if seen.contains(token) {
reject(
"duplicate_numeric_relation", left, "This relation is declared more than once.",
)
}
seen[token] = true
{ left, op, right, }
})
parsed.sort_by((a, b) => {
let left = lexical_compare(a.left, b.left)
if left != 0 {
left
} else {
let op = lexical_compare(a.op, b.op)
if op != 0 {
op
} else {
lexical_compare(a.right, b.right)
}
}
})
let result : NumericRules = { fields: rules, relations: parsed, }
if result.to_json().length() > 2097152 {
reject(
"input_limit", "", "Canonical numeric rules exceed 2,097,152 UTF-16 units.",
)
}
result
}