///|
fn make_schema_rule(
entity_name : String,
minimum_arguments : Int,
maximum_arguments : Int?,
required_reference_arguments : Array[Int],
family : String,
description : String,
) -> SchemaRule {
{
entity_name: entity_name.to_upper(),
minimum_arguments,
maximum_arguments,
required_reference_arguments,
family,
description,
}
}
///|
fn common_schema_rules() -> Array[SchemaRule] {
[
make_schema_rule(
"IFCPROJECT",
9,
Some(9),
[1, 7, 8],
"spatial",
"project root",
),
make_schema_rule(
"IFCSITE",
9,
Some(9),
[1, 5, 6, 9],
"spatial",
"site root",
),
make_schema_rule(
"IFCBUILDING",
9,
Some(9),
[1, 5, 6, 9],
"spatial",
"building root",
),
make_schema_rule(
"IFCBUILDINGSTOREY",
10,
Some(10),
[1, 5],
"spatial",
"storey",
),
make_schema_rule("IFCSPACE", 10, Some(10), [1, 5, 6], "spatial", "space"),
make_schema_rule(
"IFCWALL",
8,
Some(8),
[1, 5, 6],
"product",
"wall product",
),
make_schema_rule(
"IFCWALLSTANDARDCASE",
8,
Some(8),
[1, 5, 6],
"product",
"wall product",
),
make_schema_rule(
"IFCWALLELEMENTEDCASE",
8,
Some(8),
[1, 5, 6],
"product",
"wall product",
),
make_schema_rule(
"IFCDOOR",
9,
Some(9),
[1, 5, 6],
"product",
"door product",
),
make_schema_rule(
"IFCWINDOW",
9,
Some(9),
[1, 5, 6],
"product",
"window product",
),
make_schema_rule(
"IFCBEAM",
8,
Some(8),
[1, 5, 6],
"product",
"beam product",
),
make_schema_rule(
"IFCCOLUMN",
8,
Some(8),
[1, 5, 6],
"product",
"column product",
),
make_schema_rule(
"IFCSLAB",
8,
Some(8),
[1, 5, 6],
"product",
"slab product",
),
make_schema_rule(
"IFCROOF",
8,
Some(8),
[1, 5, 6],
"product",
"roof product",
),
make_schema_rule(
"IFCOWNERHISTORY",
8,
Some(8),
[],
"metadata",
"owner history",
),
make_schema_rule(
"IFCLOCALPLACEMENT",
2,
Some(2),
[1],
"placement",
"relative placement",
),
make_schema_rule(
"IFCAXIS2PLACEMENT3D",
3,
Some(3),
[0],
"placement",
"3D placement",
),
make_schema_rule(
"IFCAXIS2PLACEMENT2D",
2,
Some(2),
[0],
"placement",
"2D placement",
),
make_schema_rule(
"IFCCARTESIANPOINT",
1,
Some(1),
[],
"geometry",
"point coordinates",
),
make_schema_rule(
"IFCDIRECTION",
1,
Some(1),
[],
"geometry",
"direction ratios",
),
make_schema_rule(
"IFCEXTRUDEDAREASOLID",
4,
Some(4),
[0, 1, 2],
"geometry",
"extruded solid",
),
make_schema_rule(
"IFCRECTANGLEPROFILEDEF",
5,
Some(5),
[],
"geometry",
"rectangle profile",
),
make_schema_rule(
"IFCCIRCLEPROFILEDEF",
4,
Some(4),
[],
"geometry",
"circle profile",
),
make_schema_rule(
"IFCPRODUCTDEFINITIONSHAPE",
3,
Some(3),
[2],
"geometry",
"product shape",
),
make_schema_rule(
"IFCSHAPEREPRESENTATION",
4,
Some(4),
[0, 3],
"geometry",
"shape representation",
),
make_schema_rule(
"IFCGEOMETRICREPRESENTATIONCONTEXT",
6,
None,
[4],
"geometry",
"representation context",
),
make_schema_rule(
"IFCUNITASSIGNMENT",
1,
Some(1),
[0],
"units",
"unit assignment",
),
make_schema_rule("IFCSIUNIT", 4, Some(4), [], "units", "SI unit"),
make_schema_rule(
"IFCPROPERTYSET",
5,
Some(5),
[1, 4],
"property",
"property set",
),
make_schema_rule(
"IFCPROPERTYSINGLEVALUE",
4,
Some(4),
[],
"property",
"single property",
),
make_schema_rule(
"IFCRELCONTAINEDINSPATIALSTRUCTURE",
6,
Some(6),
[1, 4, 5],
"relationship",
"spatial containment",
),
make_schema_rule(
"IFCRELAGGREGATES",
6,
Some(6),
[1, 4, 5],
"relationship",
"spatial aggregation",
),
make_schema_rule(
"IFCRELDEFINESBYPROPERTIES",
6,
Some(6),
[1, 4, 5],
"relationship",
"property association",
),
make_schema_rule(
"IFCRELASSOCIATESMATERIAL",
6,
Some(6),
[1, 4, 5],
"relationship",
"material association",
),
make_schema_rule(
"IFCFACETEDBREP",
1,
Some(1),
[0],
"topology",
"faceted boundary representation",
),
make_schema_rule(
"IFCCLOSEDSHELL",
1,
Some(1),
[0],
"topology",
"closed shell",
),
make_schema_rule("IFCFACE", 1, Some(1), [0], "topology", "face"),
make_schema_rule(
"IFCFACEOUTERBOUND",
2,
Some(2),
[0, 1],
"topology",
"outer face boundary",
),
make_schema_rule("IFCPOLYLOOP", 1, Some(1), [0], "topology", "poly loop"),
]
}
///|
fn rules_for_version(version : SchemaVersion) -> Array[SchemaRule] {
let rules = common_schema_rules()
match version {
IFC2X3 => rules
IFC4 => rules
IFC4X3 => rules
UnknownSchema(_) => rules
}
}
///|
pub fn schema_profile(version : SchemaVersion) -> SchemaProfile {
let name = match version {
IFC2X3 => "IFC 2x3 coordination profile"
IFC4 => "IFC 4 coordination profile"
IFC4X3 => "IFC 4.3 coordination profile"
UnknownSchema(value) => "Unknown schema profile: " + value
}
let rules = rules_for_version(version)
{ name, version, known_entity_count: rules.length(), rules }
}
///|
pub fn default_schema_profile() -> SchemaProfile {
schema_profile(IFC4)
}
///|
fn schema_rule_for(
profile : SchemaProfile,
entity_name : String,
) -> SchemaRule? {
let wanted = entity_name.to_upper()
for rule in profile.rules {
if rule.entity_name == wanted {
return Some(rule)
}
}
None
}
///|
pub fn SchemaProfile::rule(
self : SchemaProfile,
entity_name : String,
) -> SchemaRule? {
schema_rule_for(self, entity_name)
}
///|
fn schema_issue(
code : String,
entity : Entity,
message : String,
severity : DiagnosticSeverity,
) -> SchemaIssue {
{ code, entity_id: entity.id, entity_name: entity.name, message, severity }
}
///|
fn argument_is_reference(entity : Entity, index : Int) -> Bool {
match entity.arguments.get(index) {
Some(Reference(_)) => true
_ => false
}
}
///|
fn check_schema_entity(
profile : SchemaProfile,
index : ModelIndex,
entity : Entity,
issues : Array[SchemaIssue],
) -> Unit {
match schema_rule_for(profile, entity.name) {
None =>
issues.push(
schema_issue(
"SCHEMA001",
entity,
"entity is not covered by the selected semantic profile; generic parsing is retained",
Note,
),
)
Some(rule) => {
let actual = entity.arguments.length()
if actual < rule.minimum_arguments {
issues.push(
schema_issue(
"SCHEMA002",
entity,
"expected at least " +
rule.minimum_arguments.to_string() +
" argument(s), found " +
actual.to_string(),
Error,
),
)
}
match rule.maximum_arguments {
Some(maximum) if actual > maximum =>
issues.push(
schema_issue(
"SCHEMA003",
entity,
"expected at most " +
maximum.to_string() +
" argument(s), found " +
actual.to_string(),
Error,
),
)
_ => ()
}
for required in rule.required_reference_arguments {
match entity.arguments.get(required) {
Some(value) => {
let targets = references(value)
if targets.length() == 0 {
issues.push(
schema_issue(
"SCHEMA004",
entity,
"required reference argument " +
required.to_string() +
" is unset",
Error,
),
)
}
for target in targets {
if !index.has(target) {
issues.push(
schema_issue(
"SCHEMA005",
entity,
"reference argument " +
required.to_string() +
" points to missing entity #" +
target.to_string(),
Error,
),
)
}
}
}
None =>
issues.push(
schema_issue(
"SCHEMA006",
entity,
"required reference argument " +
required.to_string() +
" is missing",
Error,
),
)
}
}
if rule.family == "relationship" && !argument_is_reference(entity, 1) {
issues.push(
schema_issue(
"SCHEMA007",
entity,
"OwnerHistory relationship slot is not a reference",
Warning,
),
)
}
}
}
}
///|
fn count_schema_severity(issues : Array[SchemaIssue]) -> (Int, Int) {
let mut errors = 0
let mut warnings = 0
for issue in issues {
match issue.severity {
Error => errors += 1
Warning => warnings += 1
Note => ()
}
}
(errors, warnings)
}
///|
pub fn validate_with_schema(
model : Model,
profile : SchemaProfile,
) -> SchemaReport {
let index = model.index()
let issues : Array[SchemaIssue] = []
for entity in model.entities {
check_schema_entity(profile, index, entity, issues)
}
let (error_count, warning_count) = count_schema_severity(issues)
{
profile,
checked_entities: model.entities.length(),
issues,
error_count,
warning_count,
}
}
///|
pub fn validate_schema(model : Model) -> SchemaReport {
validate_with_schema(model, schema_profile(schema_version(model)))
}
///|
pub fn schema_issues_by_code(
report : SchemaReport,
code : String,
) -> Array[SchemaIssue] {
report.issues.filter(issue => issue.code == code)
}
///|
pub fn SchemaReport::is_valid(self : SchemaReport) -> Bool {
self.error_count == 0
}
///|
pub fn SchemaReport::unknown_entity_count(self : SchemaReport) -> Int {
schema_issues_by_code(self, "SCHEMA001").length()
}
///|
fn schema_severity_text(severity : DiagnosticSeverity) -> String {
match severity {
Error => "error"
Warning => "warning"
Note => "note"
}
}
///|
pub fn SchemaReport::format(self : SchemaReport) -> String {
let header = self.profile.name +
": checked=" +
self.checked_entities.to_string() +
", errors=" +
self.error_count.to_string() +
", warnings=" +
self.warning_count.to_string()
if self.issues.length() == 0 {
return header
}
header +
"\n" +
self.issues
.map(issue => {
schema_severity_text(issue.severity) +
"[" +
issue.code +
"] #" +
issue.entity_id.to_string() +
" " +
issue.entity_name +
": " +
issue.message
})
.join("\n")
}
///|
pub fn SchemaReport::headline(self : SchemaReport) -> String {
if self.is_valid() {
self.profile.name +
" valid with " +
self.unknown_entity_count().to_string() +
" extension note(s)"
} else {
self.profile.name +
" invalid: " +
self.error_count.to_string() +
" error(s)"
}
}
///|
pub fn schema_profile_summary(profile : SchemaProfile) -> String {
profile.name +
": " +
profile.known_entity_count.to_string() +
" entity rules (" +
profile.rules.map(rule => rule.entity_name).join(", ") +
")"
}
///|
pub fn schema_family_histogram(profile : SchemaProfile) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for rule in profile.rules {
result.update_or_default(rule.family, 1, count => count + 1)
}
result
}
///|
pub fn schema_required_reference_count(profile : SchemaProfile) -> Int {
profile.rules.fold(init=0, (count, rule) => {
count + rule.required_reference_arguments.length()
})
}
///|
pub fn schema_rule_names(profile : SchemaProfile) -> Array[String] {
profile.rules.map(rule => rule.entity_name)
}
///|
pub fn schema_entity_is_known(
profile : SchemaProfile,
entity_name : String,
) -> Bool {
schema_rule_for(profile, entity_name) is Some(_)
}
///|
pub fn schema_validate_entity(
profile : SchemaProfile,
model : Model,
entity_id : Int,
) -> Array[SchemaIssue] {
let index = model.index()
match index.entity(entity_id) {
Some(entity) => {
let issues : Array[SchemaIssue] = []
check_schema_entity(profile, index, entity, issues)
issues
}
None => []
}
}
///|
pub fn schema_compare_profiles(
left : SchemaProfile,
right : SchemaProfile,
) -> Array[String] {
let result : Array[String] = []
for rule in left.rules {
if !schema_entity_is_known(right, rule.entity_name) {
result.push("removed: " + rule.entity_name)
}
}
for rule in right.rules {
if !schema_entity_is_known(left, rule.entity_name) {
result.push("added: " + rule.entity_name)
}
}
result
}