1use std::collections::{BTreeSet, HashMap, VecDeque};
35
36use energyml_core::identifiers::EtpUri;
37
38use crate::error::{Error, Result};
39use crate::graph::{Edge, EdgeKind};
40use crate::resource::{ActiveStatus, ObjectSummary};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub enum Scope {
46 #[default]
48 Itself,
49 Sources,
51 Targets,
53 SourcesOrSelf,
55 TargetsOrSelf,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
61pub enum Navigable {
62 Primary,
64 Secondary,
66 #[default]
68 Both,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct Context {
78 pub uri: String,
82 pub depth: u32,
84 pub scope: Scope,
86 pub types: Vec<String>,
89 pub navigable: Navigable,
91 pub include_secondary_targets: bool,
93 pub include_secondary_sources: bool,
95 pub store_last_write_after: Option<i64>,
98 pub active_status: Option<ActiveStatus>,
101 pub count_objects: bool,
105}
106
107impl Context {
108 pub fn of(uri: impl Into<String>, scope: Scope) -> Self {
111 Self {
112 uri: uri.into(),
113 depth: 1,
114 scope,
115 types: Vec::new(),
116 navigable: Navigable::Both,
117 include_secondary_targets: false,
118 include_secondary_sources: false,
119 store_last_write_after: None,
120 active_status: None,
121 count_objects: false,
122 }
123 }
124
125 pub fn dataspace(uri: impl Into<String>) -> Self {
127 Self::of(uri, Scope::Itself)
128 }
129
130 pub fn with_depth(mut self, depth: u32) -> Self {
132 self.depth = depth;
133 self
134 }
135
136 pub fn with_types(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
138 self.types = types.into_iter().map(Into::into).collect();
139 self
140 }
141}
142
143#[derive(Debug, Clone, Default, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct Found {
148 pub resources: Vec<ObjectSummary>,
150 pub edges: Vec<Edge>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159#[non_exhaustive]
160pub struct DeletedResource {
161 pub uri: String,
163 pub deleted_time: i64,
166}
167
168impl DeletedResource {
169 pub fn new(uri: impl Into<String>, deleted_time: i64) -> Self {
171 Self {
172 uri: uri.into(),
173 deleted_time,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181#[non_exhaustive]
182pub struct DeletedFilter {
183 pub dataspace: String,
185 pub after: Option<i64>,
187 pub types: Vec<String>,
189}
190
191impl DeletedFilter {
192 pub fn of(dataspace: impl Into<String>) -> Self {
194 Self {
195 dataspace: dataspace.into(),
196 after: None,
197 types: Vec::new(),
198 }
199 }
200}
201
202pub fn is_dataspace_uri(uri: &str) -> Result<bool> {
205 let parsed = EtpUri::parse(uri).map_err(|error| Error::BadUri {
206 uri: uri.to_string(),
207 reason: error.to_string(),
208 })?;
209 Ok(parsed.uuid.is_none() && parsed.object_type.is_none())
210}
211
212fn unstated(edge: &Edge) -> Error {
214 let source_type = EtpUri::parse(&edge.from)
215 .ok()
216 .and_then(|uri| Some(format!("{}{}.{}", uri.domain?, uri.version?, uri.object_type?)))
217 .unwrap_or_else(|| edge.from.clone());
218 let why = match edge.basis {
219 Some(crate::kinds::KindBasis::Unknown { why }) => why,
220 _ => "no rule or server states the kind of this edge",
221 };
222 Error::UnstatedKind { source_type, why }
223}
224
225fn needs_kind(edge: &Edge) -> Result<()> {
227 if edge.kind.is_none() {
228 return Err(unstated(edge));
229 }
230 Ok(())
231}
232
233pub fn discover(
239 dataspace: &str,
240 resources: Vec<ObjectSummary>,
241 edges: Vec<Edge>,
242 context: &Context,
243) -> Result<Found> {
244 if let Some(odd) = context
245 .types
246 .iter()
247 .find(|t| t.contains('*') && (!t.ends_with(".*") || t[..t.len() - 1].contains('*')))
248 {
249 return Err(Error::BadUri {
250 uri: odd.clone(),
251 reason: "a type wildcard is a schema version followed by `.*` (ETP 1.2 §23.34.15)".into(),
252 });
253 }
254 let typed = |summary: &ObjectSummary| {
255 context.types.is_empty()
256 || context
257 .types
258 .iter()
259 .any(|t| type_matches(t, &summary.qualified_type))
260 };
261 let in_status = |summary: &ObjectSummary| match context.active_status {
262 None => Ok(true),
263 Some(wanted) => summary
264 .active_status
265 .map(|status| status == wanted)
266 .ok_or(Error::Unsupported {
267 operation: "filtering on the active status",
268 store: "discover",
269 }),
270 };
271 let written_after = |summary: &ObjectSummary| match context.store_last_write_after {
272 None => Ok(true),
273 Some(after) => summary
274 .store_last_write
275 .map(|written| written > after)
276 .ok_or(Error::Unsupported {
277 operation: "filtering on the store's last write",
278 store: "discover",
279 }),
280 };
281
282 if is_dataspace_uri(&context.uri)? {
283 if !same_dataspace(&context.uri, dataspace)? {
284 return Err(Error::NotFound(context.uri.clone()));
285 }
286 let mut kept = Vec::new();
287 for summary in resources {
288 if typed(&summary) && written_after(&summary)? && in_status(&summary)? {
289 kept.push(summary);
290 }
291 }
292 return Ok(counted(with_edges(kept, &edges), &edges, context));
293 }
294
295 let follows = |edge: &Edge| match context.navigable {
298 Navigable::Both => true,
299 Navigable::Primary => edge.kind == Some(EdgeKind::Primary),
300 Navigable::Secondary => edge.kind == Some(EdgeKind::Secondary),
301 };
302
303 let start = context.uri.as_str();
304 if !resources.iter().any(|summary| summary.uri == start) {
305 return Err(Error::NotFound(context.uri.clone()));
306 }
307 let mut out_of: HashMap<&str, Vec<&Edge>> = HashMap::new();
308 let mut into: HashMap<&str, Vec<&Edge>> = HashMap::new();
309 for edge in &edges {
310 out_of.entry(edge.from.as_str()).or_default().push(edge);
311 into.entry(edge.to.as_str()).or_default().push(edge);
312 }
313
314 let (towards_targets, towards_sources, with_self) = match context.scope {
316 Scope::Itself => (false, false, true),
317 Scope::Sources => (false, true, false),
318 Scope::Targets => (true, false, false),
319 Scope::SourcesOrSelf => (false, true, true),
320 Scope::TargetsOrSelf => (true, false, true),
321 };
322 let mut reached: BTreeSet<String> = BTreeSet::new();
323 let mut seen: BTreeSet<&str> = BTreeSet::from([start]);
324 let mut queue: VecDeque<(&str, u32)> = VecDeque::from([(start, 0)]);
325 let depth = if context.scope == Scope::Itself {
326 0
327 } else {
328 context.depth
329 };
330 while let Some((uri, level)) = queue.pop_front() {
331 if level == depth {
332 continue;
333 }
334 let mut next: Vec<&str> = Vec::new();
335 if towards_targets {
336 for edge in out_of.get(uri).into_iter().flatten() {
337 if context.navigable != Navigable::Both {
338 needs_kind(edge)?;
339 }
340 if follows(edge) {
341 next.push(edge.to.as_str());
342 }
343 }
344 }
345 if towards_sources {
346 for edge in into.get(uri).into_iter().flatten() {
347 if context.navigable != Navigable::Both {
348 needs_kind(edge)?;
349 }
350 if follows(edge) {
351 next.push(edge.from.as_str());
352 }
353 }
354 }
355 for other in next {
356 if seen.insert(other) {
357 reached.insert(other.to_string());
358 queue.push_back((other, level + 1));
359 }
360 }
361 }
362 if with_self {
365 reached.insert(start.to_string());
366 }
367
368 let candidates: Vec<String> = reached.iter().cloned().collect();
370 for uri in &candidates {
371 if context.include_secondary_targets {
372 for edge in out_of.get(uri.as_str()).into_iter().flatten() {
373 needs_kind(edge)?;
374 if edge.kind == Some(EdgeKind::Secondary) {
375 reached.insert(edge.to.clone());
376 }
377 }
378 }
379 if context.include_secondary_sources {
380 for edge in into.get(uri.as_str()).into_iter().flatten() {
381 needs_kind(edge)?;
382 if edge.kind == Some(EdgeKind::Secondary) {
383 reached.insert(edge.from.clone());
384 }
385 }
386 }
387 }
388
389 let mut kept = Vec::new();
391 for summary in resources {
392 if reached.contains(&summary.uri)
393 && typed(&summary)
394 && written_after(&summary)?
395 && in_status(&summary)?
396 {
397 kept.push(summary);
398 }
399 }
400 Ok(counted(with_edges(kept, &edges), &edges, context))
402}
403
404pub fn type_matches(pattern: &str, qualified_type: &str) -> bool {
408 match pattern.strip_suffix(".*") {
409 Some(version) => qualified_type
410 .split_once('.')
411 .is_some_and(|(prefix, _)| prefix == version),
412 None => pattern == qualified_type,
413 }
414}
415
416fn with_edges(resources: Vec<ObjectSummary>, edges: &[Edge]) -> Found {
420 let found: BTreeSet<&str> = resources.iter().map(|summary| summary.uri.as_str()).collect();
421 let edges = edges
422 .iter()
423 .filter(|edge| found.contains(edge.from.as_str()) || found.contains(edge.to.as_str()))
424 .cloned()
425 .collect();
426 Found { resources, edges }
427}
428
429fn counted(mut found: Found, edges: &[Edge], context: &Context) -> Found {
437 if !context.count_objects {
438 return found;
439 }
440 let mut sources: HashMap<&str, BTreeSet<&str>> = HashMap::new();
441 let mut targets: HashMap<&str, BTreeSet<&str>> = HashMap::new();
442 for edge in edges {
443 sources
444 .entry(edge.to.as_str())
445 .or_default()
446 .insert(edge.from.as_str());
447 targets
448 .entry(edge.from.as_str())
449 .or_default()
450 .insert(edge.to.as_str());
451 }
452 let count = |map: &HashMap<&str, BTreeSet<&str>>, uri: &str| {
453 u32::try_from(map.get(uri).map_or(0, BTreeSet::len)).unwrap_or(u32::MAX)
454 };
455 for summary in &mut found.resources {
456 summary.source_count = Some(count(&sources, &summary.uri));
457 summary.target_count = Some(count(&targets, &summary.uri));
458 }
459 found
460}
461
462pub fn types_of(found: &Found) -> Vec<(String, usize)> {
466 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
467 for summary in &found.resources {
468 *counts.entry(summary.qualified_type.as_str()).or_default() += 1;
469 }
470 counts.into_iter().map(|(t, n)| (t.to_string(), n)).collect()
471}
472
473fn same_dataspace(a: &str, b: &str) -> Result<bool> {
474 let parse = |uri: &str| {
475 EtpUri::parse(uri).map_err(|error| Error::BadUri {
476 uri: uri.to_string(),
477 reason: error.to_string(),
478 })
479 };
480 Ok(parse(a)?.dataspace == parse(b)?.dataspace)
481}