///|
pub enum RuleOperator {
GreaterThan
LessThan
EqualTo
AtLeast
AtMost
} derive(Debug, Eq)
///|
pub fn greater_than() -> RuleOperator {
GreaterThan
}
///|
pub fn less_than() -> RuleOperator {
LessThan
}
///|
pub fn equal_to() -> RuleOperator {
EqualTo
}
///|
pub enum RuleSeverity {
RuleInfo
RuleWarning
RuleError
} derive(Debug, Eq)
///|
pub fn rule_warning() -> RuleSeverity {
RuleWarning
}
///|
pub fn rule_error() -> RuleSeverity {
RuleError
}
///|
pub fn rule_info() -> RuleSeverity {
RuleInfo
}
///|
pub struct Rule {
code : String
operator : RuleOperator
field : String
limit : Float
severity : RuleSeverity
} derive(Debug, Eq)
///|
pub fn rule(
code : String,
operator : RuleOperator,
field : String,
limit : Float,
severity : RuleSeverity,
) -> Rule {
{ code, operator, field, limit, severity }
}
///|
pub struct RuleValue {
field : String
value : Float
} derive(Debug, Eq)
///|
pub fn rule_value(field : String, value : Float) -> RuleValue {
{ field, value }
}
///|
pub struct RuleViolation {
code : String
field : String
actual : Float
limit : Float
severity : RuleSeverity
message : String
} derive(Debug, Eq)
///|
pub struct RuleEvaluation {
passed : Bool
violations : Array[RuleViolation]
checked : Int
} derive(Debug, Eq)
///|
fn rule_value_for(values : Array[RuleValue], field : String) -> Float? {
for value in values {
if value.field == field {
return Some(value.value)
}
}
None
}
///|
fn rule_passes(operator : RuleOperator, actual : Float, limit : Float) -> Bool {
match operator {
GreaterThan => actual > limit
LessThan => actual < limit
EqualTo => (actual - limit).abs() < 0.000001
AtLeast => actual >= limit
AtMost => actual <= limit
}
}
///|
fn rule_message(
operator : RuleOperator,
field : String,
actual : Float,
limit : Float,
) -> String {
let operation = match operator {
GreaterThan => ">"
LessThan => "<"
EqualTo => "="
AtLeast => ">="
AtMost => "<="
}
field +
"=" +
actual.to_string() +
" must satisfy " +
operation +
" " +
limit.to_string()
}
///|
pub fn evaluate_rules(
rules : Array[Rule],
values : Array[RuleValue],
) -> RuleEvaluation {
let violations : Array[RuleViolation] = []
let mut checked = 0
for item in rules {
match rule_value_for(values, item.field) {
Some(actual) => {
checked = checked + 1
if !rule_passes(item.operator, actual, item.limit) {
violations.push({
code: item.code,
field: item.field,
actual,
limit: item.limit,
severity: item.severity,
message: rule_message(item.operator, item.field, actual, item.limit),
})
}
}
None =>
violations.push({
code: item.code,
field: item.field,
actual: 0.0,
limit: item.limit,
severity: item.severity,
message: item.field + " is missing",
})
}
}
{ passed: violations.length() == 0, violations, checked }
}
///|
pub fn RuleEvaluation::error_count(self : RuleEvaluation) -> Int {
let mut count = 0
for violation in self.violations {
if violation.severity is RuleError {
count = count + 1
}
}
count
}
///|
pub fn RuleEvaluation::warning_count(self : RuleEvaluation) -> Int {
let mut count = 0
for violation in self.violations {
if violation.severity is RuleWarning {
count = count + 1
}
}
count
}
///|
pub fn RuleEvaluation::to_table(self : RuleEvaluation) -> ReportTable {
let rows : Array[Array[String]] = []
for violation in self.violations {
rows.push([
violation.code,
violation.field,
"{violation.actual}",
"{violation.limit}",
violation.message,
])
}
table(["code", "field", "actual", "limit", "message"], rows)
}
///|
pub fn RuleEvaluation::to_diagnostics(
self : RuleEvaluation,
) -> Array[Diagnostic] {
let result : Array[Diagnostic] = []
for violation in self.violations {
let severity = match violation.severity {
RuleInfo => DiagHint
RuleWarning => DiagWarning
RuleError => DiagError
}
result.push(
diagnostic(
violation.code,
severity,
violation.field,
violation.message,
"review the input and calculation assumptions",
),
)
}
result
}
///|
pub fn rule_gate(rules : Array[Rule], values : Array[RuleValue]) -> Bool {
evaluate_rules(rules, values).passed
}
///|
pub fn merge_rules(left : Array[Rule], right : Array[Rule]) -> Array[Rule] {
let result = left.copy()
for item in right {
result.push(item)
}
result
}
///|
pub fn rule_codes(rules : Array[Rule]) -> Array[String] {
rules.map(fn(item) { item.code })
}
///|
pub fn rule_fields(rules : Array[Rule]) -> Array[String] {
rules.map(fn(item) { item.field })
}
///|
pub fn rule_summary(evaluation : RuleEvaluation) -> String {
"checked=" +
evaluation.checked.to_string() +
", violations=" +
evaluation.violations.length().to_string() +
", errors=" +
evaluation.error_count().to_string()
}
///|
pub fn positive_rule(
code : String,
field : String,
severity : RuleSeverity,
) -> Rule {
rule(code, GreaterThan, field, 0.0, severity)
}
///|
pub fn non_negative_rule(
code : String,
field : String,
severity : RuleSeverity,
) -> Rule {
rule(code, AtLeast, field, 0.0, severity)
}
///|
pub fn upper_bound_rule(
code : String,
field : String,
limit : Float,
severity : RuleSeverity,
) -> Rule {
rule(code, AtMost, field, limit, severity)
}
///|
pub fn lower_bound_rule(
code : String,
field : String,
limit : Float,
severity : RuleSeverity,
) -> Rule {
rule(code, AtLeast, field, limit, severity)
}