///|
pub fn ModelRule::new(
code~ : String,
description~ : String,
severity? : DiagnosticSeverity = Warning,
) -> ModelRule {
{
code,
description,
severity,
entity_type: None,
minimum_arguments: None,
required_attributes: [],
required_properties: [],
minimum_value: None,
maximum_value: None,
}
}
///|
pub fn ModelRule::for_entity(
self : ModelRule,
entity_type : String,
) -> ModelRule {
{ ..self, entity_type: Some(entity_type) }
}
///|
pub fn ModelRule::with_minimum_arguments(
self : ModelRule,
count : Int,
) -> ModelRule {
{ ..self, minimum_arguments: Some(if count < 0 { 0 } else { count }) }
}
///|
pub fn ModelRule::requiring_attribute(
self : ModelRule,
index : Int,
) -> ModelRule {
let attributes = self.required_attributes.copy()
if index >= 0 && !attributes.contains(index) {
attributes.push(index)
}
{ ..self, required_attributes: attributes }
}
///|
pub fn ModelRule::requiring_property(
self : ModelRule,
name : String,
) -> ModelRule {
let properties = self.required_properties.copy()
if !properties.contains(name) {
properties.push(name)
}
{ ..self, required_properties: properties }
}
///|
pub fn ModelRule::with_value_range(
self : ModelRule,
minimum : Double?,
maximum : Double?,
) -> ModelRule {
{ ..self, minimum_value: minimum, maximum_value: maximum }
}
///|
pub fn ModelRule::is_error(self : ModelRule) -> Bool {
self.severity == Error
}
///|
pub fn ModelRule::headline(self : ModelRule) -> String {
self.code + ": " + self.description
}
///|
fn rule_entity_matches(rule : ModelRule, entity : Entity) -> Bool {
match rule.entity_type {
Some(name) => entity.name.to_upper() == name.to_upper()
None => true
}
}
///|
fn rule_entity_product(products : Array[Product], id : Int) -> Product? {
for product in products {
if product.id == id {
return Some(product)
}
}
None
}
///|
fn rule_issue(
rule : ModelRule,
entity : Entity?,
message : String,
property_name : String?,
) -> RuleViolation {
{
rule_code: rule.code,
severity: rule.severity,
entity_id: entity.map(item => item.id),
entity_type: entity.map(item => item.name),
message,
property_name,
}
}
///|
fn rule_arguments_have_value(entity : Entity, index : Int) -> Bool {
match entity.arguments.get(index) {
Some(Null | Omitted) | None => false
Some(_) => true
}
}
///|
fn rule_numeric_values(entity : Entity) -> Array[Double] {
let values : Array[Double] = []
fn visit(value : Value, result : Array[Double]) -> Unit {
match value {
Integer(number) => result.push(number.to_double())
Real(number) => result.push(number)
Typed(name~, value~) => {
ignore(name)
visit(value, result)
}
List(items) =>
for item in items {
visit(item, result)
}
_ => ()
}
}
for value in entity.arguments {
visit(value, values)
}
values
}
///|
fn rule_value_in_range(rule : ModelRule, entity : Entity) -> Bool {
let numbers = rule_numeric_values(entity)
for number in numbers {
match (rule.minimum_value, rule.maximum_value) {
(Some(minimum), Some(maximum)) if number < minimum || number > maximum =>
return false
(Some(minimum), None) if number < minimum => return false
(None, Some(maximum)) if number > maximum => return false
_ => ()
}
}
true
}
///|
fn rule_entity_violations(
index : ModelIndex,
products : Array[Product],
rule : ModelRule,
entity : Entity,
) -> Array[RuleViolation] raise ParseError {
if !rule_entity_matches(rule, entity) {
return []
}
let result : Array[RuleViolation] = []
match rule.minimum_arguments {
Some(count) if entity.arguments.length() < count =>
result.push(
rule_issue(
rule,
Some(entity),
"expected at least " + count.to_string() + " arguments",
None,
),
)
_ => ()
}
for argument in rule.required_attributes {
if !rule_arguments_have_value(entity, argument) {
result.push(
rule_issue(
rule,
Some(entity),
"required argument " + argument.to_string() + " is missing",
None,
),
)
}
}
if !rule_value_in_range(rule, entity) {
result.push(
rule_issue(
rule,
Some(entity),
"numeric argument is outside the allowed range",
None,
),
)
}
if rule.required_properties.length() > 0 {
match rule_entity_product(products, entity.id) {
Some(product) => {
let properties = property_sets_for_product(index, product)
for name in rule.required_properties {
if property_named(properties, name) is None {
result.push(
rule_issue(
rule,
Some(entity),
"required property is missing",
Some(name),
),
)
}
}
}
None => ()
}
}
result
}
///|
fn rule_duplicate_ids(model : Model, rule : ModelRule) -> Array[RuleViolation] {
let seen : Map[Int, Bool] = Map([])
let result : Array[RuleViolation] = []
for entity in model.entities {
if seen.contains(entity.id) {
result.push(
rule_issue(rule, Some(entity), "entity id occurs more than once", None),
)
}
seen[entity.id] = true
}
result
}
///|
fn rule_orphan_references(
model : Model,
rule : ModelRule,
) -> Array[RuleViolation] {
let index = model.index()
let result : Array[RuleViolation] = []
for entity in model.entities {
for target in entity_references(entity) {
if !index.has(target) {
result.push(
rule_issue(
rule,
Some(entity),
"reference target #" + target.to_string() + " is absent",
None,
),
)
}
}
}
result
}
///|
fn rule_product_geometry(
model : Model,
rule : ModelRule,
) -> Array[RuleViolation] {
let index = model.index()
let result : Array[RuleViolation] = []
for product in model.products() {
if product.representation is Some(_) {
let geometries = geometries_for_product(index, product)
if geometries.length() == 0 {
result.push(
rule_issue(
rule,
index.entity(product.id),
"representation has no decodable geometry",
None,
),
)
}
}
}
result
}
///|
fn rule_product_names(model : Model, rule : ModelRule) -> Array[RuleViolation] {
let result : Array[RuleViolation] = []
for product in model.products() {
if product.name.unwrap_or("").trim().length() == 0 {
result.push({
rule_code: rule.code,
severity: rule.severity,
entity_id: Some(product.id),
entity_type: Some(product.ifc_type),
message: "product has no display name",
property_name: None,
})
}
}
result
}
///|
fn rule_global_violations(
model : Model,
rule : ModelRule,
) -> Array[RuleViolation] {
match rule.code {
"MODEL_UNIQUE_ID" => rule_duplicate_ids(model, rule)
"MODEL_REFERENCE_TARGET" => rule_orphan_references(model, rule)
"MODEL_GEOMETRY_DECODE" => rule_product_geometry(model, rule)
"PRODUCT_DISPLAY_NAME" => rule_product_names(model, rule)
_ => []
}
}
///|
fn rule_count_severity(violations : Array[RuleViolation]) -> (Int, Int) {
violations.fold(init=(0, 0), (result, violation) => {
let (errors, warnings) = result
match violation.severity {
Error => (errors + 1, warnings)
Warning => (errors, warnings + 1)
Note => (errors, warnings)
}
})
}
///|
/// Apply custom rules plus model-wide integrity rules without changing the model.
pub fn evaluate_rules(
model : Model,
rules : Array[ModelRule],
) -> RuleReport raise ParseError {
let index = model.index()
let products = model.products()
let violations : Array[RuleViolation] = []
for rule in rules {
if rule.code == "MODEL_UNIQUE_ID" ||
rule.code == "MODEL_REFERENCE_TARGET" ||
rule.code == "MODEL_GEOMETRY_DECODE" ||
rule.code == "PRODUCT_DISPLAY_NAME" {
for violation in rule_global_violations(model, rule) {
violations.push(violation)
}
} else {
for entity in model.entities {
for violation in rule_entity_violations(index, products, rule, entity) {
violations.push(violation)
}
}
}
}
let (error_count, warning_count) = rule_count_severity(violations)
{
rules,
violations,
error_count,
warning_count,
checked_entities: model.entities.length(),
checked_products: products.length(),
}
}
///|
pub fn standard_model_rules() -> Array[ModelRule] {
[
ModelRule::new(
code="MODEL_UNIQUE_ID",
description="entity ids are unique",
severity=Error,
),
ModelRule::new(
code="MODEL_REFERENCE_TARGET",
description="all references resolve to an entity",
severity=Error,
),
ModelRule::new(
code="MODEL_GEOMETRY_DECODE",
description="declared product representations have decodable geometry",
severity=Warning,
),
]
}
///|
pub fn delivery_model_rules() -> Array[ModelRule] {
standard_model_rules() +
[
ModelRule::new(
code="PRODUCT_DISPLAY_NAME",
description="products have a display name for coordination views",
severity=Warning,
),
]
}
///|
pub fn rule_report(
model : Model,
rules : Array[ModelRule],
) -> RuleReport raise ParseError {
evaluate_rules(model, rules)
}
///|
pub fn delivery_rule_report(model : Model) -> RuleReport raise ParseError {
evaluate_rules(model, delivery_model_rules())
}
///|
pub fn RuleReport::is_valid(self : RuleReport) -> Bool {
self.error_count == 0
}
///|
pub fn RuleReport::violation_count(self : RuleReport) -> Int {
self.violations.length()
}
///|
pub fn RuleReport::errors(self : RuleReport) -> Array[RuleViolation] {
self.violations.filter(violation => violation.severity == Error)
}
///|
pub fn RuleReport::warnings(self : RuleReport) -> Array[RuleViolation] {
self.violations.filter(violation => violation.severity == Warning)
}
///|
pub fn RuleReport::for_code(
self : RuleReport,
code : String,
) -> Array[RuleViolation] {
self.violations.filter(violation => violation.rule_code == code)
}
///|
pub fn RuleReport::for_entity(
self : RuleReport,
id : Int,
) -> Array[RuleViolation] {
self.violations.filter(violation => violation.entity_id == Some(id))
}
///|
pub fn RuleReport::codes(self : RuleReport) -> Array[String] {
let result : Array[String] = []
for violation in self.violations {
if !result.contains(violation.rule_code) {
result.push(violation.rule_code)
}
}
result
}
///|
fn rule_severity_text(severity : DiagnosticSeverity) -> String {
match severity {
Error => "error"
Warning => "warning"
Note => "note"
}
}
///|
pub fn RuleViolation::headline(self : RuleViolation) -> String {
let entity = self.entity_id.map(id => " #" + id.to_string()).unwrap_or("")
self.rule_code +
entity +
" [" +
rule_severity_text(self.severity) +
"]: " +
self.message
}
///|
pub fn RuleReport::headline(self : RuleReport) -> String {
"valid=" +
self.is_valid().to_string() +
" errors=" +
self.error_count.to_string() +
" warnings=" +
self.warning_count.to_string() +
" checked_entities=" +
self.checked_entities.to_string()
}
///|
pub fn RuleReport::format(self : RuleReport) -> String {
let builder = StringBuilder()
builder <+ "\{self.headline()}\n"
for violation in self.violations {
builder <+ "- \{violation.headline()}\n"
}
builder.to_string()
}
///|
pub fn RuleReport::to_json(self : RuleReport) -> String {
let values = self.violations.map(violation => {
"{\"code\":\"" +
violation.rule_code +
"\",\"severity\":\"" +
rule_severity_text(violation.severity) +
"\",\"entity_id\":" +
violation.entity_id.map(id => id.to_string()).unwrap_or("null") +
",\"message\":\"" +
violation.message +
"\"}"
})
"{\"valid\":" +
self.is_valid().to_string() +
",\"error_count\":" +
self.error_count.to_string() +
",\"warning_count\":" +
self.warning_count.to_string() +
",\"checked_entities\":" +
self.checked_entities.to_string() +
",\"violations\":[" +
values.join(",") +
"]}"
}
///|
pub fn RuleReport::to_markdown(self : RuleReport) -> String {
let builder = StringBuilder()
builder <+ "# Model rule report\n\n"
builder <+ "- Valid: \{self.is_valid()}\n"
builder <+ "- Errors: \{self.error_count}\n"
builder <+ "- Warnings: \{self.warning_count}\n"
builder <+ "- Checked entities: \{self.checked_entities}\n\n"
if self.violations.length() == 0 {
builder <+ "No violations.\n"
} else {
builder <+ "| Rule | Entity | Severity | Message |\n"
builder <+ "| --- | ---: | --- | --- |\n"
for violation in self.violations {
let entity_text = violation.entity_id
.map(id => id.to_string())
.unwrap_or("")
builder <+
"| \{violation.rule_code} | \{entity_text} | \{rule_severity_text(violation.severity)} | \{violation.message} |\n"
}
}
builder.to_string()
}
///|
pub fn validate_rule(
model : Model,
rule : ModelRule,
) -> RuleReport raise ParseError {
evaluate_rules(model, [rule])
}
///|
pub fn RuleReport::merge(self : RuleReport, other : RuleReport) -> RuleReport {
let violations = self.violations + other.violations
{
rules: self.rules + other.rules,
violations,
error_count: self.error_count + other.error_count,
warning_count: self.warning_count + other.warning_count,
checked_entities: if self.checked_entities > other.checked_entities {
self.checked_entities
} else {
other.checked_entities
},
checked_products: if self.checked_products > other.checked_products {
self.checked_products
} else {
other.checked_products
},
}
}
///|
pub fn rule_violation_codes(report : RuleReport) -> Map[String, Int] {
report.violations.fold(init=Map([]), (result, violation) => {
result.update_or_default(violation.rule_code, 1, count => count + 1)
result
})
}
///|
pub fn rule_names(rules : Array[ModelRule]) -> Array[String] {
rules.map(rule => rule.code)
}