///|
pub(all) struct ContractIssue {
code : String
message : String
} derive(Eq, Debug)
///|
/// Inspect a contract before running it so configuration errors are explicit.
pub fn inspect_contract(contract : Contract) -> Array[ContractIssue] {
let issues = []
if contract.name.trim().to_owned() == "" {
add_issue(issues, "missing_name", "contract name must not be empty")
}
if contract.version.trim().to_owned() == "" {
add_issue(issues, "missing_version", "contract version must not be empty")
}
let seen_fields = Map([])
for field in contract.fields {
if field.name.trim().to_owned() == "" {
add_issue(issues, "empty_field_name", "field name must not be empty")
}
if seen_fields.contains(field.name) {
add_issue(
issues,
"duplicate_field",
"field name is declared more than once: " + field.name,
)
} else {
seen_fields[field.name] = ()
}
inspect_field(field, issues)
}
for rule in contract.rules {
inspect_rule(rule, issues)
}
let references = analyze_contract_references(contract)
for field in references.missing_fields {
add_issue(
issues,
"missing_field_reference",
"rule references an undeclared field: " + field,
)
}
issues
}
///|
pub fn contract_field_names(contract : Contract) -> Array[String] {
let names = contract.fields.map(fn(field) { field.name })
names.sort()
names
}
///|
pub fn contract_rule_names(contract : Contract) -> Array[String] {
contract.rules.map(rule_name)
}
///|
pub fn has_field(contract : Contract, name : String) -> Bool {
contract.fields.any(fn(field) { field.name == name })
}
///|
fn inspect_field(field : FieldSpec, issues : Array[ContractIssue]) -> Unit {
match (field.min_int, field.max_int) {
(Some(min), Some(max)) =>
if min > max {
add_issue(
issues,
"invalid_field_range",
"field has min_int greater than max_int: " + field.name,
)
}
_ => ()
}
if field.kind != Int && (field.min_int is Some(_) || field.max_int is Some(_)) {
add_issue(
issues,
"numeric_bounds_on_non_int",
"field has integer bounds but is not an Int field: " + field.name,
)
}
for index, value in field.allowed_values {
if field.allowed_values[index + 1:].contains(value) {
add_issue(
issues,
"duplicate_allowed_value",
"field repeats an allowed value: " + field.name + "=" + value,
)
}
}
}
///|
fn inspect_rule(rule : Rule, issues : Array[ContractIssue]) -> Unit {
match rule {
Unique(fields~, ..) =>
if fields.is_empty() {
add_issue(issues, "empty_unique_fields", "unique rule needs a field")
}
Completeness(field~, min_percent~, ..) =>
if field == "" || min_percent < 0 || min_percent > 100 {
add_issue(
issues, "invalid_completeness", "completeness must use a field and a percentage from 0 to 100",
)
}
Enum(field~, allowed_values~, ..) =>
if field == "" || allowed_values.is_empty() {
add_issue(
issues, "invalid_enum", "enum rule needs a field and at least one allowed value",
)
}
IntRange(field~, min~, max~, ..) =>
if field == "" || min > max {
add_issue(
issues, "invalid_int_range", "int_range needs a field and min <= max",
)
}
CompareInts(left~, right~, ..) =>
if left == "" || right == "" || left == right {
add_issue(
issues, "invalid_compare", "compare_ints needs two different fields",
)
}
PatternMatch(field~, pattern~, ..) =>
if field == "" || pattern == "" {
add_issue(
issues, "invalid_pattern", "pattern_match needs a field and pattern",
)
}
StringLength(field~, min~, max~, ..) =>
if field == "" || min < 0 || min > max {
add_issue(
issues, "invalid_string_length", "string_length needs a field and 0 <= min <= max",
)
}
DistinctCount(field~, min~, max~, ..) =>
if field == "" || min < 0 || min > max {
add_issue(
issues, "invalid_distinct_count", "distinct_count needs a field and 0 <= min <= max",
)
}
RequiredIf(condition_field~, condition_value~, field~, ..) =>
if condition_field == "" ||
condition_value == "" ||
field == "" ||
condition_field == field {
add_issue(
issues, "invalid_required_if", "required_if needs two different fields and a condition value",
)
}
RowCount(min~, max~, ..) =>
match (min, max) {
(Some(minimum), Some(maximum)) =>
if minimum < 0 || minimum > maximum {
add_issue(
issues, "invalid_row_count", "row_count needs 0 <= min <= max",
)
}
(Some(minimum), None) =>
if minimum < 0 {
add_issue(
issues, "invalid_row_count", "row_count minimum must not be negative",
)
}
(None, Some(maximum)) =>
if maximum < 0 {
add_issue(
issues, "invalid_row_count", "row_count maximum must not be negative",
)
}
(None, None) =>
add_issue(
issues, "unbounded_row_count", "row_count needs a minimum or maximum",
)
}
}
}
///|
fn add_issue(
issues : Array[ContractIssue],
code : String,
message : String,
) -> Unit {
issues.push({ code, message })
}