///|
/// Schema helpers for named finite-domain models.
///
/// A schema is a stable, serializable view of variables and defaults. It is
/// useful for CLI forms, configuration validation, and checking that a saved
/// solution still matches the model shape after a library upgrade.
pub struct SchemaField {
id : Int
name : String
lower : Int
upper : Int
default : Int
}
///|
/// Create a field.
pub fn schema_field(
id : Int,
name : String,
lower : Int,
upper : Int,
default : Int,
) -> SchemaField {
{ id, name, lower, upper, default }
}
///|
/// Return whether the field is valid.
pub fn SchemaField::valid(self : SchemaField) -> Bool {
self.id >= 0 &&
self.name.trim() != "" &&
self.lower <= self.upper &&
self.default >= self.lower &&
self.default <= self.upper
}
///|
/// Return its finite domain.
pub fn SchemaField::domain(self : SchemaField) -> Domain {
domain(self.lower, self.upper)
}
///|
/// A model schema.
pub struct ModelSchema {
name : String
fields : Array[SchemaField]
}
///|
/// Create an empty schema.
pub fn model_schema(name : String) -> ModelSchema {
{ name, fields: [] }
}
///|
/// Add a field if its id and name are unique.
pub fn ModelSchema::add(self : ModelSchema, field : SchemaField) -> Bool {
if !field.valid() || field.id != self.fields.length() {
return false
}
for current in self.fields {
if current.name == field.name {
return false
}
}
self.fields.push(field)
true
}
///|
/// Return schema field count.
pub fn ModelSchema::length(self : ModelSchema) -> Int {
self.fields.length()
}
///|
/// Read a field.
pub fn ModelSchema::field(self : ModelSchema, id : Int) -> SchemaField {
if id < 0 || id >= self.fields.length() {
abort("schema field is outside the schema")
}
self.fields[id]
}
///|
/// Find a field by name.
pub fn ModelSchema::find(self : ModelSchema, name : String) -> SchemaField? {
for field in self.fields {
if field.name == name {
return Some(field)
}
}
None
}
///|
/// Return default values.
pub fn ModelSchema::defaults(self : ModelSchema) -> Array[Int] {
self.fields.map(field => field.default)
}
///|
/// Return field names.
pub fn ModelSchema::names(self : ModelSchema) -> Array[String] {
self.fields.map(field => field.name)
}
///|
/// Validate values against the schema.
pub fn ModelSchema::validate(
self : ModelSchema,
values : Array[Int],
) -> ValidationReport {
let report = validation_report()
if values.length() != self.fields.length() {
report.error("schema-length", "values", "value count differs from schema")
return report
}
for index, field in self.fields {
ignore(
validate_range(
report,
"values[\{index}]",
values[index],
field.lower,
field.upper,
),
)
}
report
}
///|
/// Return a schema report.
pub fn ModelSchema::report(self : ModelSchema) -> ValidationReport {
let report = validation_report()
if self.name.trim() == "" {
report.error("schema-name", "name", "schema name is blank")
}
let names : Array[String] = []
for field in self.fields {
if !field.valid() {
report.error(
"field-invalid",
"fields[\{field.id}]",
"field definition is invalid",
)
}
names.push(field.name)
}
ignore(validate_names(report, "fields", names))
report
}
///|
/// Render a schema as CSV.
pub fn ModelSchema::csv(self : ModelSchema) -> String {
let builder = StringBuilder()
for index, field in self.fields {
if index > 0 {
builder.write_char('\n')
}
builder.write_string(
"\{field.id},\{field.name},\{field.lower},\{field.upper},\{field.default}",
)
}
builder.to_string()
}
///|
/// Return an assignment difference from defaults.
pub fn ModelSchema::changed_defaults(
self : ModelSchema,
values : Array[Int],
) -> Array[(Int, Int, Int)] {
let result : Array[(Int, Int, Int)] = []
let defaults = self.defaults()
let limit = if values.length() < defaults.length() {
values.length()
} else {
defaults.length()
}
for index in 0.. Int {
let mut result = 31
for field in self.fields {
result = result * 37 +
field.id * 3 +
field.lower * 5 +
field.upper * 7 +
field.default
}
result
}
///|
/// Return the Cartesian size capped at a limit.
pub fn ModelSchema::cartesian_size(self : ModelSchema, limit : Int) -> Int {
let cap = if limit < 1 { 1 } else { limit }
let mut result = 1
for field in self.fields {
result *= field.upper - field.lower + 1
if result >= cap {
return cap
}
}
result
}
///|
/// Return whether two schemas have the same field shape.
pub fn schemas_compatible(left : ModelSchema, right : ModelSchema) -> Bool {
if left.fields.length() != right.fields.length() {
return false
}
for index in 0.. String {
"name=\{self.name}, fields=\{self.fields.length()}, combinations=\{self.cartesian_size(1000000)}, valid=\{self.report().valid()}"
}