use std::collections::{BTreeSet, HashMap, VecDeque};
use energyml_core::identifiers::EtpUri;
use crate::error::{Error, Result};
use crate::graph::{Edge, EdgeKind};
use crate::resource::{ActiveStatus, ObjectSummary};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Scope {
#[default]
Itself,
Sources,
Targets,
SourcesOrSelf,
TargetsOrSelf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Navigable {
Primary,
Secondary,
#[default]
Both,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Context {
pub uri: String,
pub depth: u32,
pub scope: Scope,
pub types: Vec<String>,
pub navigable: Navigable,
pub include_secondary_targets: bool,
pub include_secondary_sources: bool,
pub store_last_write_after: Option<i64>,
pub active_status: Option<ActiveStatus>,
pub count_objects: bool,
}
impl Context {
pub fn of(uri: impl Into<String>, scope: Scope) -> Self {
Self {
uri: uri.into(),
depth: 1,
scope,
types: Vec::new(),
navigable: Navigable::Both,
include_secondary_targets: false,
include_secondary_sources: false,
store_last_write_after: None,
active_status: None,
count_objects: false,
}
}
pub fn dataspace(uri: impl Into<String>) -> Self {
Self::of(uri, Scope::Itself)
}
pub fn with_depth(mut self, depth: u32) -> Self {
self.depth = depth;
self
}
pub fn with_types(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.types = types.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Found {
pub resources: Vec<ObjectSummary>,
pub edges: Vec<Edge>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DeletedResource {
pub uri: String,
pub deleted_time: i64,
}
impl DeletedResource {
pub fn new(uri: impl Into<String>, deleted_time: i64) -> Self {
Self {
uri: uri.into(),
deleted_time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DeletedFilter {
pub dataspace: String,
pub after: Option<i64>,
pub types: Vec<String>,
}
impl DeletedFilter {
pub fn of(dataspace: impl Into<String>) -> Self {
Self {
dataspace: dataspace.into(),
after: None,
types: Vec::new(),
}
}
}
pub fn is_dataspace_uri(uri: &str) -> Result<bool> {
let parsed = EtpUri::parse(uri).map_err(|error| Error::BadUri {
uri: uri.to_string(),
reason: error.to_string(),
})?;
Ok(parsed.uuid.is_none() && parsed.object_type.is_none())
}
fn unstated(edge: &Edge) -> Error {
let source_type = EtpUri::parse(&edge.from)
.ok()
.and_then(|uri| Some(format!("{}{}.{}", uri.domain?, uri.version?, uri.object_type?)))
.unwrap_or_else(|| edge.from.clone());
let why = match edge.basis {
Some(crate::kinds::KindBasis::Unknown { why }) => why,
_ => "no rule or server states the kind of this edge",
};
Error::UnstatedKind { source_type, why }
}
fn needs_kind(edge: &Edge) -> Result<()> {
if edge.kind.is_none() {
return Err(unstated(edge));
}
Ok(())
}
pub fn discover(
dataspace: &str,
resources: Vec<ObjectSummary>,
edges: Vec<Edge>,
context: &Context,
) -> Result<Found> {
if let Some(odd) = context
.types
.iter()
.find(|t| t.contains('*') && (!t.ends_with(".*") || t[..t.len() - 1].contains('*')))
{
return Err(Error::BadUri {
uri: odd.clone(),
reason: "a type wildcard is a schema version followed by `.*` (ETP 1.2 §23.34.15)".into(),
});
}
let typed = |summary: &ObjectSummary| {
context.types.is_empty()
|| context
.types
.iter()
.any(|t| type_matches(t, &summary.qualified_type))
};
let in_status = |summary: &ObjectSummary| match context.active_status {
None => Ok(true),
Some(wanted) => summary
.active_status
.map(|status| status == wanted)
.ok_or(Error::Unsupported {
operation: "filtering on the active status",
store: "discover",
}),
};
let written_after = |summary: &ObjectSummary| match context.store_last_write_after {
None => Ok(true),
Some(after) => summary
.store_last_write
.map(|written| written > after)
.ok_or(Error::Unsupported {
operation: "filtering on the store's last write",
store: "discover",
}),
};
if is_dataspace_uri(&context.uri)? {
if !same_dataspace(&context.uri, dataspace)? {
return Err(Error::NotFound(context.uri.clone()));
}
let mut kept = Vec::new();
for summary in resources {
if typed(&summary) && written_after(&summary)? && in_status(&summary)? {
kept.push(summary);
}
}
return Ok(counted(with_edges(kept, &edges), &edges, context));
}
let follows = |edge: &Edge| match context.navigable {
Navigable::Both => true,
Navigable::Primary => edge.kind == Some(EdgeKind::Primary),
Navigable::Secondary => edge.kind == Some(EdgeKind::Secondary),
};
let start = context.uri.as_str();
if !resources.iter().any(|summary| summary.uri == start) {
return Err(Error::NotFound(context.uri.clone()));
}
let mut out_of: HashMap<&str, Vec<&Edge>> = HashMap::new();
let mut into: HashMap<&str, Vec<&Edge>> = HashMap::new();
for edge in &edges {
out_of.entry(edge.from.as_str()).or_default().push(edge);
into.entry(edge.to.as_str()).or_default().push(edge);
}
let (towards_targets, towards_sources, with_self) = match context.scope {
Scope::Itself => (false, false, true),
Scope::Sources => (false, true, false),
Scope::Targets => (true, false, false),
Scope::SourcesOrSelf => (false, true, true),
Scope::TargetsOrSelf => (true, false, true),
};
let mut reached: BTreeSet<String> = BTreeSet::new();
let mut seen: BTreeSet<&str> = BTreeSet::from([start]);
let mut queue: VecDeque<(&str, u32)> = VecDeque::from([(start, 0)]);
let depth = if context.scope == Scope::Itself {
0
} else {
context.depth
};
while let Some((uri, level)) = queue.pop_front() {
if level == depth {
continue;
}
let mut next: Vec<&str> = Vec::new();
if towards_targets {
for edge in out_of.get(uri).into_iter().flatten() {
if context.navigable != Navigable::Both {
needs_kind(edge)?;
}
if follows(edge) {
next.push(edge.to.as_str());
}
}
}
if towards_sources {
for edge in into.get(uri).into_iter().flatten() {
if context.navigable != Navigable::Both {
needs_kind(edge)?;
}
if follows(edge) {
next.push(edge.from.as_str());
}
}
}
for other in next {
if seen.insert(other) {
reached.insert(other.to_string());
queue.push_back((other, level + 1));
}
}
}
if with_self {
reached.insert(start.to_string());
}
let candidates: Vec<String> = reached.iter().cloned().collect();
for uri in &candidates {
if context.include_secondary_targets {
for edge in out_of.get(uri.as_str()).into_iter().flatten() {
needs_kind(edge)?;
if edge.kind == Some(EdgeKind::Secondary) {
reached.insert(edge.to.clone());
}
}
}
if context.include_secondary_sources {
for edge in into.get(uri.as_str()).into_iter().flatten() {
needs_kind(edge)?;
if edge.kind == Some(EdgeKind::Secondary) {
reached.insert(edge.from.clone());
}
}
}
}
let mut kept = Vec::new();
for summary in resources {
if reached.contains(&summary.uri)
&& typed(&summary)
&& written_after(&summary)?
&& in_status(&summary)?
{
kept.push(summary);
}
}
Ok(counted(with_edges(kept, &edges), &edges, context))
}
pub fn type_matches(pattern: &str, qualified_type: &str) -> bool {
match pattern.strip_suffix(".*") {
Some(version) => qualified_type
.split_once('.')
.is_some_and(|(prefix, _)| prefix == version),
None => pattern == qualified_type,
}
}
fn with_edges(resources: Vec<ObjectSummary>, edges: &[Edge]) -> Found {
let found: BTreeSet<&str> = resources.iter().map(|summary| summary.uri.as_str()).collect();
let edges = edges
.iter()
.filter(|edge| found.contains(edge.from.as_str()) || found.contains(edge.to.as_str()))
.cloned()
.collect();
Found { resources, edges }
}
fn counted(mut found: Found, edges: &[Edge], context: &Context) -> Found {
if !context.count_objects {
return found;
}
let mut sources: HashMap<&str, BTreeSet<&str>> = HashMap::new();
let mut targets: HashMap<&str, BTreeSet<&str>> = HashMap::new();
for edge in edges {
sources
.entry(edge.to.as_str())
.or_default()
.insert(edge.from.as_str());
targets
.entry(edge.from.as_str())
.or_default()
.insert(edge.to.as_str());
}
let count = |map: &HashMap<&str, BTreeSet<&str>>, uri: &str| {
u32::try_from(map.get(uri).map_or(0, BTreeSet::len)).unwrap_or(u32::MAX)
};
for summary in &mut found.resources {
summary.source_count = Some(count(&sources, &summary.uri));
summary.target_count = Some(count(&targets, &summary.uri));
}
found
}
pub fn types_of(found: &Found) -> Vec<(String, usize)> {
let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
for summary in &found.resources {
*counts.entry(summary.qualified_type.as_str()).or_default() += 1;
}
counts.into_iter().map(|(t, n)| (t.to_string(), n)).collect()
}
fn same_dataspace(a: &str, b: &str) -> Result<bool> {
let parse = |uri: &str| {
EtpUri::parse(uri).map_err(|error| Error::BadUri {
uri: uri.to_string(),
reason: error.to_string(),
})
};
Ok(parse(a)?.dataspace == parse(b)?.dataspace)
}