use std::collections::HashMap;
use std::sync::Arc;
use energyml_core::identifiers::EtpUri;
use energyml_core::registry::Registry;
use crate::graph::{Edge, EdgeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KindBasis {
Stated,
EtpSpec {
section: &'static str,
page: u16,
},
DerivedByValue,
DerivedXsd {
required: bool,
},
Unknown {
why: &'static str,
},
}
impl KindBasis {
pub fn is_official(&self) -> bool {
matches!(self, KindBasis::Stated | KindBasis::EtpSpec { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Judged {
pub kind: Option<EdgeKind>,
pub basis: KindBasis,
}
impl Judged {
pub fn primary(basis: KindBasis) -> Self {
Self {
kind: Some(EdgeKind::Primary),
basis,
}
}
pub fn secondary(basis: KindBasis) -> Self {
Self {
kind: Some(EdgeKind::Secondary),
basis,
}
}
pub fn unknown(why: &'static str) -> Self {
Self {
kind: None,
basis: KindBasis::Unknown { why },
}
}
pub fn is_consistent(&self) -> bool {
self.kind.is_none() == matches!(self.basis, KindBasis::Unknown { .. })
}
}
pub trait KindRules: Send + Sync {
fn dor(&self, source_qualified_type: &str, path: &str) -> Judged;
fn containment(&self, container_qualified_type: &str) -> Judged;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ml {
Resqml,
Witsml,
Prodml,
Eml,
Other,
}
impl Ml {
fn of(domain: &str, version: &str) -> Self {
match (domain, version) {
("resqml", "20" | "22") => Ml::Resqml,
("witsml", "20" | "21") => Ml::Witsml,
("prodml", _) => Ml::Prodml,
("eml", _) => Ml::Eml,
_ => Ml::Other,
}
}
fn of_type(qualified_type: &str) -> (Self, &str) {
match energyml_core::identity::parse_qualified_type(qualified_type) {
Ok(parts) => (Ml::of(parts.domain, parts.version), parts.object_type),
Err(_) => (Ml::Other, ""),
}
}
}
const RESQML_ALL_PRIMARY: KindBasis = KindBasis::EtpSpec {
section: "8.2.2 (rule 1.a)",
page: 167,
};
const WITSML_EXAMPLE: KindBasis = KindBasis::EtpSpec {
section: "8.1.1.1.2",
page: 157,
};
type RequiredLookup = dyn Fn(&str, &str) -> Option<bool> + Send + Sync;
pub struct StandardRules {
required: Option<Box<RequiredLookup>>,
}
impl StandardRules {
pub fn new() -> Self {
Self { required: None }
}
pub fn with_required(lookup: impl Fn(&str, &str) -> Option<bool> + Send + Sync + 'static) -> Self {
Self {
required: Some(Box::new(lookup)),
}
}
pub fn over(registry: Arc<Registry>) -> Self {
Self::with_required(move |qualified_type, path| {
let info = registry.by_qualified_type(qualified_type)?.info;
let relations = registry.relations_of(info)?;
let mut answer = None;
for slot in relations.refers {
let slot_path = slot
.path
.iter()
.map(|step| step.name)
.collect::<Vec<_>>()
.join(".");
if slot_path != path {
continue;
}
match answer {
None => answer = Some(slot.required),
Some(seen) if seen == slot.required => {}
Some(_) => return None,
}
}
answer
})
}
pub fn covered_prefixes(&self, registry: &Registry) -> Vec<String> {
let mut prefixes: Vec<String> = registry
.types()
.filter(|info| {
let compact = info.schema_version.replace('.', "");
!matches!(Ml::of(info.package, &compact), Ml::Prodml | Ml::Eml | Ml::Other)
})
.map(|info| format!("{}{}", info.package, info.schema_version.replace('.', "")))
.collect();
prefixes.sort();
prefixes.dedup();
prefixes
}
}
impl Default for StandardRules {
fn default() -> Self {
Self::new()
}
}
fn without_indices(path: &str) -> String {
let mut out = String::with_capacity(path.len());
let mut skipping = false;
for c in path.chars() {
match c {
'[' => skipping = true,
']' => skipping = false,
_ if !skipping => out.push(c),
_ => {}
}
}
out
}
impl KindRules for StandardRules {
fn dor(&self, source_qualified_type: &str, path: &str) -> Judged {
let (ml, type_name) = Ml::of_type(source_qualified_type);
match ml {
Ml::Resqml => Judged::primary(RESQML_ALL_PRIMARY),
Ml::Witsml => {
let path = without_indices(path);
if type_name == "Channel" && path == "Wellbore" {
return Judged::secondary(WITSML_EXAMPLE);
}
match self
.required
.as_ref()
.and_then(|lookup| lookup(source_qualified_type, &path))
{
Some(true) => Judged::primary(KindBasis::DerivedXsd { required: true }),
Some(false) => Judged::secondary(KindBasis::DerivedXsd { required: false }),
None => Judged::unknown(
"WITSML has no stated rule for this reference and the schema tables hold no fact for it (ETP 1.2 section 8.1.1.1.2 note 1: the implementation guide is not published)",
),
}
}
Ml::Prodml => Judged::unknown(
"PRODML has no stated rule for which relationships are primary (ETP 1.2 section 8.1.1.1.2 note 1: the implementation guide is not published)",
),
Ml::Eml => Judged::unknown(
"EML objects alone have no stated rule for which relationships are primary (ETP 1.2 section 8.1.1.1.2)",
),
Ml::Other => Judged::unknown("no rule for the ML of this object (ETP 1.2 section 8.1.1.1.2)"),
}
}
fn containment(&self, container_qualified_type: &str) -> Judged {
let (ml, type_name) = Ml::of_type(container_qualified_type);
match ml {
Ml::Resqml => Judged::primary(RESQML_ALL_PRIMARY),
Ml::Witsml => match type_name {
"Log" | "ChannelSet" => Judged::primary(WITSML_EXAMPLE),
_ => Judged::primary(KindBasis::DerivedByValue),
},
Ml::Prodml => Judged::unknown(
"PRODML has no stated rule for which relationships are primary, by value included (ETP 1.2 section 8.1.1.1.2 note 1)",
),
Ml::Eml => Judged::unknown(
"EML containers (aggregates, dictionaries) have no stated rule for which relationships are primary (ETP 1.2 section 8.1.1.1.2)",
),
Ml::Other => Judged::unknown("no rule for the ML of this container (ETP 1.2 section 8.1.1.1.2)"),
}
}
}
pub(crate) fn qualified_type_of(uri: &str) -> Option<String> {
let parsed = EtpUri::parse(uri).ok()?;
Some(format!(
"{}{}.{}",
parsed.domain?, parsed.version?, parsed.object_type?
))
}
pub fn judge_edges(rules: &dyn KindRules, edges: Vec<Edge>) -> Vec<Edge> {
let mut types: HashMap<String, Option<String>> = HashMap::new();
edges
.into_iter()
.map(|edge| {
if edge.kind.is_some() {
return edge;
}
let source = types
.entry(edge.from.clone())
.or_insert_with(|| qualified_type_of(&edge.from));
let judged = match source {
Some(qualified_type) => rules.dor(qualified_type, &edge.path),
None => Judged::unknown("the source of the edge is not an object URI"),
};
edge.with_judgement(judged)
})
.collect()
}