use indexmap::{IndexMap, IndexSet};
use crate::schema::{Field, FieldType, Record, Ref, Schema};
pub fn satisfiable_set(s: &Schema) -> IndexSet<String> {
let mut sat: IndexSet<String> = IndexSet::new();
let mut changed = true;
while changed {
changed = false;
for (name, rec) in s.env() {
if sat.contains(name) {
continue;
}
if record_satisfiable(rec, &sat) {
sat.insert(name.clone());
changed = true;
}
}
}
sat
}
fn record_satisfiable(rec: &Record, sat: &IndexSet<String>) -> bool {
for f in rec.fields() {
if f.min < 1 {
continue;
}
if let FieldType::Ref(r) = &f.ty
&& !sat.contains(&r.name)
{
return false;
}
}
true
}
pub fn is_empty(s: &Schema) -> bool {
!satisfiable_set(s).contains(&s.root().name)
}
pub fn prune(s: &Schema) -> Schema {
let sat = satisfiable_set(s);
let root_ok = sat.contains(&s.root().name);
let reachable = reachable_from_root(s, &sat, root_ok);
let mut new_env: IndexMap<String, Record> = IndexMap::new();
for (name, rec) in s.env() {
if !reachable.contains(name) {
continue;
}
if !root_ok && *name == s.root().name {
new_env.insert(name.clone(), rec.clone());
} else {
new_env.insert(name.clone(), prune_record(rec, &sat));
}
}
Schema::new(Ref::new(s.root().name.clone()), new_env).expect(
"prune only drops unreachable records and never-emittable/unsatisfiable-optional \
fields; every surviving Ref still resolves within the surviving env",
)
}
fn reachable_from_root(s: &Schema, sat: &IndexSet<String>, root_ok: bool) -> IndexSet<String> {
let mut seen: IndexSet<String> = IndexSet::new();
let mut stack = vec![s.root().name.clone()];
while let Some(name) = stack.pop() {
if seen.contains(&name) {
continue;
}
let rec = s
.env()
.get(&name)
.expect("Schema's own invariant: every Ref target resolves within its env");
seen.insert(name.clone());
let is_unpruned_root = name == s.root().name && !root_ok;
for f in rec.fields() {
if !is_unpruned_root {
if f.max == Some(0) {
continue;
}
if f.min == 0
&& let FieldType::Ref(r) = &f.ty
&& !sat.contains(&r.name)
{
continue;
}
}
if let FieldType::Ref(r) = &f.ty {
stack.push(r.name.clone());
}
}
}
seen
}
fn prune_record(rec: &Record, sat: &IndexSet<String>) -> Record {
let kept: Vec<Field> = rec
.fields()
.iter()
.filter(|f| {
if f.max == Some(0) {
return false;
}
if f.min == 0
&& let FieldType::Ref(r) = &f.ty
&& !sat.contains(&r.name)
{
return false;
}
true
})
.cloned()
.collect();
Record::new(kept).expect(
"filtering fields out of an already-valid Record cannot introduce a duplicate label",
)
}