use std::collections::{BTreeMap, BTreeSet};
use crate::generated::model::StepModel;
use crate::generated::read::{RefSlot, complex_ref_slots, in_subset, read as gen_read, ref_slots};
use crate::parser::{Attribute, Error, RawEntity, parse_bytes};
mod entity_normalize;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DropKind {
NonstandardValue,
Unimplemented,
NonstandardReference,
Cascade,
Unclassified,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DropReason {
pub kind: DropKind,
pub key: String,
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub n_in: usize,
pub n_synth: usize,
pub validated: usize,
pub dropped: Vec<(u64, DropReason)>,
pub norm: Vec<&'static str>,
}
fn collect_refs(a: &Attribute, out: &mut Vec<u64>) {
match a {
Attribute::EntityRef(n) => out.push(*n),
Attribute::List(l) => l.iter().for_each(|e| collect_refs(e, out)),
Attribute::Typed { value, .. } => collect_refs(value, out),
_ => {}
}
}
fn entity_refs(ent: &RawEntity) -> Vec<u64> {
let mut out = Vec::new();
match ent {
RawEntity::Simple { attributes, .. } => {
for a in attributes {
collect_refs(a, &mut out);
}
}
RawEntity::Complex { parts, .. } => {
for p in parts {
for a in &p.attributes {
collect_refs(a, &mut out);
}
}
}
}
out
}
fn ent_name(e: &RawEntity) -> String {
match e {
RawEntity::Simple { name, .. } => name.clone(),
RawEntity::Complex { parts, .. } => {
let mut n: Vec<&str> = parts.iter().map(|p| p.name.as_str()).collect();
n.sort_unstable();
format!("({})", n.join("+"))
}
}
}
fn nonstd_ref(ent: &RawEntity, graph: &BTreeMap<u64, RawEntity>) -> Option<String> {
match ent {
RawEntity::Simple {
name, attributes, ..
} => check_ref_slots(name, attributes, ref_slots(name), graph),
RawEntity::Complex { parts, .. } => parts.iter().find_map(|p| {
check_ref_slots(&p.name, &p.attributes, complex_ref_slots(&p.name), graph)
}),
}
}
fn check_ref_slots(
ename: &str,
attrs: &[Attribute],
slots: &[RefSlot],
graph: &BTreeMap<u64, RawEntity>,
) -> Option<String> {
for rs in slots {
let Some(a) = attrs.get(rs.idx) else { continue };
let mut targets: Vec<u64> = Vec::new();
collect_refs(a, &mut targets);
for r in targets {
let Some(target) = graph.get(&r) else {
continue;
};
if !in_subset(target) {
continue; }
let admitted = match target {
RawEntity::Complex { .. } => rs.complex_ok,
RawEntity::Simple { name, .. } => rs.allowed.contains(&name.as_str()),
};
if !admitted {
return Some(format!(
"{ename}: '{}' references {}",
rs.name,
ent_name(target)
));
}
}
}
None
}
fn drop_pass(
graph: &BTreeMap<u64, RawEntity>,
known_bad: &BTreeSet<u64>,
) -> (BTreeSet<u64>, Vec<(u64, DropReason)>) {
let mut dropped: Vec<(u64, DropReason)> = Vec::new();
let mut keep: BTreeSet<u64> = BTreeSet::new();
let mut root_of: BTreeMap<u64, String> = BTreeMap::new();
for (&id, e) in graph {
if known_bad.contains(&id) {
root_of.insert(id, "unclassified".to_string());
} else if in_subset(e) {
keep.insert(id);
} else {
let n = ent_name(e);
root_of.insert(id, n.clone());
dropped.push((
id,
DropReason {
kind: DropKind::Unimplemented,
key: n,
},
));
}
}
loop {
let mut removed = false;
let snapshot: Vec<u64> = keep.iter().copied().collect();
for id in snapshot {
let ent = &graph[&id];
let mut cascaded = false;
for r in entity_refs(ent) {
if !keep.contains(&r) {
keep.remove(&id);
let root = root_of
.get(&r)
.cloned()
.unwrap_or_else(|| "dangling".to_string());
dropped.push((
id,
DropReason {
kind: DropKind::Cascade,
key: format!("via-{root}"),
},
));
root_of.insert(id, root);
removed = true;
cascaded = true;
break;
}
}
if cascaded {
continue;
}
if let Some(reason) = nonstd_ref(ent, graph) {
keep.remove(&id);
root_of.insert(id, reason.clone());
dropped.push((
id,
DropReason {
kind: DropKind::NonstandardReference,
key: reason,
},
));
removed = true;
}
}
if !removed {
break;
}
}
(keep, dropped)
}
#[allow(clippy::type_complexity)]
fn normalize_all(
raw: BTreeMap<u64, RawEntity>,
) -> (
BTreeMap<u64, RawEntity>,
Vec<&'static str>,
Vec<(u64, &'static str)>,
usize,
) {
let mut raw = raw;
let mut norm: Vec<&'static str> = Vec::new();
let before = raw.len();
entity_normalize::apply(&mut raw, &mut norm);
let n_synth = raw.len() - before;
let (normalized, gnorm, slot_drops) = crate::generated::generic_normalize::normalize(raw);
norm.extend(gnorm);
(normalized, norm, slot_drops, n_synth)
}
pub fn read(src: &[u8]) -> Result<(StepModel, Report), Error> {
const MAX_ITERS: usize = 3;
let g = parse_bytes(src)?;
let n_in = g.entities.len();
let header = crate::header::from_raw(&g.header, g.schema);
let raw: BTreeMap<u64, RawEntity> = g.entities;
let (normalized, norm, slot_drops, n_synth) = normalize_all(raw);
let mut known_bad: BTreeSet<u64> = BTreeSet::new();
let mut read_fail: BTreeMap<u64, String> = BTreeMap::new();
let mut model = StepModel::default();
let mut dropped: Vec<(u64, DropReason)> = Vec::new();
let mut validated = 0usize;
let mut converged = false;
for _ in 0..MAX_ITERS {
let (kept, kept_dropped) = drop_pass(&normalized, &known_bad);
let (m, _idmap, read_drops) = gen_read(&normalized, &kept);
model = m;
dropped = kept_dropped;
validated = kept.len();
if read_drops.is_empty() {
converged = true;
break;
}
for (id, r) in read_drops {
read_fail.entry(id).or_insert(r);
known_bad.insert(id);
}
}
debug_assert!(
converged,
"fallible read did not converge in {MAX_ITERS} iters"
);
for (id, r) in slot_drops {
dropped.push((
id,
DropReason {
kind: DropKind::NonstandardValue,
key: r.to_string(),
},
));
}
for (id, r) in read_fail {
dropped.push((
id,
DropReason {
kind: DropKind::Unclassified,
key: r,
},
));
}
model.header = header;
Ok((
model,
Report {
n_in,
n_synth,
validated,
dropped,
norm,
},
))
}