use chrono::{DateTime, Utc};
use kanban_core::graph::{Edge, EdgeBase};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::edge_meta::{RelatesKind, Severity};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpawnsEdge {
#[serde(flatten)]
pub base: EdgeBase,
}
impl SpawnsEdge {
pub fn new(parent: Uuid, child: Uuid) -> Self {
Self {
base: EdgeBase::new(parent, child),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlocksEdge {
#[serde(flatten)]
pub base: EdgeBase,
#[serde(default)]
pub severity: Severity,
}
impl BlocksEdge {
pub fn new(blocker: Uuid, blocked: Uuid, severity: Severity) -> Self {
Self {
base: EdgeBase::new(blocker, blocked),
severity,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelatesEdge {
#[serde(flatten)]
pub base: EdgeBase,
#[serde(default)]
pub kind: RelatesKind,
}
impl RelatesEdge {
pub fn new(a: Uuid, b: Uuid, kind: RelatesKind) -> Self {
Self {
base: EdgeBase::new(a, b),
kind,
}
}
}
impl Edge for SpawnsEdge {
type NodeId = Uuid;
fn source(&self) -> Uuid {
self.base.source
}
fn target(&self) -> Uuid {
self.base.target
}
fn created_at(&self) -> DateTime<Utc> {
self.base.created_at
}
fn archived_at(&self) -> Option<DateTime<Utc>> {
self.base.archived_at
}
fn archive(&mut self) {
self.base.archive();
}
fn unarchive(&mut self) {
self.base.unarchive();
}
fn from_endpoints(source: Uuid, target: Uuid) -> Self {
SpawnsEdge::new(source, target)
}
}
impl Edge for BlocksEdge {
type NodeId = Uuid;
fn source(&self) -> Uuid {
self.base.source
}
fn target(&self) -> Uuid {
self.base.target
}
fn created_at(&self) -> DateTime<Utc> {
self.base.created_at
}
fn archived_at(&self) -> Option<DateTime<Utc>> {
self.base.archived_at
}
fn archive(&mut self) {
self.base.archive();
}
fn unarchive(&mut self) {
self.base.unarchive();
}
fn from_endpoints(source: Uuid, target: Uuid) -> Self {
BlocksEdge::new(source, target, Severity::default())
}
}
impl Edge for RelatesEdge {
type NodeId = Uuid;
fn source(&self) -> Uuid {
self.base.source
}
fn target(&self) -> Uuid {
self.base.target
}
fn created_at(&self) -> DateTime<Utc> {
self.base.created_at
}
fn archived_at(&self) -> Option<DateTime<Utc>> {
self.base.archived_at
}
fn archive(&mut self) {
self.base.archive();
}
fn unarchive(&mut self) {
self.base.unarchive();
}
fn from_endpoints(source: Uuid, target: Uuid) -> Self {
RelatesEdge::new(source, target, RelatesKind::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spawns_edge_new_has_no_metadata() {
let parent = Uuid::new_v4();
let child = Uuid::new_v4();
let e = SpawnsEdge::new(parent, child);
assert_eq!(e.source(), parent);
assert_eq!(e.target(), child);
assert!(e.is_active());
}
#[test]
fn test_blocks_edge_carries_severity() {
let blocker = Uuid::new_v4();
let blocked = Uuid::new_v4();
let e = BlocksEdge::new(blocker, blocked, Severity::High);
assert_eq!(e.source(), blocker);
assert_eq!(e.severity, Severity::High);
}
#[test]
fn test_relates_edge_carries_kind() {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let e = RelatesEdge::new(a, b, RelatesKind::Duplicates);
assert_eq!(e.kind, RelatesKind::Duplicates);
}
#[test]
fn test_archive_unarchive_through_edge_trait_round_trips() {
let mut e = BlocksEdge::new(Uuid::new_v4(), Uuid::new_v4(), Severity::Low);
assert!(e.is_active());
e.archive();
assert!(e.is_archived());
e.unarchive();
assert!(e.is_active());
}
#[test]
fn test_per_kind_structs_are_object_safe_as_dyn_edge() {
fn _accepts(_e: &dyn Edge<NodeId = Uuid>) {}
let s = SpawnsEdge::new(Uuid::new_v4(), Uuid::new_v4());
let b = BlocksEdge::new(Uuid::new_v4(), Uuid::new_v4(), Severity::Critical);
let r = RelatesEdge::new(Uuid::new_v4(), Uuid::new_v4(), RelatesKind::General);
_accepts(&s);
_accepts(&b);
_accepts(&r);
}
#[test]
fn test_spawns_edge_serializes_without_metadata_field() {
let e = SpawnsEdge::new(Uuid::nil(), Uuid::from_u128(0x42));
let json = serde_json::to_value(&e).unwrap();
let obj = json.as_object().unwrap();
let keys: Vec<_> = obj.keys().cloned().collect();
keys.iter()
.find(|k| k.as_str() == "source")
.expect("source key");
keys.iter()
.find(|k| k.as_str() == "target")
.expect("target key");
for unexpected in ["severity", "kind", "edge_type", "weight", "direction"] {
assert!(
!keys.iter().any(|k| k == unexpected),
"SpawnsEdge should not serialise {unexpected}; got keys {keys:?}"
);
}
}
#[test]
fn test_blocks_edge_serialises_severity_inline() {
let e = BlocksEdge::new(Uuid::nil(), Uuid::from_u128(0x42), Severity::High);
let json = serde_json::to_value(&e).unwrap();
let obj = json.as_object().unwrap();
assert_eq!(obj["severity"], "High");
assert!(obj.contains_key("source"));
assert!(obj.contains_key("target"));
assert!(
!obj.contains_key("base"),
"flatten should inline EdgeBase, no `base` wrapper key; got {obj:?}"
);
}
#[test]
fn test_blocks_edge_deserialises_missing_severity_as_default() {
let json = serde_json::json!({
"source": Uuid::nil(),
"target": Uuid::from_u128(0x42),
"created_at": "2024-01-01T00:00:00Z",
"archived_at": null,
});
let e: BlocksEdge = serde_json::from_value(json).unwrap();
assert_eq!(e.severity, Severity::Medium);
}
#[test]
fn test_relates_edge_deserialises_missing_kind_as_default() {
let json = serde_json::json!({
"source": Uuid::nil(),
"target": Uuid::from_u128(0x42),
"created_at": "2024-01-01T00:00:00Z",
"archived_at": null,
});
let e: RelatesEdge = serde_json::from_value(json).unwrap();
assert_eq!(e.kind, RelatesKind::General);
}
}