///|
/// Unified constraint information extracted from Rule annotations.
/// This data structure consolidates all constraint metadata across different types.
pub(all) struct ConstraintInfo {
min_value : Double // For string length, number min, array minItems
max_value : Double // For string length, number max, array maxItems
format : String // "email", "uri", "date-time", "uuid", "ipv4", etc.
pattern : String // regex pattern
is_int : Bool // For numbers: must be integer
is_positive : Bool // For numbers: > 0
is_negative : Bool // For numbers: < 0
multiple_of : Double // For numbers: multiple of N
custom_messages : Array[String] // Custom error messages
}
///|
/// Create a default empty ConstraintInfo
pub fn constraint_info_default() -> ConstraintInfo {
{
min_value: -1.0,
max_value: -1.0,
format: "",
pattern: "",
is_int: false,
is_positive: false,
is_negative: false,
multiple_of: 0.0,
custom_messages: [],
}
}
///|
/// Extract all constraint information from a Rule array.
/// Unified extraction that works across all types.
pub fn extract_constraints(rules : Array[Rule]) -> ConstraintInfo {
let mut min_value = -1.0
let mut max_value = -1.0
let mut format_val = ""
let mut pattern_val = ""
let mut is_int_val = false
let mut is_positive_val = false
let mut is_negative_val = false
let mut multiple_of_val = 0.0
let custom_messages_val : Array[String] = []
// First pass: extract from JSON annotations
for rule in rules {
match rule.annotation {
Object(map) => {
// Length/Value constraints
if map.contains("minLength") {
match map.get("minLength") {
Some(Number(v, ..)) => min_value = v
_ => ()
}
}
if map.contains("maxLength") {
match map.get("maxLength") {
Some(Number(v, ..)) => max_value = v
_ => ()
}
}
if map.contains("minimum") {
match map.get("minimum") {
Some(Number(v, ..)) => min_value = v
_ => ()
}
}
if map.contains("maximum") {
match map.get("maximum") {
Some(Number(v, ..)) => max_value = v
_ => ()
}
}
if map.contains("minItems") {
match map.get("minItems") {
Some(Number(v, ..)) => min_value = v
_ => ()
}
}
if map.contains("maxItems") {
match map.get("maxItems") {
Some(Number(v, ..)) => max_value = v
_ => ()
}
}
// Format and pattern
if map.contains("format") {
match map.get("format") {
Some(String(s)) => format_val = s
_ => ()
}
}
if map.contains("pattern") {
match map.get("pattern") {
Some(String(s)) => pattern_val = s
_ => ()
}
}
// Type-specific flags
if map.contains("type") {
match map.get("type") {
Some(String(s)) => if s == "integer" { is_int_val = true }
_ => ()
}
}
// Numeric constraints
if map.contains("exclusiveMinimum") {
match map.get("exclusiveMinimum") {
Some(Number(v, ..)) => if v == 0.0 { is_positive_val = true }
_ => ()
}
}
if map.contains("exclusiveMaximum") {
match map.get("exclusiveMaximum") {
Some(Number(v, ..)) => if v == 0.0 { is_negative_val = true }
_ => ()
}
}
if map.contains("multipleOf") {
match map.get("multipleOf") {
Some(Number(v, ..)) => multiple_of_val = v
_ => ()
}
}
}
_ => ()
}
}
// Second pass: collect custom error messages
for rule in rules {
match rule.annotation {
Null =>
// Skip certain standard messages
if rule.message != "String must not be empty" {
custom_messages_val.push(rule.message)
}
_ => ()
}
}
{
min_value,
max_value,
format: format_val,
pattern: pattern_val,
is_int: is_int_val,
is_positive: is_positive_val,
is_negative: is_negative_val,
multiple_of: multiple_of_val,
custom_messages: custom_messages_val,
}
}
///|
/// Convert ConstraintInfo to prompt comment string for strings.
pub fn constraint_info_to_string_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
// Length constraints
if info.min_value >= 0.0 && info.max_value >= 0.0 {
parts.push(
format_double_simple(info.min_value) +
"-" +
format_double_simple(info.max_value) +
" chars",
)
} else if info.min_value >= 0.0 {
parts.push("min: " + format_double_simple(info.min_value))
} else if info.max_value >= 0.0 {
parts.push("max: " + format_double_simple(info.max_value))
}
// Format
if info.format == "email" {
parts.push("email")
} else if info.format == "uri" {
parts.push("url")
} else if !info.format.is_empty() {
parts.push(info.format)
}
// Pattern
if !info.pattern.is_empty() {
parts.push("pattern: " + info.pattern)
}
// Custom messages
for msg in info.custom_messages {
parts.push(msg)
}
if parts.is_empty() {
""
} else {
join_parts(parts)
}
}
///|
/// Convert ConstraintInfo to prompt comment string for numbers.
pub fn constraint_info_to_number_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
// Type
if info.is_int {
parts.push("int")
}
// Range
if info.is_positive {
parts.push("positive")
} else if info.is_negative {
parts.push("negative")
} else if info.min_value >= 0.0 && info.max_value >= 0.0 {
parts.push(
format_double_simple(info.min_value) +
"-" +
format_double_simple(info.max_value),
)
} else if info.min_value >= 0.0 {
parts.push("min: " + format_double_simple(info.min_value))
} else if info.max_value >= 0.0 {
parts.push("max: " + format_double_simple(info.max_value))
}
// Special constraints
if info.multiple_of > 0.0 {
parts.push("multiple of " + format_double_simple(info.multiple_of))
}
// Custom messages
for msg in info.custom_messages {
parts.push(msg)
}
if parts.is_empty() {
""
} else {
join_parts(parts)
}
}
///|
/// Convert ConstraintInfo to prompt comment string for arrays.
pub fn constraint_info_to_array_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
// Item count (use min/max format, not range format like strings)
if info.min_value >= 0.0 {
parts.push("min: " + format_double_simple(info.min_value) + " items")
}
if info.max_value >= 0.0 {
parts.push("max: " + format_double_simple(info.max_value) + " items")
}
// Custom messages
for msg in info.custom_messages {
parts.push(msg)
}
if parts.is_empty() {
""
} else {
join_parts(parts)
}
}
///|
/// Convert ConstraintInfo to prompt comment string (fallback for unknown types).
pub fn constraint_info_to_fallback_comment(info : ConstraintInfo) -> String {
let parts : Array[String] = []
for msg in info.custom_messages {
parts.push(msg)
}
if parts.is_empty() {
""
} else {
join_parts(parts)
}
}
///|
/// Extract and format constraint comment for a schema (without description).
/// Dispatches to type-appropriate formatter based on the unwrapped schema type.
pub fn constraint_comment(schema : Schema) -> String {
let unwrapped = unwrap_schema(schema)
let info = extract_constraints(unwrapped.rules)
match unwrapped.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)
}
}