use std::sync::Arc;
use crate::schema::types::WildcardConstraint;
use crate::schema::xsd::content_automaton::{
AutomatonState, ContentAutomaton, PosMatcher, StepResult,
};
use super::symbols::{SymbolId, SymbolTable};
pub(crate) enum BoundMatcher {
Element {
syms: Box<[SymbolId]>,
},
Wildcard(Arc<WildcardConstraint>),
}
pub(crate) struct BoundAutomaton {
auto: Arc<ContentAutomaton>,
bound: Vec<BoundMatcher>,
}
impl BoundAutomaton {
pub fn bind(auto: Arc<ContentAutomaton>, symbols: &mut SymbolTable) -> Self {
let bound = auto
.matchers()
.iter()
.map(|m| match m {
PosMatcher::Element { names, .. } => {
let mut syms: Vec<SymbolId> = names.iter().map(|n| symbols.intern(n)).collect();
syms.sort_unstable();
syms.dedup();
BoundMatcher::Element {
syms: syms.into_boxed_slice(),
}
}
PosMatcher::Wildcard(wc) => BoundMatcher::Wildcard(Arc::clone(wc)),
})
.collect();
Self { auto, bound }
}
pub fn step_syms(
&self,
st: &mut AutomatonState,
qname_sym: SymbolId,
local_sym: SymbolId,
ns: Option<&str>,
) -> StepResult {
self.auto.step_matched(st, |pos| match &self.bound[pos] {
BoundMatcher::Element { syms } => {
syms.binary_search(&qname_sym).is_ok()
|| (local_sym != qname_sym && syms.binary_search(&local_sym).is_ok())
}
BoundMatcher::Wildcard(wc) => wc.matches(ns),
})
}
#[cfg(test)]
pub fn inner(&self) -> &ContentAutomaton {
&self.auto
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::types::{ElementDef, Particle};
use crate::schema::xsd::content_automaton::build_automaton;
fn elem(name: &str, min: u32, max: Option<u32>) -> Particle {
let mut def = ElementDef::new(name);
def.min_occurs = min;
def.max_occurs = max;
Particle::Element(def)
}
fn no_subst(_: &str) -> Vec<String> {
Vec::new()
}
fn run_both_arc(auto: &Arc<ContentAutomaton>, children: &[&str]) -> Result<(), String> {
let mut symbols = SymbolTable::default();
let bound = BoundAutomaton::bind(Arc::clone(auto), &mut symbols);
let mut st_str = AutomatonState::default();
let mut st_sym = AutomatonState::default();
for child in children {
let qsym = symbols.intern(child);
let lsym = symbols.local(qsym);
let local: &str = child.rsplit(':').next().unwrap_or(child);
let r_str = auto.step(&mut st_str, child, local, None);
let r_sym = bound.step_syms(&mut st_sym, qsym, lsym, None);
assert_eq!(
r_str, r_sym,
"step vs step_syms diverged on child '{child}'"
);
assert_eq!(
st_str, st_sym,
"configuration sets diverged after child '{child}'"
);
match r_sym {
StepResult::Matched => {}
StepResult::TooMany { max } => {
return Err(format!("'{child}' too many (max {max})"));
}
StepResult::NotExpected { expected } => {
return Err(format!("'{child}' not expected ({expected:?})"));
}
}
}
bound
.inner()
.finish(&st_sym)
.map_err(|e| format!("incomplete: {e:?}"))
}
fn build(p: &Particle) -> Arc<ContentAutomaton> {
Arc::new(build_automaton(p, &no_subst).unwrap())
}
#[test]
fn sequence_order_and_occurrence() {
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![
elem("a", 1, Some(1)),
elem("b", 0, Some(2)),
elem("c", 1, None),
],
};
let a = build(&p);
assert!(a.upa_violation.is_none());
assert!(run_both_arc(&a, &["a", "c"]).is_ok());
assert!(run_both_arc(&a, &["a", "b", "b", "c", "c"]).is_ok());
assert!(run_both_arc(&a, &["b", "c"]).is_err(), "missing required a");
assert!(
run_both_arc(&a, &["a", "b", "b", "b", "c"]).is_err(),
"b too many"
);
assert!(
run_both_arc(&a, &["a", "c", "b"]).is_err(),
"b out of order"
);
assert!(run_both_arc(&a, &["a"]).is_err(), "missing c");
}
#[test]
fn choice_totals_across_alternatives() {
let p = Particle::Choice {
min: 2,
max: Some(2),
items: vec![elem("a", 1, Some(1)), elem("b", 1, Some(1))],
};
let a = build(&p);
assert!(run_both_arc(&a, &["a", "b"]).is_ok());
assert!(run_both_arc(&a, &["b", "b"]).is_ok());
assert!(run_both_arc(&a, &["a"]).is_err(), "only one of two");
assert!(run_both_arc(&a, &["a", "b", "a"]).is_err(), "three of two");
}
#[test]
fn nested_group_occurrence_ambiguity() {
let p = Particle::Sequence {
min: 1,
max: Some(2),
items: vec![elem("item", 2, Some(4))],
};
let a = build(&p);
assert!(run_both_arc(&a, &["item", "item"]).is_ok());
assert!(run_both_arc(&a, &["item", "item", "item", "item"]).is_ok());
assert!(run_both_arc(&a, &["item"]).is_err(), "below min");
}
#[test]
fn prefixed_child_matches_local_label() {
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![elem("LinearRing", 1, Some(1))],
};
let a = build(&p);
assert!(run_both_arc(&a, &["gml:LinearRing"]).is_ok());
assert!(run_both_arc(&a, &["gml:Other"]).is_err());
}
#[test]
fn substitution_group_member_matches_head_label() {
let subst = |head: &str| -> Vec<String> {
if head == "_Geometry" {
vec!["gml:Point".to_string(), "gml:LineString".to_string()]
} else {
Vec::new()
}
};
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![elem("_Geometry", 1, Some(2))],
};
let a = Arc::new(build_automaton(&p, &subst).unwrap());
assert!(run_both_arc(&a, &["gml:Point"]).is_ok());
assert!(run_both_arc(&a, &["Point"]).is_ok(), "member local name");
assert!(run_both_arc(&a, &["gml:Point", "gml:LineString"]).is_ok());
assert!(run_both_arc(&a, &["gml:Polygon"]).is_err(), "non-member");
}
}