use std::collections::{HashMap, HashSet};
use crate::naming;
use crate::reflect::TypeInfo;
use crate::registry::normalize_qualified_type;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Step {
pub name: &'static str,
pub when_type: Option<&'static str>,
}
impl Step {
pub const fn new(name: &'static str) -> Step {
Step {
name,
when_type: None,
}
}
pub const fn when(name: &'static str, when_type: &'static str) -> Step {
Step {
name,
when_type: Some(when_type),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContainedSlot {
pub path: &'static [Step],
pub declared: &'static str,
pub alternatives: &'static [&'static str],
pub abstract_bound: bool,
pub min_occurs: u32,
pub max_occurs: Option<u32>,
pub source: &'static str,
}
impl ContainedSlot {
pub fn admits(&self, qualified_type: &str) -> bool {
(!self.abstract_bound && self.declared == qualified_type)
|| self.alternatives.contains(&qualified_type)
}
pub fn dotted_path(&self) -> String {
dotted(self.path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DorSlot {
pub path: &'static [Step],
pub required: bool,
pub min_occurs: u32,
pub max_occurs: Option<u32>,
pub source: &'static str,
}
impl DorSlot {
pub fn dotted_path(&self) -> String {
dotted(self.path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TypeRelations {
pub package: &'static str,
pub version: &'static str,
pub name: &'static str,
pub qualified_type: &'static str,
pub contains: &'static [ContainedSlot],
pub refers: &'static [DorSlot],
}
impl TypeRelations {
pub fn is_container(&self) -> bool {
!self.contains.is_empty()
}
}
pub fn dotted(path: &[Step]) -> String {
let mut out = String::new();
for (i, step) in path.iter().enumerate() {
if i > 0 {
out.push('.');
}
out.push_str(step.name);
}
out
}
pub fn dotted_typed(path: &[Step]) -> String {
let mut out = String::new();
for (i, step) in path.iter().enumerate() {
if i > 0 {
out.push('.');
}
out.push_str(step.name);
if let Some(when) = step.when_type {
out.push('{');
out.push_str(when);
out.push('}');
}
}
out
}
#[derive(Default)]
pub(crate) struct RelationsIndex {
tables: Vec<&'static TypeRelations>,
by_name: HashMap<(String, String, String), usize>,
by_qualified: HashMap<String, usize>,
contained: HashSet<String>,
}
impl RelationsIndex {
pub(crate) fn add(&mut self, tables: &'static [TypeRelations]) {
for table in tables {
let key = name_key(table.package, table.version, table.name);
if let Some(held) = self.by_name.get(&key)
&& std::ptr::eq(self.tables[*held], table)
{
continue;
}
let idx = self.tables.len();
self.tables.push(table);
self.by_name.insert(key, idx);
let qualified = normalize_qualified_type(table.qualified_type);
let displaced_release = naming::is_prerelease(table.version)
&& self
.by_qualified
.get(&qualified)
.is_some_and(|held| !naming::is_prerelease(self.tables[*held].version));
if !displaced_release {
self.by_qualified.insert(qualified, idx);
}
for slot in table.contains {
self.contained.insert(normalize_qualified_type(slot.declared));
for alternative in slot.alternatives {
self.contained.insert(normalize_qualified_type(alternative));
}
}
}
}
pub(crate) fn of_name(&self, package: &str, version: &str, name: &str) -> Option<&'static TypeRelations> {
self.by_name
.get(&name_key(package, version, name))
.map(|i| self.tables[*i])
}
pub(crate) fn of(&self, info: &TypeInfo) -> Option<&'static TypeRelations> {
self.of_name(info.package, info.version, info.name)
}
pub(crate) fn for_qualified(&self, qualified_type: &str) -> Option<&'static TypeRelations> {
self.by_qualified
.get(&normalize_qualified_type(qualified_type))
.map(|i| self.tables[*i])
}
pub(crate) fn may_be_contained(&self, qualified_type: &str) -> bool {
self.contained.contains(&normalize_qualified_type(qualified_type))
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &'static TypeRelations> + '_ {
self.tables.iter().copied()
}
}
fn name_key(package: &str, version: &str, name: &str) -> (String, String, String) {
(
package.to_ascii_lowercase(),
version.to_string(),
name.to_ascii_lowercase(),
)
}
#[cfg(test)]
mod tests {
use super::*;
static CHANNEL: &[&str] = &["witsml21.PPFGChannel"];
static CHANNEL_SET_SLOTS: &[ContainedSlot] = &[ContainedSlot {
path: &[Step::new("Channel")],
declared: "witsml21.Channel",
alternatives: CHANNEL,
abstract_bound: false,
min_occurs: 0,
max_occurs: None,
source: "witsml/v2.1/xsd_schemas/Log.xsd#ChannelSet.Channel",
}];
static TABLES: &[TypeRelations] = &[
TypeRelations {
package: "witsml",
version: "2.1",
name: "ChannelSet",
qualified_type: "witsml21.ChannelSet",
contains: CHANNEL_SET_SLOTS,
refers: &[],
},
TypeRelations {
package: "witsml",
version: "2.1",
name: "Wellbore",
qualified_type: "witsml21.Wellbore",
contains: &[],
refers: &[],
},
];
static DROP: &[TypeRelations] = &[TypeRelations {
package: "resqml",
version: "2.2_dev3",
name: "Thing",
qualified_type: "resqml22.Thing",
contains: &[],
refers: &[],
}];
static RELEASE: &[TypeRelations] = &[TypeRelations {
package: "resqml",
version: "2.2",
name: "Thing",
qualified_type: "resqml22.Thing",
contains: CHANNEL_SET_SLOTS,
refers: &[],
}];
fn index() -> RelationsIndex {
let mut index = RelationsIndex::default();
index.add(TABLES);
index
}
#[test]
fn a_table_is_found_by_its_package_version_and_name() {
let index = index();
assert!(index.of_name("witsml", "2.1", "ChannelSet").is_some());
assert!(index.of_name("WITSML", "2.1", "channelset").is_some());
assert!(index.of_name("witsml", "2.0", "ChannelSet").is_none());
assert!(index.of_name("resqml", "2.1", "ChannelSet").is_none());
}
#[test]
fn a_table_is_found_by_qualified_type_in_the_spellings_the_registry_accepts() {
let index = index();
assert!(index.for_qualified("witsml21.ChannelSet").is_some());
assert!(index.for_qualified("witsml21.obj_ChannelSet").is_some());
assert!(index.for_qualified("witsml21.Nothing").is_none());
}
#[test]
fn a_type_may_be_contained_if_a_slot_declares_or_allows_it() {
let index = index();
assert!(index.may_be_contained("witsml21.Channel"));
assert!(index.may_be_contained("witsml21.PPFGChannel"));
assert!(!index.may_be_contained("witsml21.ChannelSet"));
assert!(!index.may_be_contained("witsml21.Wellbore"));
}
#[test]
fn only_a_table_with_slots_is_a_container() {
let index = index();
assert!(index.for_qualified("witsml21.ChannelSet").unwrap().is_container());
assert!(!index.for_qualified("witsml21.Wellbore").unwrap().is_container());
}
#[test]
fn a_development_drop_never_displaces_a_release() {
let mut index = RelationsIndex::default();
index.add(RELEASE);
index.add(DROP);
let held = index.for_qualified("resqml22.Thing").unwrap();
assert_eq!(held.version, "2.2");
assert!(held.is_container());
assert!(index.of_name("resqml", "2.2_dev3", "Thing").is_some());
let mut reversed = RelationsIndex::default();
reversed.add(DROP);
reversed.add(RELEASE);
assert_eq!(reversed.for_qualified("resqml22.Thing").unwrap().version, "2.2");
}
#[test]
fn adding_the_same_tables_twice_changes_nothing() {
let mut index = index();
index.add(TABLES);
assert_eq!(index.iter().count(), TABLES.len());
}
#[test]
fn a_slot_admits_its_declared_type_and_its_alternatives_but_not_an_abstract_declaration() {
let slot = CHANNEL_SET_SLOTS[0];
assert!(slot.admits("witsml21.Channel"));
assert!(slot.admits("witsml21.PPFGChannel"));
assert!(!slot.admits("witsml21.Log"));
let abstract_slot = ContainedSlot {
declared: "eml23.AbstractObject",
alternatives: &["eml23.PropertyKind"],
abstract_bound: true,
..slot
};
assert!(abstract_slot.admits("eml23.PropertyKind"));
assert!(
!abstract_slot.admits("eml23.AbstractObject"),
"an abstract type is not a data object"
);
}
#[test]
fn paths_print_with_and_without_their_type_conditions() {
let path = [
Step::new("ReportingEntityVolumes"),
Step::when("Disposition", "prodml23.TransferDisposition"),
Step::new("Transfer"),
];
assert_eq!(dotted(&path), "ReportingEntityVolumes.Disposition.Transfer");
assert_eq!(
dotted_typed(&path),
"ReportingEntityVolumes.Disposition{prodml23.TransferDisposition}.Transfer"
);
}
}