use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use crate::object_cache::ObjectId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelationshipKind {
Reference,
Composition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
Mono,
Bi,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CascadeMode {
None,
Update,
Delete,
All,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relationship {
pub name: String,
pub source: ObjectId,
pub target: ObjectId,
pub kind: RelationshipKind,
pub direction: Direction,
pub cascade: CascadeMode,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct RelationshipResolver {
by_source: BTreeMap<ObjectId, Vec<Relationship>>,
}
impl RelationshipResolver {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, rel: Relationship) {
let source = rel.source.clone();
self.by_source.entry(source).or_default().push(rel.clone());
if rel.direction == Direction::Bi {
let mut reverse = rel;
core::mem::swap(&mut reverse.source, &mut reverse.target);
self.by_source
.entry(reverse.source.clone())
.or_default()
.push(reverse);
}
}
#[must_use]
pub fn outgoing(&self, source: &ObjectId) -> Vec<&Relationship> {
self.by_source
.get(source)
.map_or_else(Vec::new, |v| v.iter().collect())
}
#[must_use]
pub fn len(&self) -> usize {
self.by_source.values().map(Vec::len).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_source.is_empty()
}
#[must_use]
pub fn cascade_targets_for_delete(&self, source: &ObjectId) -> Vec<ObjectId> {
self.by_source.get(source).map_or_else(Vec::new, |rels| {
rels.iter()
.filter(|r| matches!(r.cascade, CascadeMode::Delete | CascadeMode::All))
.map(|r| r.target.clone())
.collect()
})
}
#[must_use]
pub fn cascade_targets_for_update(&self, source: &ObjectId) -> Vec<ObjectId> {
self.by_source.get(source).map_or_else(Vec::new, |rels| {
rels.iter()
.filter(|r| matches!(r.cascade, CascadeMode::Update | CascadeMode::All))
.map(|r| r.target.clone())
.collect()
})
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
fn oid(t: &str, k: &[u8]) -> ObjectId {
ObjectId::new(t.into(), k.to_vec())
}
fn rel(name: &str, src: ObjectId, tgt: ObjectId) -> Relationship {
Relationship {
name: name.into(),
source: src,
target: tgt,
kind: RelationshipKind::Reference,
direction: Direction::Mono,
cascade: CascadeMode::None,
}
}
#[test]
fn mono_adds_one_entry() {
let mut r = RelationshipResolver::new();
r.add(rel("a", oid("T", b"1"), oid("T", b"2")));
assert_eq!(r.len(), 1);
assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
assert_eq!(r.outgoing(&oid("T", b"2")).len(), 0);
}
#[test]
fn bi_adds_inverse() {
let mut r = RelationshipResolver::new();
let mut x = rel("a", oid("T", b"1"), oid("T", b"2"));
x.direction = Direction::Bi;
r.add(x);
assert_eq!(r.len(), 2);
assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
assert_eq!(r.outgoing(&oid("T", b"2")).len(), 1);
}
#[test]
fn cascade_delete_targets_only_marked_relations() {
let mut r = RelationshipResolver::new();
let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
a.cascade = CascadeMode::Delete;
let b = rel("b", oid("T", b"1"), oid("T", b"3"));
r.add(a);
r.add(b);
let targets = r.cascade_targets_for_delete(&oid("T", b"1"));
assert_eq!(targets, alloc::vec![oid("T", b"2")]);
}
#[test]
fn cascade_update_targets_only_marked_relations() {
let mut r = RelationshipResolver::new();
let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
a.cascade = CascadeMode::Update;
r.add(a);
let mut b = rel("b", oid("T", b"1"), oid("T", b"3"));
b.cascade = CascadeMode::All;
r.add(b);
let targets = r.cascade_targets_for_update(&oid("T", b"1"));
assert_eq!(targets.len(), 2);
}
#[test]
fn relationship_kind_distinct() {
assert_ne!(RelationshipKind::Reference, RelationshipKind::Composition);
}
#[test]
fn cascade_modes_distinct() {
assert_ne!(CascadeMode::None, CascadeMode::All);
assert_ne!(CascadeMode::Update, CascadeMode::Delete);
}
#[test]
fn empty_resolver_returns_empty_lists() {
let r = RelationshipResolver::new();
assert!(r.is_empty());
assert!(r.cascade_targets_for_delete(&oid("T", b"x")).is_empty());
assert!(r.cascade_targets_for_update(&oid("T", b"x")).is_empty());
}
}