#![allow(clippy::wildcard_imports)]
use super::*;
use crate::subd::SubdSurface;
macro_rules! define_model_entity_json {
($( $field:ident: $element:ty, $doc:literal, [$($attribute:meta),*] => $key:expr; )*) => {
fn model_entity_json(ir: &CadIr, id: &str) -> Option<serde_json::Value> {
$(
let key: fn(&$element) -> String = $key;
if let Some(entity) = ir.model.$field.iter().find(|entity| key(entity) == id) {
return serde_json::to_value(entity).ok();
}
)*
None
}
};
}
crate::document::arena_registry!(define_model_entity_json);
fn entity_json(ir: &CadIr, id: &str) -> Option<serde_json::Value> {
if let Some(value) = model_entity_json(ir, id) {
return Some(value);
}
ir.native
.0
.values()
.flat_map(|namespace| namespace.arenas.values())
.flatten()
.find(|record| record.id == id)
.and_then(|record| serde_json::to_value(record).ok())
}
pub(super) fn check_annotations(
ir: &CadIr,
all_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
) {
for (id, provenance) in &ir.annotations.provenance {
if !all_ids.contains(id) {
annotation_finding(
findings,
Severity::Error,
id,
"provenance key does not resolve to an entity",
);
}
if provenance.stream as usize >= ir.annotations.streams.len() {
annotation_finding(
findings,
Severity::Error,
id,
"provenance stream index is out of range",
);
}
}
for (id, note) in &ir.annotations.exactness {
if !all_ids.contains(id) {
annotation_finding(
findings,
Severity::Error,
id,
"exactness key does not resolve to an entity",
);
continue;
}
if note.fields.is_empty() {
continue;
}
let Some(entity) = entity_json(ir, id) else {
annotation_finding(
findings,
Severity::Warning,
id,
"entity could not be serialized to validate its exactness field paths",
);
continue;
};
for path in note.fields.keys() {
if path.is_empty() || !field_path_resolves(&entity, path) {
annotation_finding(
findings,
Severity::Warning,
id,
&format!("exactness field path `{path}` does not resolve"),
);
}
}
}
}
fn annotation_finding(findings: &mut Vec<Finding>, severity: Severity, id: &str, message: &str) {
findings.push(Finding {
check: Check::Annotations,
severity,
message: message.into(),
entity: Some(id.into()),
});
}
fn field_path_resolves(mut value: &serde_json::Value, path: &str) -> bool {
for component in path.split('.') {
match value {
serde_json::Value::Object(object) => {
let Some(next) = object.get(component) else {
return false;
};
value = next;
}
serde_json::Value::Array(array) => {
let Ok(index) = component.parse::<usize>() else {
return false;
};
let Some(next) = array.get(index) else {
return false;
};
value = next;
}
_ => return false,
}
}
true
}
pub(super) fn check_native_links(
ir: &CadIr,
all_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
) {
let native_ids = collect_native_ids(ir)
.into_iter()
.map(|(_, id)| id)
.collect::<HashSet<_>>();
for feature in &ir.model.features {
if let Some(target) = &feature.native_ref {
if !native_ids.contains(target.as_str()) {
findings.push(Finding {
check: Check::NativeLinks,
severity: Severity::Error,
message: format!("native_ref `{target}` does not resolve"),
entity: Some(feature.id.0.clone()),
});
}
}
}
let native_unknowns = ir.all_native_unknowns().unwrap_or_default();
for record in &native_unknowns {
for target in &record.links {
if !all_ids.contains(target) {
findings.push(Finding {
check: Check::NativeLinks,
severity: Severity::Error,
message: format!("unknown-record link `{target}` does not resolve"),
entity: Some(record.id.0.clone()),
});
}
}
}
for namespace in ir.native.0.values() {
for records in namespace.arenas.values() {
for record in records {
let Some(serde_json::Value::Array(links)) = record.fields.get("links") else {
continue;
};
for target in links.iter().filter_map(serde_json::Value::as_str) {
if !all_ids.contains(target) {
findings.push(Finding {
check: Check::NativeLinks,
severity: Severity::Error,
message: format!("native-record link `{target}` does not resolve"),
entity: Some(record.id.clone()),
});
}
}
}
}
}
}