use serde::Serialize;
use super::{Adr, Status};
#[derive(Debug, Clone, Serialize)]
pub struct Node {
pub id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
impl Node {
#[must_use]
pub fn from_adr(adr: &Adr) -> Self {
Self {
id: adr.id().as_str().to_string(),
status: adr.status().as_str().to_string(),
title: Some(adr.title().to_string()),
}
}
#[must_use]
pub fn placeholder(id: impl Into<String>) -> Self {
Self {
id: id.into(),
status: Status::default().as_str().to_string(),
title: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EdgeType {
Related,
Supersedes,
}
impl EdgeType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Related => "related",
Self::Supersedes => "supersedes",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Edge {
pub source: String,
pub target: String,
#[serde(rename = "type")]
pub edge_type: EdgeType,
}
impl Edge {
#[must_use]
pub fn new(source: impl Into<String>, target: impl Into<String>, edge_type: EdgeType) -> Self {
Self {
source: source.into(),
target: target.into(),
edge_type,
}
}
#[must_use]
pub fn related(source: impl Into<String>, target: impl Into<String>) -> Self {
Self::new(source, target, EdgeType::Related)
}
#[must_use]
pub fn supersedes(source: impl Into<String>, target: impl Into<String>) -> Self {
Self::new(source, target, EdgeType::Supersedes)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
}
impl Graph {
#[must_use]
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
}
}
#[must_use]
pub fn from_adrs(adrs: &[Adr]) -> Self {
let mut nodes: Vec<Node> = adrs.iter().map(Node::from_adr).collect();
let mut edges: Vec<Edge> = Vec::new();
let known_ids: std::collections::HashSet<&str> =
adrs.iter().map(|a| a.id().as_str()).collect();
for adr in adrs {
let source_id = adr.id().as_str();
for related_ref in adr.related() {
let target_id = extract_id_from_ref(related_ref);
edges.push(Edge::related(source_id, &target_id));
if !known_ids.contains(target_id.as_str()) {
nodes.push(Node::placeholder(&target_id));
}
}
}
nodes.dedup_by(|a, b| a.id == b.id);
Self { nodes, edges }
}
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
impl Default for Graph {
fn default() -> Self {
Self::new()
}
}
fn extract_id_from_ref(reference: &str) -> String {
reference
.strip_suffix(".md")
.unwrap_or(reference)
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{AdrId, Frontmatter};
use std::path::PathBuf;
fn create_test_adr(id: &str, related: Vec<String>) -> Adr {
let frontmatter = Frontmatter::new(format!("Test {id}")).with_related(related);
Adr::new(
AdrId::new(id),
format!("{id}.md"),
PathBuf::from(format!("{id}.md")),
frontmatter,
String::new(),
String::new(),
String::new(),
)
}
#[test]
fn test_node_from_adr() {
let adr = create_test_adr("adr_0001", vec![]);
let node = Node::from_adr(&adr);
assert_eq!(node.id, "adr_0001");
assert_eq!(node.status, "proposed");
assert_eq!(node.title, Some("Test adr_0001".to_string()));
}
#[test]
fn test_node_placeholder() {
let node = Node::placeholder("adr_0005");
assert_eq!(node.id, "adr_0005");
assert!(node.title.is_none());
}
#[test]
fn test_edge_creation() {
let edge = Edge::related("adr_0001", "adr_0005");
assert_eq!(edge.source, "adr_0001");
assert_eq!(edge.target, "adr_0005");
assert_eq!(edge.edge_type, EdgeType::Related);
}
#[test]
fn test_graph_from_adrs() {
let adrs = vec![
create_test_adr("adr_0001", vec!["adr_0002.md".to_string()]),
create_test_adr("adr_0002", vec![]),
];
let graph = Graph::from_adrs(&adrs);
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
assert_eq!(graph.edges[0].source, "adr_0001");
assert_eq!(graph.edges[0].target, "adr_0002");
}
#[test]
fn test_graph_with_missing_reference() {
let adrs = vec![create_test_adr(
"adr_0001",
vec!["adr_missing.md".to_string()],
)];
let graph = Graph::from_adrs(&adrs);
assert_eq!(graph.node_count(), 2);
assert!(graph.nodes.iter().any(|n| n.id == "adr_missing"));
}
#[test]
fn test_extract_id_from_ref() {
assert_eq!(extract_id_from_ref("adr_0005.md"), "adr_0005");
assert_eq!(extract_id_from_ref("adr_0005"), "adr_0005");
}
#[test]
fn test_edge_type_as_str() {
assert_eq!(EdgeType::Related.as_str(), "related");
assert_eq!(EdgeType::Supersedes.as_str(), "supersedes");
}
#[test]
fn test_edge_supersedes() {
let edge = Edge::supersedes("adr_0002", "adr_0001");
assert_eq!(edge.source, "adr_0002");
assert_eq!(edge.target, "adr_0001");
assert_eq!(edge.edge_type, EdgeType::Supersedes);
}
#[test]
fn test_graph_new() {
let graph = Graph::new();
assert!(graph.is_empty());
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_graph_default() {
let graph = Graph::default();
assert!(graph.is_empty());
assert_eq!(graph.nodes.len(), 0);
assert_eq!(graph.edges.len(), 0);
}
#[test]
fn test_graph_is_empty() {
let empty_graph = Graph::new();
assert!(empty_graph.is_empty());
let non_empty_graph = Graph::from_adrs(&[create_test_adr("adr_0001", vec![])]);
assert!(!non_empty_graph.is_empty());
}
#[test]
fn test_edge_new() {
let edge = Edge::new("source", "target", EdgeType::Related);
assert_eq!(edge.source, "source");
assert_eq!(edge.target, "target");
assert_eq!(edge.edge_type, EdgeType::Related);
}
#[test]
fn test_node_serialization() {
let node = Node::placeholder("adr_0001");
let json = serde_json::to_string(&node).expect("should serialize");
assert!(json.contains("\"id\":\"adr_0001\""));
assert!(!json.contains("\"title\":null"));
}
#[test]
fn test_edge_serialization() {
let edge = Edge::supersedes("adr_0002", "adr_0001");
let json = serde_json::to_string(&edge).expect("should serialize");
assert!(json.contains("\"source\":\"adr_0002\""));
assert!(json.contains("\"target\":\"adr_0001\""));
assert!(json.contains("\"type\":\"supersedes\""));
}
#[test]
fn test_graph_serialization() {
let adrs = vec![
create_test_adr("adr_0001", vec!["adr_0002.md".to_string()]),
create_test_adr("adr_0002", vec![]),
];
let graph = Graph::from_adrs(&adrs);
let json = serde_json::to_string(&graph).expect("should serialize");
assert!(json.contains("\"nodes\""));
assert!(json.contains("\"edges\""));
}
}