use crate::IdsVersion;
use openbim_core::Detected;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Signal {
OccursOnRequirementFacet,
CardinalityOnRequirementFacet,
PropertyName,
PropertyBaseName,
MeasureAttribute,
DataTypeAttribute,
Ifc4x3Unsuffixed,
Ifc4x3Add2,
ClassificationWithoutSystem,
}
impl Signal {
#[must_use]
pub fn indicates(self) -> IdsVersion {
match self {
Signal::CardinalityOnRequirementFacet
| Signal::PropertyBaseName
| Signal::DataTypeAttribute
| Signal::Ifc4x3Add2 => IdsVersion::Ids1_0,
Signal::OccursOnRequirementFacet
| Signal::PropertyName
| Signal::MeasureAttribute
| Signal::Ifc4x3Unsuffixed
| Signal::ClassificationWithoutSystem => IdsVersion::Draft0_9,
}
}
#[must_use]
pub fn is_1_0_only(self) -> bool {
self.indicates() == IdsVersion::Ids1_0
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VersionSignals {
observed: std::collections::BTreeSet<Signal>,
}
impl VersionSignals {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, signal: Signal) -> &mut Self {
self.observed.insert(signal);
self
}
#[must_use]
pub fn saw(&self, signal: Signal) -> bool {
self.observed.contains(&signal)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.observed.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = Signal> + '_ {
self.observed.iter().copied()
}
#[must_use]
pub fn signals_for_1_0(&self) -> Vec<Signal> {
self.iter().filter(|s| s.is_1_0_only()).collect()
}
#[must_use]
pub fn signals_for_draft(&self) -> Vec<Signal> {
self.iter().filter(|s| !s.is_1_0_only()).collect()
}
#[must_use]
pub fn is_internally_inconsistent(&self) -> bool {
!self.signals_for_1_0().is_empty() && !self.signals_for_draft().is_empty()
}
#[must_use]
pub fn infer(&self) -> Option<IdsVersion> {
if self.is_empty() || self.is_internally_inconsistent() {
return None;
}
Some(if self.signals_for_1_0().is_empty() {
IdsVersion::Draft0_9
} else {
IdsVersion::Ids1_0
})
}
}
#[must_use]
pub fn detect_version(
declared: Option<IdsVersion>,
signals: &VersionSignals,
) -> Option<Detected<IdsVersion>> {
let observed = signals.infer();
match (declared, observed) {
(Some(declared), Some(observed)) => Some(if versions_agree(declared, observed) {
Detected::Declared(declared)
} else {
Detected::Conflict { declared, observed }
}),
(Some(declared), None) => Some(Detected::Declared(declared)),
(None, Some(observed)) => Some(Detected::Inferred(observed)),
(None, None) => None,
}
}
fn versions_agree(declared: IdsVersion, observed: IdsVersion) -> bool {
match observed {
IdsVersion::Ids1_0 => declared == IdsVersion::Ids1_0,
_ => declared != IdsVersion::Ids1_0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn signals(list: &[Signal]) -> VersionSignals {
let mut s = VersionSignals::new();
for signal in list {
s.observe(*signal);
}
s
}
#[test]
fn every_signal_indicates_a_revision() {
let all = [
Signal::OccursOnRequirementFacet,
Signal::CardinalityOnRequirementFacet,
Signal::PropertyName,
Signal::PropertyBaseName,
Signal::MeasureAttribute,
Signal::DataTypeAttribute,
Signal::Ifc4x3Unsuffixed,
Signal::Ifc4x3Add2,
Signal::ClassificationWithoutSystem,
];
for signal in all {
assert!(matches!(
signal.indicates(),
IdsVersion::Ids1_0 | IdsVersion::Draft0_9
));
}
assert_eq!(all.iter().filter(|s| s.is_1_0_only()).count(), 4);
}
#[test]
fn occurs_on_a_requirement_facet_is_a_pre_1_0_tell() {
let s = signals(&[Signal::OccursOnRequirementFacet]);
assert_eq!(s.infer(), Some(IdsVersion::Draft0_9));
}
#[test]
fn cardinality_on_a_requirement_facet_is_1_0() {
let s = signals(&[Signal::CardinalityOnRequirementFacet]);
assert_eq!(s.infer(), Some(IdsVersion::Ids1_0));
}
#[test]
fn declared_1_0_over_a_draft_shape_is_a_conflict() {
let s = signals(&[Signal::OccursOnRequirementFacet]);
let detected = detect_version(Some(IdsVersion::Ids1_0), &s).expect("evidence");
assert_eq!(
detected,
Detected::Conflict {
declared: IdsVersion::Ids1_0,
observed: IdsVersion::Draft0_9,
}
);
assert!(detected.is_conflict());
assert_eq!(detected.resolved(), None, "a conflict must not resolve");
}
#[test]
fn declared_draft_over_a_1_0_shape_is_a_conflict() {
let s = signals(&[Signal::CardinalityOnRequirementFacet]);
let detected = detect_version(Some(IdsVersion::Draft0_9_7), &s).expect("evidence");
assert!(detected.is_conflict());
}
#[test]
fn agreement_is_declared_not_inferred() {
let s = signals(&[
Signal::CardinalityOnRequirementFacet,
Signal::PropertyBaseName,
]);
assert_eq!(
detect_version(Some(IdsVersion::Ids1_0), &s),
Some(Detected::Declared(IdsVersion::Ids1_0))
);
}
#[test]
fn shape_alone_infers() {
let s = signals(&[Signal::PropertyBaseName, Signal::DataTypeAttribute]);
assert_eq!(
detect_version(None, &s),
Some(Detected::Inferred(IdsVersion::Ids1_0))
);
}
#[test]
fn no_declaration_and_no_evidence_yields_nothing() {
assert_eq!(detect_version(None, &VersionSignals::new()), None);
}
#[test]
fn a_declaration_with_no_evidence_stands() {
assert_eq!(
detect_version(Some(IdsVersion::Ids1_0), &VersionSignals::new()),
Some(Detected::Declared(IdsVersion::Ids1_0))
);
}
#[test]
fn a_declared_draft_does_not_conflict_with_an_inferred_draft() {
let s = signals(&[Signal::PropertyName]);
for declared in [
IdsVersion::Draft0_9,
IdsVersion::Draft0_9_6,
IdsVersion::Draft0_9_7,
] {
let detected = detect_version(Some(declared), &s).expect("evidence");
assert_eq!(
detected,
Detected::Declared(declared),
"{declared} must not be reported as conflicting with a draft shape"
);
}
}
#[test]
fn mixed_shapes_are_inconsistent_and_do_not_infer() {
let s = signals(&[
Signal::OccursOnRequirementFacet,
Signal::CardinalityOnRequirementFacet,
]);
assert!(s.is_internally_inconsistent());
assert_eq!(
s.infer(),
None,
"a self-contradictory document must not infer"
);
}
#[test]
fn each_rename_is_detected() {
let cases = [
(Signal::PropertyName, Signal::PropertyBaseName),
(Signal::MeasureAttribute, Signal::DataTypeAttribute),
(Signal::Ifc4x3Unsuffixed, Signal::Ifc4x3Add2),
];
for (old, new) in cases {
assert_eq!(
signals(&[old]).infer(),
Some(IdsVersion::Draft0_9),
"{old:?} must indicate a draft"
);
assert_eq!(
signals(&[new]).infer(),
Some(IdsVersion::Ids1_0),
"{new:?} must indicate 1.0"
);
}
}
#[test]
fn missing_classification_system_indicates_a_draft() {
assert_eq!(
signals(&[Signal::ClassificationWithoutSystem]).infer(),
Some(IdsVersion::Draft0_9)
);
}
#[test]
fn observing_is_idempotent_and_queryable() {
let mut s = VersionSignals::new();
assert!(s.is_empty());
s.observe(Signal::PropertyBaseName)
.observe(Signal::PropertyBaseName);
assert!(!s.is_empty());
assert!(s.saw(Signal::PropertyBaseName));
assert!(!s.saw(Signal::PropertyName));
assert_eq!(s.iter().count(), 1);
}
}