///|
/// Reusable validation and diagnostics for application-facing APIs.
///
/// Validation results carry machine-readable codes and paths so command-line
/// clients can show actionable failures without parsing exception text. The
/// report is immutable from a caller's perspective but uses arrays internally
/// for low-allocation model construction.
pub enum ValidationSeverity {
ValidationInfo
ValidationWarning
ValidationError
}
///|
/// One validation finding.
pub struct ValidationIssue {
code : String
path : String
message : String
severity : ValidationSeverity
}
///|
/// Construct an issue.
pub fn validation_issue(
code : String,
path : String,
message : String,
severity : ValidationSeverity,
) -> ValidationIssue {
{ code, path, message, severity }
}
///|
/// Read issue code.
pub fn ValidationIssue::code(self : ValidationIssue) -> String {
self.code
}
///|
/// Read issue path.
pub fn ValidationIssue::path(self : ValidationIssue) -> String {
self.path
}
///|
/// Read issue message.
pub fn ValidationIssue::message(self : ValidationIssue) -> String {
self.message
}
///|
/// Return a stable severity label.
pub fn ValidationIssue::severity_name(self : ValidationIssue) -> String {
match self.severity {
ValidationInfo => "info"
ValidationWarning => "warning"
ValidationError => "error"
}
}
///|
/// Render an issue for logs.
pub fn ValidationIssue::describe(self : ValidationIssue) -> String {
"\{self.severity_name()}:\{self.code}:\{self.path}:\{self.message}"
}
///|
/// A collection of findings.
pub struct ValidationReport {
issues : Array[ValidationIssue]
}
///|
/// Create an empty report.
pub fn validation_report() -> ValidationReport {
{ issues: [] }
}
///|
/// Add an issue.
pub fn ValidationReport::push(
self : ValidationReport,
issue : ValidationIssue,
) -> Unit {
self.issues.push(issue)
}
///|
/// Add an error.
pub fn ValidationReport::error(
self : ValidationReport,
code : String,
path : String,
message : String,
) -> Unit {
self.push(validation_issue(code, path, message, ValidationError))
}
///|
/// Add a warning.
pub fn ValidationReport::warning(
self : ValidationReport,
code : String,
path : String,
message : String,
) -> Unit {
self.push(validation_issue(code, path, message, ValidationWarning))
}
///|
/// Add an informational note.
pub fn ValidationReport::info(
self : ValidationReport,
code : String,
path : String,
message : String,
) -> Unit {
self.push(validation_issue(code, path, message, ValidationInfo))
}
///|
/// Return issue count.
pub fn ValidationReport::length(self : ValidationReport) -> Int {
self.issues.length()
}
///|
/// Return whether no errors exist.
pub fn ValidationReport::valid(self : ValidationReport) -> Bool {
self.error_count() == 0
}
///|
/// Count errors.
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
let mut result = 0
for issue in self.issues {
if issue.severity is ValidationError {
result += 1
}
}
result
}
///|
/// Count warnings.
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
let mut result = 0
for issue in self.issues {
if issue.severity is ValidationWarning {
result += 1
}
}
result
}
///|
/// Return copied issues.
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues.copy()
}
///|
/// Render all issues as newline-separated lines.
pub fn ValidationReport::describe(self : ValidationReport) -> String {
let builder = StringBuilder()
for index, issue in self.issues {
if index > 0 {
builder.write_char('\n')
}
builder.write_string(issue.describe())
}
builder.to_string()
}
///|
/// Validate an inclusive integer range.
pub fn validate_range(
report : ValidationReport,
path : String,
value : Int,
lower : Int,
upper : Int,
) -> Bool {
if lower > upper {
report.error("invalid-range", path, "lower bound exceeds upper bound")
return false
}
if value < lower || value > upper {
report.error("out-of-range", path, "value is outside the inclusive range")
return false
}
true
}
///|
/// Validate a non-negative integer.
pub fn validate_nonnegative(
report : ValidationReport,
path : String,
value : Int,
) -> Bool {
if value < 0 {
report.error("negative-value", path, "value must be non-negative")
return false
}
true
}
///|
/// Validate an array's exact length.
pub fn validate_length(
report : ValidationReport,
path : String,
values : Array[Int],
expected : Int,
) -> Bool {
if values.length() != expected {
report.error(
"length-mismatch", path, "array length does not match the schema",
)
return false
}
true
}
///|
/// Validate an array is nondecreasing.
pub fn validate_nondecreasing(
report : ValidationReport,
path : String,
values : Array[Int],
) -> Bool {
for index in 1.. Bool {
for index in 1.. Bool {
for left in 0.. Bool {
let mut result = true
for index, value in values {
if !allowed.contains(value) {
report.error(
"invalid-member",
"\{path}[\{index}]",
"value is not in the allowed set",
)
result = false
}
}
result
}
///|
/// Validate matrix shape.
pub fn validate_matrix_shape(
report : ValidationReport,
path : String,
matrix : Array[Array[Int]],
rows : Int,
columns : Int,
) -> Bool {
if matrix.length() != rows {
report.error("row-count-mismatch", path, "matrix row count is invalid")
return false
}
let mut result = true
for index, row in matrix {
if row.length() != columns {
report.error(
"column-count-mismatch",
"\{path}[\{index}]",
"matrix column count is invalid",
)
result = false
}
}
result
}
///|
/// Validate all domains are non-empty.
pub fn validate_domains(
report : ValidationReport,
domains : Array[Domain],
) -> Bool {
let mut result = true
for index, value_domain in domains {
if value_domain.is_empty() {
report.error(
"empty-domain",
"domains[\{index}]",
"domain contains no candidate",
)
result = false
}
}
result
}
///|
/// Validate every assignment fits its variable domain.
pub fn validate_assignment_domains(
report : ValidationReport,
domains : Array[Domain],
values : Array[Int],
) -> Bool {
if domains.length() != values.length() {
report.error(
"assignment-length", "values", "assignment and domain lengths differ",
)
return false
}
let mut result = true
for index in 0.. Bool {
let variables = solver.variables()
if variables.length() != solution.values.length() {
report.error(
"solution-length", "solution", "solution length differs from model",
)
return false
}
let mut result = true
for index, variable in variables {
if !variable.domain().contains(solution.values[index]) {
report.error(
"solution-domain",
"solution[\{index}]",
"solution value is outside its domain",
)
result = false
}
}
result
}
///|
/// Validate a finite-domain model before solving.
pub fn validate_model(report : ValidationReport, solver : Solver) -> Bool {
if solver.variable_count() == 0 {
report.warning("empty-model", "solver", "model contains no variables")
}
if solver.constraint_count() == 0 {
report.warning(
"unconstrained-model", "solver", "model contains no constraints",
)
}
true
}
///|
/// Validate a text field is not blank.
pub fn validate_text(
report : ValidationReport,
path : String,
value : String,
) -> Bool {
if value.trim() == "" {
report.error("blank-text", path, "text must not be blank")
return false
}
true
}
///|
/// Validate a set of names is unique and non-empty.
pub fn validate_names(
report : ValidationReport,
path : String,
names : Array[String],
) -> Bool {
let mut result = true
for index, name in names {
if name.trim() == "" {
report.error("blank-name", "\{path}[\{index}]", "name must not be blank")
result = false
}
for previous in 0.. Bool {
if table.length() == 0 {
report.warning("empty-table", "table", "table has no rows")
return true
}
let columns = table[0].length()
let mut result = true
for row in table {
if row.length() != columns {
report.error("ragged-table", "table", "rows have different lengths")
result = false
}
for value in row {
if value < 0 {
report.warning(
"negative-entry", "table", "table contains a negative entry",
)
}
}
}
result
}
///|
/// Validate route errors into a structured report.
pub fn validate_routing_report(
report : ValidationReport,
routing : RoutingReport,
) -> Bool {
if routing.errors.length() > 0 {
for error in routing.errors {
report.error("routing-\{error}", "routing", error)
}
return false
}
true
}
///|
/// Return a report from a solver solve attempt.
pub fn solver_validation_report(
solver : Solver,
solution : Solution?,
) -> ValidationReport {
let report = validation_report()
ignore(validate_model(report, solver))
match solution {
Some(value) => ignore(validate_solution(report, solver, value))
None => report.error("unsatisfiable", "solver", "no solution was produced")
}
report
}
///|
/// Return a stable report fingerprint.
pub fn ValidationReport::signature(self : ValidationReport) -> Int {
let mut result = 29
for issue in self.issues {
for character in issue.code {
result = result * 31 + character.to_int()
}
for character in issue.path {
result = result * 37 + character.to_int()
}
}
result
}