///|
/// A duplicate key issue reported by `validate_unique_key`.
pub(all) struct CsvDuplicateKeyError {
first_row : Int
duplicate_row : Int
key : String
} derive(Eq, Debug)
///|
/// Validate that all required columns exist in the table header.
pub fn validate_required_columns(
table : CsvTable,
columns : Array[String],
) -> Array[CsvValidationError] {
let errors : Array[CsvValidationError] = Array::new()
for column in columns {
if schema_column_index(table.headers, column) is None {
errors.push({ row: 1, column, message: "missing required column" })
}
}
errors
}
///|
/// Validate that each row has exactly the same number of cells as the header.
pub fn validate_row_widths(table : CsvTable) -> Array[CsvValidationError] {
let errors : Array[CsvValidationError] = Array::new()
let expected = table.headers.length()
for i in 0.. Array[CsvValidationError] {
match schema_column_index(table.headers, column) {
Some(index) => {
let errors : Array[CsvValidationError] = Array::new()
for row_index in 0..
if number < minimum {
errors.push({
row: row_index + 2,
column,
message: "expected number >= \{minimum}, got \{value}",
})
}
None =>
errors.push({
row: row_index + 2,
column,
message: "expected number, got \{value}",
})
}
}
}
errors
}
None => [{ row: 1, column, message: "missing column" }]
}
}
///|
/// Validate that all non-empty numeric values are at most `maximum`.
pub fn validate_max_number(
table : CsvTable,
column : String,
maximum : Double,
) -> Array[CsvValidationError] {
match schema_column_index(table.headers, column) {
Some(index) => {
let errors : Array[CsvValidationError] = Array::new()
for row_index in 0..
if number > maximum {
errors.push({
row: row_index + 2,
column,
message: "expected number <= \{maximum}, got \{value}",
})
}
None =>
errors.push({
row: row_index + 2,
column,
message: "expected number, got \{value}",
})
}
}
}
errors
}
None => [{ row: 1, column, message: "missing column" }]
}
}
///|
/// Validate that values are in an allowed set. Empty values are allowed when `required` is false.
pub fn validate_allowed_values(
table : CsvTable,
column : String,
allowed : Array[String],
required : Bool,
) -> Array[CsvValidationError] {
match schema_column_index(table.headers, column) {
Some(index) => {
let errors : Array[CsvValidationError] = Array::new()
for row_index in 0.. [{ row: 1, column, message: "missing column" }]
}
}
///|
/// Validate uniqueness for one or more key columns.
pub fn validate_unique_key(
table : CsvTable,
columns : Array[String],
) -> Array[CsvDuplicateKeyError] {
let indices : Array[Int] = Array::new()
for column in columns {
match schema_column_index(table.headers, column) {
Some(index) => indices.push(index)
None => ()
}
}
if indices.length() != columns.length() {
return []
}
let keys : Array[String] = Array::new()
let first_rows : Array[Int] = Array::new()
let errors : Array[CsvDuplicateKeyError] = Array::new()
for row_index in 0..
errors.push({
first_row: first_rows[existing],
duplicate_row: row_index + 2,
key,
})
None => {
keys.push(key)
first_rows.push(row_index + 2)
}
}
}
errors
}
///|
/// Convert duplicate key errors into a human-readable report.
pub fn duplicate_key_errors_to_text(
errors : Array[CsvDuplicateKeyError],
) -> String {
if errors.length() == 0 {
"ok"
} else {
let out = StringBuilder()
for i in 0.. 0 {
out.write_char('\n')
}
let error = errors[i]
out.write_string(
"row \{error.duplicate_row} duplicates row \{error.first_row}: \{error.key}",
)
}
out.to_string()
}
}
///|
/// Merge multiple validation error arrays into one array.
pub fn validation_errors_concat(
first : Array[CsvValidationError],
second : Array[CsvValidationError],
) -> Array[CsvValidationError] {
let errors : Array[CsvValidationError] = Array::new()
for error in first {
errors.push(error)
}
for error in second {
errors.push(error)
}
errors
}
///|
/// Render a schema validation summary as Markdown.
pub fn schema_validation_markdown(errors : Array[CsvValidationError]) -> String {
if errors.length() == 0 {
"## Schema Validation\n\nNo validation errors."
} else {
let out = StringBuilder()
out.write_string("## Schema Validation\n\n")
out.write_string("| row | column | message |\n")
out.write_string("| ---: | --- | --- |\n")
for error in errors {
out.write_string("| \{error.row} | ")
schema_write_markdown_cell(out, error.column)
out.write_string(" | ")
schema_write_markdown_cell(out, error.message)
out.write_string(" |\n")
}
out.to_string()
}
}
///|
fn schema_column_index(headers : Array[String], column : String) -> Int? {
for i in 0.. String {
if index >= 0 && index < row.length() {
row[index]
} else {
""
}
}
///|
fn schema_contains(values : Array[String], value : String) -> Bool {
for item in values {
if item == value {
return true
}
}
false
}
///|
fn schema_index_of(values : Array[String], value : String) -> Int? {
for i in 0.. Double? {
try @string.parse_double(value) catch {
_ => None
} noraise {
number => Some(number)
}
}
///|
fn schema_key(row : Array[String], indices : Array[Int]) -> String {
let out = StringBuilder()
for i in 0.. 0 {
out.write_char('|')
}
out.write_string(schema_cell_at(row, indices[i]))
}
out.to_string()
}
///|
fn schema_write_markdown_cell(out : StringBuilder, value : String) -> Unit {
for ch in value.iter() {
if ch == '|' {
out.write_string("\\|")
} else if ch == '\n' || ch == '\r' {
out.write_char(' ')
} else {
out.write_char(ch)
}
}
}