///|
/// Constraint metadata uses optional bounds, preserving negative values.
pub(all) struct ConstraintInfo {
min_value : Double?
max_value : Double?
min_inclusive : Bool
max_inclusive : Bool
format : String
pattern : String
patterns : Array[String]
formats : Array[String]
is_int : Bool
is_positive : Bool
is_negative : Bool
multiple_of : Double
multiples : Array[Double]
custom_messages : Array[String]
}
///|
/// Empty constraints use None instead of a numeric sentinel.
pub fn constraint_info_default() -> ConstraintInfo {
{
min_value: None,
max_value: None,
min_inclusive: true,
max_inclusive: true,
format: "",
pattern: "",
patterns: [],
formats: [],
is_int: false,
is_positive: false,
is_negative: false,
multiple_of: 0.0,
multiples: [],
custom_messages: [],
}
}
///|
/// Extract built-in constraints and explicit annotation extensions.
pub fn extract_constraints(rules : Array[Rule]) -> ConstraintInfo {
let mut info = constraint_info_default()
for rule in rules {
match rule.constraint {
Minimum(_, value, inclusive) =>
info = fold_minimum(info, value, inclusive)
Maximum(_, value, inclusive) =>
info = fold_maximum(info, value, inclusive)
ExactLength(_, value) => {
info = fold_minimum(info, value.to_double(), true)
info = fold_maximum(info, value.to_double(), true)
}
Multiple(value) => {
info.multiples.push(value)
info = { ..info, multiple_of: value, }
}
Integer | SafeInteger => info = { ..info, is_int: true, }
Format(value) => {
info.formats.push(value)
info = { ..info, format: value, }
}
Finite | Prefix(_) | Suffix(_) | Contains(_) => ()
Pattern(value) => {
info.patterns.push(value)
info = { ..info, pattern: value, }
}
Opaque =>
if rule.annotation is Null {
info.custom_messages.push(rule.message)
}
}
if rule.annotation is Object(fields) {
for key, value in fields {
match (key, value) {
("minLength" | "minItems" | "minimum", Number(v, ..)) =>
info = fold_minimum(info, v, true)
("maxLength" | "maxItems" | "maximum", Number(v, ..)) =>
info = fold_maximum(info, v, true)
("exclusiveMinimum", Number(v, ..)) =>
info = fold_minimum(info, v, false)
("exclusiveMaximum", Number(v, ..)) =>
info = fold_maximum(info, v, false)
("type", String("integer")) => info = { ..info, is_int: true, }
("format", String(v)) =>
if !info.formats.contains(v) {
info.formats.push(v)
info = { ..info, format: v, }
}
("pattern", String(v)) =>
if !info.patterns.contains(v) {
info.patterns.push(v)
info = { ..info, pattern: v, }
}
("multipleOf", Number(v, ..)) =>
if !info.multiples.contains(v) {
info.multiples.push(v)
info = { ..info, multiple_of: v, }
}
_ => ()
}
}
}
}
{
..info,
is_positive: info.min_value is Some(0.0) && !info.min_inclusive,
is_negative: info.max_value is Some(0.0) && !info.max_inclusive,
}
}
///|
fn fold_minimum(
info : ConstraintInfo,
value : Double,
inclusive : Bool,
) -> ConstraintInfo {
match info.min_value {
Some(previous) =>
if value < previous {
return info
} else if value == previous {
return { ..info, min_inclusive: info.min_inclusive && inclusive, }
}
None => ()
}
{ ..info, min_value: Some(value), min_inclusive: inclusive, }
}
///|
fn fold_maximum(
info : ConstraintInfo,
value : Double,
inclusive : Bool,
) -> ConstraintInfo {
match info.max_value {
Some(previous) =>
if value > previous {
return info
} else if value == previous {
return { ..info, max_inclusive: info.max_inclusive && inclusive, }
}
None => ()
}
{ ..info, max_value: Some(value), max_inclusive: inclusive, }
}
///|
fn range_parts(info : ConstraintInfo, suffix : String) -> Array[String] {
match (info.min_value, info.max_value) {
(Some(a), Some(b)) =>
if info.min_inclusive && info.max_inclusive {
[format_double_simple(a) + "-" + format_double_simple(b) + suffix]
} else {
[
(if info.min_inclusive { "min: " } else { "> " }) +
format_double_simple(a),
(if info.max_inclusive { "max: " } else { "< " }) +
format_double_simple(b),
]
}
(Some(a), None) =>
[
(if info.min_inclusive { "min: " } else { "> " }) +
format_double_simple(a),
]
(None, Some(b)) =>
[
(if info.max_inclusive { "max: " } else { "< " }) +
format_double_simple(b),
]
_ => []
}
}
///|
/// Render every string bound, format and pattern.
pub fn constraint_info_to_string_comment(info : ConstraintInfo) -> String {
let parts = range_parts(info, " chars")
for format in info.formats {
parts.push(if format == "uri" { "url" } else { format })
}
for pattern in info.patterns {
parts.push("pattern: " + pattern)
}
parts.append(info.custom_messages)
parts.join(", ")
}
///|
/// Render numeric constraints, including negative and exclusive bounds.
pub fn constraint_info_to_number_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
if info.is_int {
parts.push("int")
}
if info.is_positive && info.max_value is None {
parts.push("positive")
} else if info.is_negative && info.min_value is None {
parts.push("negative")
} else {
parts.append(range_parts(info, ""))
}
for multiple in info.multiples {
parts.push("multiple of " + format_double_simple(multiple))
}
parts.append(info.custom_messages)
parts.join(", ")
}
///|
/// Render array cardinality constraints.
pub fn constraint_info_to_array_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
if info.min_value is Some(value) {
parts.push("min: " + format_double_simple(value) + " items")
}
if info.max_value is Some(value) {
parts.push("max: " + format_double_simple(value) + " items")
}
parts.append(info.custom_messages)
parts.join(", ")
}
///|
/// Render opaque predicates.
pub fn constraint_info_to_fallback_comment(info : ConstraintInfo) -> String {
info.custom_messages.join(", ")
}
///|
/// Keep checks attached after built-in effects when rendering constraints.
pub fn constraint_comment(schema : Schema) -> String {
let info = extract_constraints(effective_rules(schema))
match inner_type(schema.schema_type) {
StringType => constraint_info_to_string_comment(info)
NumberType => constraint_info_to_number_comment(info)
ArrayType(_) => constraint_info_to_array_comment(info)
_ => constraint_info_to_fallback_comment(info)
}
}