use crate::{Edge, Node};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeSummary {
pub id: String,
pub title: String,
pub category: Option<String>,
pub description: Option<String>,
}
impl From<&Node> for NodeSummary {
fn from(node: &Node) -> Self {
Self {
id: node.id.clone(),
title: node.title.clone(),
category: node.category.clone(),
description: node
.metadata
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EdgeInfo {
pub from: String,
pub to: String,
pub relationship: String,
pub weight: f32,
}
impl From<&Edge> for EdgeInfo {
fn from(edge: &Edge) -> Self {
Self {
from: edge.from.clone(),
to: edge.to.clone(),
relationship: edge.relationship.name().to_string(),
weight: edge.weight,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RelatedConceptsResponse {
pub source: NodeSummary,
pub related: Vec<RelatedGroup>,
pub total_count: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RelatedGroup {
pub relationship: String,
pub concepts: Vec<NodeSummary>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PathResponse {
pub from: NodeSummary,
pub to: NodeSummary,
pub path: Vec<PathStep>,
pub found: bool,
pub length: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PathStep {
pub node: NodeSummary,
pub relationship_to_next: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PrerequisitesResponse {
pub target: NodeSummary,
pub prerequisites: Vec<PrerequisiteInfo>,
pub count: usize,
pub has_cycles: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PrerequisiteInfo {
pub node: NodeSummary,
pub depth: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NeighborhoodResponse {
pub center: NodeSummary,
pub nodes: Vec<NeighborInfo>,
pub edges: Vec<EdgeInfo>,
pub radius: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NeighborInfo {
pub node: NodeSummary,
pub distance: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GraphInfoResponse {
pub node_count: usize,
pub edge_count: usize,
pub categories: Vec<CategoryCount>,
pub relationships: Vec<RelationshipCount>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CategoryCount {
pub category: String,
pub count: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RelationshipCount {
pub relationship: String,
pub count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
#[test]
fn test_node_summary_from_node() {
let node = Node::new("test-id", "Test Title")
.with_category("test-cat")
.with_metadata("description", "A test concept");
let summary = NodeSummary::from(&node);
assert_eq!(summary.id, "test-id");
assert_eq!(summary.title, "Test Title");
assert_eq!(summary.category, Some("test-cat".to_string()));
assert_eq!(summary.description, Some("A test concept".to_string()));
}
#[test]
fn test_node_summary_from_node_no_description() {
let node = Node::new("x", "X");
let summary = NodeSummary::from(&node);
assert_eq!(summary.id, "x");
assert!(summary.description.is_none());
assert!(summary.category.is_none());
}
#[test]
fn test_edge_info_from_edge() {
let edge = Edge::new("a", "b", Relationship::Prerequisite).with_weight(0.8);
let info = EdgeInfo::from(&edge);
assert_eq!(info.from, "a");
assert_eq!(info.to, "b");
assert_eq!(info.relationship, "prerequisite");
assert_eq!(info.weight, 0.8);
}
#[test]
fn test_edge_info_custom_relationship() {
let edge = Edge::new("a", "b", Relationship::Custom("implies".to_string()));
let info = EdgeInfo::from(&edge);
assert_eq!(info.relationship, "implies");
}
#[test]
fn test_node_summary_serialization() {
let summary = NodeSummary {
id: "test".to_string(),
title: "Test".to_string(),
category: Some("cat".to_string()),
description: None,
};
let json = serde_json::to_string(&summary).unwrap();
let parsed: NodeSummary = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "test");
assert_eq!(parsed.category, Some("cat".to_string()));
}
#[test]
fn test_edge_info_serialization() {
let info = EdgeInfo {
from: "a".to_string(),
to: "b".to_string(),
relationship: "prerequisite".to_string(),
weight: 1.0,
};
let json = serde_json::to_string(&info).unwrap();
let parsed: EdgeInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.from, "a");
assert_eq!(parsed.weight, 1.0);
}
#[test]
fn test_related_concepts_response_serialization() {
let response = RelatedConceptsResponse {
source: NodeSummary {
id: "src".to_string(),
title: "Source".to_string(),
category: None,
description: None,
},
related: vec![RelatedGroup {
relationship: "prerequisite".to_string(),
concepts: vec![NodeSummary {
id: "dep".to_string(),
title: "Dependency".to_string(),
category: None,
description: None,
}],
}],
total_count: 1,
};
let json = serde_json::to_string(&response).unwrap();
let parsed: RelatedConceptsResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.total_count, 1);
assert_eq!(parsed.related.len(), 1);
assert_eq!(parsed.related[0].concepts.len(), 1);
}
#[test]
fn test_path_response_serialization() {
let response = PathResponse {
from: NodeSummary {
id: "a".to_string(),
title: "A".to_string(),
category: None,
description: None,
},
to: NodeSummary {
id: "c".to_string(),
title: "C".to_string(),
category: None,
description: None,
},
path: vec![
PathStep {
node: NodeSummary {
id: "a".to_string(),
title: "A".to_string(),
category: None,
description: None,
},
relationship_to_next: Some("prerequisite".to_string()),
},
PathStep {
node: NodeSummary {
id: "c".to_string(),
title: "C".to_string(),
category: None,
description: None,
},
relationship_to_next: None,
},
],
found: true,
length: 2,
};
let json = serde_json::to_string(&response).unwrap();
let parsed: PathResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.found);
assert_eq!(parsed.path.len(), 2);
}
#[test]
fn test_prerequisites_response_serialization() {
let response = PrerequisitesResponse {
target: NodeSummary {
id: "target".to_string(),
title: "Target".to_string(),
category: None,
description: None,
},
prerequisites: vec![PrerequisiteInfo {
node: NodeSummary {
id: "prereq".to_string(),
title: "Prereq".to_string(),
category: None,
description: None,
},
depth: 1,
}],
count: 1,
has_cycles: false,
};
let json = serde_json::to_string(&response).unwrap();
let parsed: PrerequisitesResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.count, 1);
assert!(!parsed.has_cycles);
}
#[test]
fn test_neighborhood_response_serialization() {
let response = NeighborhoodResponse {
center: NodeSummary {
id: "center".to_string(),
title: "Center".to_string(),
category: None,
description: None,
},
nodes: vec![NeighborInfo {
node: NodeSummary {
id: "neighbor".to_string(),
title: "Neighbor".to_string(),
category: None,
description: None,
},
distance: 1,
}],
edges: vec![EdgeInfo {
from: "center".to_string(),
to: "neighbor".to_string(),
relationship: "relates_to".to_string(),
weight: 0.7,
}],
radius: 2,
};
let json = serde_json::to_string(&response).unwrap();
let parsed: NeighborhoodResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.radius, 2);
assert_eq!(parsed.nodes.len(), 1);
assert_eq!(parsed.edges.len(), 1);
}
#[test]
fn test_graph_info_response_serialization() {
let response = GraphInfoResponse {
node_count: 42,
edge_count: 100,
categories: vec![CategoryCount {
category: "harmony".to_string(),
count: 15,
}],
relationships: vec![RelationshipCount {
relationship: "prerequisite".to_string(),
count: 50,
}],
};
let json = serde_json::to_string(&response).unwrap();
let parsed: GraphInfoResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.node_count, 42);
assert_eq!(parsed.edge_count, 100);
assert_eq!(parsed.categories.len(), 1);
assert_eq!(parsed.relationships.len(), 1);
}
}