use crate::ids::NameId;
use crate::parser::frames::{ParticleResult, ParticleTerm};
use crate::parser::location::SourceRef;
use crate::schema::model::XsdVersion;
use crate::types::complex::{not_qnames_exclude, NamespaceConstraint, ProcessContents};
use super::error::{NfaCompileError, NfaCompileResult};
use super::nfa::NfaTerm;
use super::particle::MaxOccurs;
use super::substitution::SubstitutionGroupMap;
#[derive(Debug, Clone)]
pub struct AllGroupModel {
pub particles: Vec<AllParticle>,
pub open_content: Option<OpenContentWildcard>,
pub outer_optional: bool,
}
#[derive(Debug, Clone)]
pub struct AllParticle {
pub term: NfaTerm,
pub min_occurs: u32,
pub max_occurs: MaxOccurs,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct OpenContentWildcard {
pub namespace_constraint: NamespaceConstraint,
pub process_contents: ProcessContents,
pub mode: OpenContentMode,
pub not_qnames: Vec<(Option<NameId>, NameId)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OpenContentMode {
#[default]
None,
Interleave,
Suffix,
}
#[derive(Debug, Clone)]
pub struct AllGroupState {
consumed: Vec<u32>,
}
impl AllGroupModel {
pub fn new(particles: Vec<AllParticle>) -> Self {
Self {
particles,
open_content: None,
outer_optional: false,
}
}
pub fn with_open_content(
particles: Vec<AllParticle>,
open_content: OpenContentWildcard,
) -> Self {
Self {
particles,
open_content: Some(open_content),
outer_optional: false,
}
}
pub fn is_empty(&self) -> bool {
self.particles.is_empty()
}
pub fn particle_count(&self) -> usize {
self.particles.len()
}
pub fn is_optional(&self) -> bool {
self.particles.iter().all(|p| p.is_optional())
}
pub fn create_state(&self) -> AllGroupState {
AllGroupState::new(self)
}
}
impl AllParticle {
pub fn new(
term: NfaTerm,
min_occurs: u32,
max_occurs: MaxOccurs,
source: Option<SourceRef>,
) -> Self {
Self {
term,
min_occurs,
max_occurs,
source,
}
}
pub fn is_optional(&self) -> bool {
self.min_occurs == 0
}
pub fn is_satisfied(&self, consumed: u32) -> bool {
consumed >= self.min_occurs
}
}
impl AllGroupState {
pub fn new(model: &AllGroupModel) -> Self {
Self {
consumed: vec![0; model.particles.len()],
}
}
pub fn reset(&mut self, model: &AllGroupModel) {
self.consumed.clear();
self.consumed.resize(model.particles.len(), 0);
}
pub fn can_accept(&self, model: &AllGroupModel, index: usize) -> bool {
if let (Some(&count), Some(particle)) =
(self.consumed.get(index), model.particles.get(index))
{
match particle.max_occurs {
MaxOccurs::Unbounded => true,
MaxOccurs::Bounded(max) => count < max,
}
} else {
false
}
}
pub fn accept(&mut self, model: &AllGroupModel, index: usize) -> bool {
if self.can_accept(model, index) {
self.consumed[index] += 1;
true
} else {
false
}
}
pub fn consumed(&self, index: usize) -> u32 {
self.consumed.get(index).copied().unwrap_or(0)
}
pub fn is_satisfied(&self, model: &AllGroupModel) -> bool {
for (i, particle) in model.particles.iter().enumerate() {
if !particle.is_satisfied(self.consumed(i)) {
return false;
}
}
true
}
pub fn has_any_consumed(&self) -> bool {
self.consumed.iter().any(|&c| c > 0)
}
pub fn unsatisfied_indices(&self, model: &AllGroupModel) -> Vec<usize> {
let mut result = Vec::new();
for (i, particle) in model.particles.iter().enumerate() {
if !particle.is_satisfied(self.consumed(i)) {
result.push(i);
}
}
result
}
}
pub fn validate_all_group_constraints(
particles: &[ParticleResult],
xsd_version: XsdVersion,
source: Option<SourceRef>,
) -> NfaCompileResult<()> {
match xsd_version {
XsdVersion::V1_0 => validate_all_group_xsd10(particles, source),
#[cfg(feature = "xsd11")]
XsdVersion::V1_1 => validate_all_group_xsd11(particles, source),
#[cfg(not(feature = "xsd11"))]
XsdVersion::V1_1 => validate_all_group_xsd10(particles, source),
}
}
#[cfg(feature = "xsd11")]
fn validate_all_group_xsd11(
particles: &[ParticleResult],
source: Option<SourceRef>,
) -> NfaCompileResult<()> {
for particle in particles {
if let ParticleTerm::Group(group) = &particle.term {
if particle.min_occurs != 1 || particle.max_occurs != Some(1) {
return Err(NfaCompileError::InvalidAllGroupOccurs {
reason: "cos-all-limited.1.3: group reference inside xs:all \
must have minOccurs = maxOccurs = 1"
.into(),
location: particle.source.clone().or(source.clone()),
});
}
if group.ref_name.is_none() {
return Err(NfaCompileError::InvalidAllGroupContent {
location: particle.source.clone().or(source.clone()),
});
}
}
}
Ok(())
}
fn validate_all_group_xsd10(
particles: &[ParticleResult],
source: Option<SourceRef>,
) -> NfaCompileResult<()> {
for particle in particles {
if !matches!(particle.term, ParticleTerm::Element(_)) {
return Err(NfaCompileError::InvalidAllGroupContent {
location: particle.source.clone().or(source.clone()),
});
}
if particle.min_occurs > 1 {
return Err(NfaCompileError::InvalidAllGroupOccurs {
reason: format!(
"minOccurs must be 0 or 1 in XSD 1.0 all-group, found {}",
particle.min_occurs
),
location: particle.source.clone().or(source.clone()),
});
}
match particle.max_occurs {
Some(0) | Some(1) => {} Some(n) => {
return Err(NfaCompileError::InvalidAllGroupOccurs {
reason: format!("maxOccurs must be 0 or 1 in XSD 1.0 all-group, found {}", n),
location: particle.source.clone().or(source.clone()),
});
}
None => {
return Err(NfaCompileError::InvalidAllGroupOccurs {
reason: "maxOccurs='unbounded' not allowed in XSD 1.0 all-group".to_string(),
location: particle.source.clone().or(source.clone()),
});
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TermMatchResult {
Match,
NoMatch,
}
pub fn term_matches(
term: &NfaTerm,
element_name: NameId,
element_namespace: Option<NameId>,
target_namespace: Option<NameId>,
xsd_version: XsdVersion,
) -> TermMatchResult {
term_matches_with_substitution(
term,
element_name,
element_namespace,
target_namespace,
None,
xsd_version,
)
}
pub fn term_matches_with_substitution(
term: &NfaTerm,
element_name: NameId,
element_namespace: Option<NameId>,
target_namespace: Option<NameId>,
substitution_groups: Option<&SubstitutionGroupMap>,
xsd_version: XsdVersion,
) -> TermMatchResult {
match term {
NfaTerm::Element {
name,
namespace,
element_key,
..
} => {
if let (Some(map), Some(key)) = (substitution_groups, element_key) {
if let Some(names) = map.get(key) {
return if names.contains(&(element_name, element_namespace)) {
TermMatchResult::Match
} else {
TermMatchResult::NoMatch
};
}
}
if *name == element_name && *namespace == element_namespace {
TermMatchResult::Match
} else {
TermMatchResult::NoMatch
}
}
NfaTerm::Wildcard {
namespace_constraint,
not_qnames,
..
} => {
if !namespace_constraint.matches(element_namespace, target_namespace, xsd_version) {
return TermMatchResult::NoMatch;
}
if not_qnames_exclude(not_qnames, element_namespace, element_name) {
return TermMatchResult::NoMatch;
}
TermMatchResult::Match
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::build_substitution_group_map;
use crate::ids::NameId;
use crate::schema::model::{DerivationSet, SchemaSet};
fn element_data(
name: NameId,
target_namespace: Option<NameId>,
) -> crate::arenas::ElementDeclData {
crate::arenas::ElementDeclData {
name: Some(name),
target_namespace,
ref_name: None,
type_ref: None,
inline_type: None,
substitution_group: Vec::new(),
default_value: None,
fixed_value: None,
nillable: false,
is_abstract: false,
min_occurs: 1,
max_occurs: Some(1),
block: DerivationSet::empty(),
final_derivation: DerivationSet::empty(),
form: None,
id: None,
alternatives: Vec::new(),
identity_constraints: Vec::new(),
pending_ic_refs: vec![],
annotation: None,
source: None,
resolved_type: None,
resolved_ref: None,
resolved_substitution_groups: Vec::new(),
deferred_type_error: None,
}
}
fn make_element_term(name: u32) -> NfaTerm {
NfaTerm::element(NameId(name), None, None)
}
fn make_particle(name: u32, min: u32, max: MaxOccurs) -> AllParticle {
AllParticle::new(make_element_term(name), min, max, None)
}
#[test]
fn test_all_group_model_new() {
let particles = vec![
make_particle(1, 1, MaxOccurs::Bounded(1)),
make_particle(2, 0, MaxOccurs::Bounded(1)),
];
let model = AllGroupModel::new(particles);
assert_eq!(model.particle_count(), 2);
assert!(!model.is_empty());
assert!(!model.is_optional()); }
#[test]
fn test_all_group_model_optional() {
let particles = vec![
make_particle(1, 0, MaxOccurs::Bounded(1)),
make_particle(2, 0, MaxOccurs::Bounded(1)),
];
let model = AllGroupModel::new(particles);
assert!(model.is_optional()); }
#[test]
fn test_all_particle_is_optional() {
let required = make_particle(1, 1, MaxOccurs::Bounded(1));
let optional = make_particle(2, 0, MaxOccurs::Bounded(1));
assert!(!required.is_optional());
assert!(optional.is_optional());
}
#[test]
fn test_all_particle_is_satisfied() {
let particle = make_particle(1, 2, MaxOccurs::Bounded(5));
assert!(!particle.is_satisfied(0));
assert!(!particle.is_satisfied(1));
assert!(particle.is_satisfied(2));
assert!(particle.is_satisfied(3));
}
#[test]
fn test_all_group_state_new() {
let particles = vec![
make_particle(1, 1, MaxOccurs::Bounded(2)),
make_particle(2, 0, MaxOccurs::Bounded(1)),
];
let model = AllGroupModel::new(particles);
let state = model.create_state();
assert!(state.can_accept(&model, 0));
assert!(state.can_accept(&model, 1));
}
#[test]
fn test_all_group_state_accept() {
let particles = vec![make_particle(1, 1, MaxOccurs::Bounded(2))];
let model = AllGroupModel::new(particles);
let mut state = model.create_state();
assert!(state.can_accept(&model, 0));
assert!(state.accept(&model, 0));
assert!(state.can_accept(&model, 0)); assert!(state.accept(&model, 0));
assert!(!state.can_accept(&model, 0)); assert!(!state.accept(&model, 0)); }
#[test]
fn test_all_group_state_accept_unbounded() {
let particles = vec![make_particle(1, 1, MaxOccurs::Unbounded)];
let model = AllGroupModel::new(particles);
let mut state = model.create_state();
for _ in 0..1000 {
assert!(state.can_accept(&model, 0));
assert!(state.accept(&model, 0));
}
assert!(state.can_accept(&model, 0)); }
#[test]
fn test_all_group_state_is_satisfied() {
let particles = vec![
make_particle(1, 1, MaxOccurs::Bounded(2)), make_particle(2, 0, MaxOccurs::Bounded(1)), ];
let model = AllGroupModel::new(particles);
let mut state = model.create_state();
assert!(!state.is_satisfied(&model));
state.accept(&model, 0); assert!(state.is_satisfied(&model)); }
#[test]
fn test_all_group_state_unsatisfied_indices() {
let particles = vec![
make_particle(1, 1, MaxOccurs::Bounded(1)),
make_particle(2, 1, MaxOccurs::Bounded(1)),
make_particle(3, 0, MaxOccurs::Bounded(1)),
];
let model = AllGroupModel::new(particles);
let mut state = model.create_state();
let unsatisfied = state.unsatisfied_indices(&model);
assert_eq!(unsatisfied, vec![0, 1]);
state.accept(&model, 0);
let unsatisfied = state.unsatisfied_indices(&model);
assert_eq!(unsatisfied, vec![1]); }
#[test]
fn test_term_matches_element() {
let term = NfaTerm::element(NameId(1), Some(NameId(100)), None);
assert_eq!(
term_matches(&term, NameId(1), Some(NameId(100)), None, XsdVersion::V1_0),
TermMatchResult::Match
);
assert_eq!(
term_matches(&term, NameId(2), Some(NameId(100)), None, XsdVersion::V1_0),
TermMatchResult::NoMatch
);
assert_eq!(
term_matches(&term, NameId(1), Some(NameId(200)), None, XsdVersion::V1_0),
TermMatchResult::NoMatch
);
}
#[test]
fn test_term_matches_wildcard_any() {
let term = NfaTerm::wildcard(NamespaceConstraint::Any, ProcessContents::Lax);
assert_eq!(
term_matches(&term, NameId(1), Some(NameId(100)), None, XsdVersion::V1_0),
TermMatchResult::Match
);
assert_eq!(
term_matches(&term, NameId(999), None, None, XsdVersion::V1_0),
TermMatchResult::Match
);
}
#[test]
fn test_term_matches_wildcard_other() {
let term = NfaTerm::wildcard(NamespaceConstraint::Other, ProcessContents::Lax);
let target_ns = Some(NameId(100));
assert_eq!(
term_matches(
&term,
NameId(1),
Some(NameId(200)),
target_ns,
XsdVersion::V1_0
),
TermMatchResult::Match
);
assert_eq!(
term_matches(&term, NameId(1), target_ns, target_ns, XsdVersion::V1_0),
TermMatchResult::NoMatch
);
}
#[test]
fn test_term_matches_substitution_group_member() {
let mut schema_set = SchemaSet::new();
let head_name = schema_set.name_table.add("head");
let member_name = schema_set.name_table.add("member");
let head_key = schema_set
.arenas
.alloc_element(element_data(head_name, None));
let member_key = schema_set
.arenas
.alloc_element(element_data(member_name, None));
schema_set
.arenas
.elements
.get_mut(member_key)
.unwrap()
.resolved_substitution_groups
.push(head_key);
let map = build_substitution_group_map(&schema_set);
let term = NfaTerm::element(head_name, None, Some(head_key));
assert_eq!(
term_matches_with_substitution(
&term,
member_name,
None,
None,
Some(&map),
XsdVersion::V1_0
),
TermMatchResult::Match
);
}
#[test]
fn test_term_matches_substitution_group_abstract_head() {
let mut schema_set = SchemaSet::new();
let head_name = schema_set.name_table.add("head");
let member_name = schema_set.name_table.add("member");
let mut head = element_data(head_name, None);
head.is_abstract = true;
let head_key = schema_set.arenas.alloc_element(head);
let member_key = schema_set
.arenas
.alloc_element(element_data(member_name, None));
schema_set
.arenas
.elements
.get_mut(member_key)
.unwrap()
.resolved_substitution_groups
.push(head_key);
let map = build_substitution_group_map(&schema_set);
let term = NfaTerm::element(head_name, None, Some(head_key));
assert_eq!(
term_matches_with_substitution(
&term,
head_name,
None,
None,
Some(&map),
XsdVersion::V1_0
),
TermMatchResult::NoMatch
);
assert_eq!(
term_matches_with_substitution(
&term,
member_name,
None,
None,
Some(&map),
XsdVersion::V1_0
),
TermMatchResult::Match
);
}
}