Skip to main content

fabryk_graph/
query.rs

1//! Query response types for graph operations.
2//!
3//! This module provides structured response types used by MCP tools
4//! and other interfaces to return graph query results. All types
5//! derive `Serialize`/`Deserialize` for JSON transport.
6
7use crate::{Edge, Node};
8use serde::{Deserialize, Serialize};
9
10// ============================================================================
11// Node / Edge summaries
12// ============================================================================
13
14/// Summary information about a node.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct NodeSummary {
17    /// Node ID.
18    pub id: String,
19    /// Node title.
20    pub title: String,
21    /// Optional category.
22    pub category: Option<String>,
23    /// Optional description or summary.
24    pub description: Option<String>,
25}
26
27impl From<&Node> for NodeSummary {
28    fn from(node: &Node) -> Self {
29        Self {
30            id: node.id.clone(),
31            title: node.title.clone(),
32            category: node.category.clone(),
33            description: node
34                .metadata
35                .get("description")
36                .and_then(|v| v.as_str())
37                .map(String::from),
38        }
39    }
40}
41
42/// Summary information about an edge.
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct EdgeInfo {
45    /// Source node ID.
46    pub from: String,
47    /// Target node ID.
48    pub to: String,
49    /// Relationship type.
50    pub relationship: String,
51    /// Edge weight.
52    pub weight: f32,
53}
54
55impl From<&Edge> for EdgeInfo {
56    fn from(edge: &Edge) -> Self {
57        Self {
58            from: edge.from.clone(),
59            to: edge.to.clone(),
60            relationship: edge.relationship.name().to_string(),
61            weight: edge.weight,
62        }
63    }
64}
65
66// ============================================================================
67// Related concepts
68// ============================================================================
69
70/// Response for related concepts query.
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct RelatedConceptsResponse {
73    /// The source concept.
74    pub source: NodeSummary,
75    /// Related concepts grouped by relationship type.
76    pub related: Vec<RelatedGroup>,
77    /// Total count of related concepts.
78    pub total_count: usize,
79}
80
81/// A group of related concepts sharing a relationship type.
82#[derive(Clone, Debug, Serialize, Deserialize)]
83pub struct RelatedGroup {
84    /// The relationship type.
85    pub relationship: String,
86    /// Concepts in this group.
87    pub concepts: Vec<NodeSummary>,
88}
89
90// ============================================================================
91// Path
92// ============================================================================
93
94/// Response for concept path query.
95#[derive(Clone, Debug, Serialize, Deserialize)]
96pub struct PathResponse {
97    /// Source node.
98    pub from: NodeSummary,
99    /// Target node.
100    pub to: NodeSummary,
101    /// Path nodes in order (including from and to).
102    pub path: Vec<PathStep>,
103    /// Whether a path was found.
104    pub found: bool,
105    /// Total path length.
106    pub length: usize,
107}
108
109/// A step in a path.
110#[derive(Clone, Debug, Serialize, Deserialize)]
111pub struct PathStep {
112    /// The node at this step.
113    pub node: NodeSummary,
114    /// Relationship to the next node (None for last node).
115    pub relationship_to_next: Option<String>,
116}
117
118// ============================================================================
119// Prerequisites
120// ============================================================================
121
122/// Response for prerequisites query.
123#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct PrerequisitesResponse {
125    /// Target concept.
126    pub target: NodeSummary,
127    /// Prerequisites in learning order (fundamentals first).
128    pub prerequisites: Vec<PrerequisiteInfo>,
129    /// Total count.
130    pub count: usize,
131    /// Whether cycles were detected in prerequisites.
132    pub has_cycles: bool,
133}
134
135/// Information about a prerequisite.
136#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct PrerequisiteInfo {
138    /// The prerequisite node.
139    pub node: NodeSummary,
140    /// Depth in the dependency tree (1 = direct prerequisite).
141    pub depth: usize,
142}
143
144// ============================================================================
145// Neighborhood
146// ============================================================================
147
148/// Response for neighborhood query.
149#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct NeighborhoodResponse {
151    /// Center node.
152    pub center: NodeSummary,
153    /// Nodes in the neighborhood.
154    pub nodes: Vec<NeighborInfo>,
155    /// Edges in the neighborhood.
156    pub edges: Vec<EdgeInfo>,
157    /// Radius used for the query.
158    pub radius: usize,
159}
160
161/// Information about a neighbor node.
162#[derive(Clone, Debug, Serialize, Deserialize)]
163pub struct NeighborInfo {
164    /// The neighbor node.
165    pub node: NodeSummary,
166    /// Distance from center.
167    pub distance: usize,
168}
169
170// ============================================================================
171// Graph info
172// ============================================================================
173
174/// Response for graph info query.
175#[derive(Clone, Debug, Serialize, Deserialize)]
176pub struct GraphInfoResponse {
177    /// Total number of nodes.
178    pub node_count: usize,
179    /// Total number of edges.
180    pub edge_count: usize,
181    /// Categories with counts.
182    pub categories: Vec<CategoryCount>,
183    /// Relationship types with counts.
184    pub relationships: Vec<RelationshipCount>,
185}
186
187/// Category with count.
188#[derive(Clone, Debug, Serialize, Deserialize)]
189pub struct CategoryCount {
190    /// The category name.
191    pub category: String,
192    /// Number of nodes in this category.
193    pub count: usize,
194}
195
196/// Relationship type with count.
197#[derive(Clone, Debug, Serialize, Deserialize)]
198pub struct RelationshipCount {
199    /// The relationship name.
200    pub relationship: String,
201    /// Number of edges with this relationship.
202    pub count: usize,
203}
204
205// ============================================================================
206// Tests
207// ============================================================================
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::types::*;
213
214    #[test]
215    fn test_node_summary_from_node() {
216        let node = Node::new("test-id", "Test Title")
217            .with_category("test-cat")
218            .with_metadata("description", "A test concept");
219
220        let summary = NodeSummary::from(&node);
221
222        assert_eq!(summary.id, "test-id");
223        assert_eq!(summary.title, "Test Title");
224        assert_eq!(summary.category, Some("test-cat".to_string()));
225        assert_eq!(summary.description, Some("A test concept".to_string()));
226    }
227
228    #[test]
229    fn test_node_summary_from_node_no_description() {
230        let node = Node::new("x", "X");
231        let summary = NodeSummary::from(&node);
232
233        assert_eq!(summary.id, "x");
234        assert!(summary.description.is_none());
235        assert!(summary.category.is_none());
236    }
237
238    #[test]
239    fn test_edge_info_from_edge() {
240        let edge = Edge::new("a", "b", Relationship::Prerequisite).with_weight(0.8);
241        let info = EdgeInfo::from(&edge);
242
243        assert_eq!(info.from, "a");
244        assert_eq!(info.to, "b");
245        assert_eq!(info.relationship, "prerequisite");
246        assert_eq!(info.weight, 0.8);
247    }
248
249    #[test]
250    fn test_edge_info_custom_relationship() {
251        let edge = Edge::new("a", "b", Relationship::Custom("implies".to_string()));
252        let info = EdgeInfo::from(&edge);
253
254        assert_eq!(info.relationship, "implies");
255    }
256
257    #[test]
258    fn test_node_summary_serialization() {
259        let summary = NodeSummary {
260            id: "test".to_string(),
261            title: "Test".to_string(),
262            category: Some("cat".to_string()),
263            description: None,
264        };
265
266        let json = serde_json::to_string(&summary).unwrap();
267        let parsed: NodeSummary = serde_json::from_str(&json).unwrap();
268
269        assert_eq!(parsed.id, "test");
270        assert_eq!(parsed.category, Some("cat".to_string()));
271    }
272
273    #[test]
274    fn test_edge_info_serialization() {
275        let info = EdgeInfo {
276            from: "a".to_string(),
277            to: "b".to_string(),
278            relationship: "prerequisite".to_string(),
279            weight: 1.0,
280        };
281
282        let json = serde_json::to_string(&info).unwrap();
283        let parsed: EdgeInfo = serde_json::from_str(&json).unwrap();
284
285        assert_eq!(parsed.from, "a");
286        assert_eq!(parsed.weight, 1.0);
287    }
288
289    #[test]
290    fn test_related_concepts_response_serialization() {
291        let response = RelatedConceptsResponse {
292            source: NodeSummary {
293                id: "src".to_string(),
294                title: "Source".to_string(),
295                category: None,
296                description: None,
297            },
298            related: vec![RelatedGroup {
299                relationship: "prerequisite".to_string(),
300                concepts: vec![NodeSummary {
301                    id: "dep".to_string(),
302                    title: "Dependency".to_string(),
303                    category: None,
304                    description: None,
305                }],
306            }],
307            total_count: 1,
308        };
309
310        let json = serde_json::to_string(&response).unwrap();
311        let parsed: RelatedConceptsResponse = serde_json::from_str(&json).unwrap();
312
313        assert_eq!(parsed.total_count, 1);
314        assert_eq!(parsed.related.len(), 1);
315        assert_eq!(parsed.related[0].concepts.len(), 1);
316    }
317
318    #[test]
319    fn test_path_response_serialization() {
320        let response = PathResponse {
321            from: NodeSummary {
322                id: "a".to_string(),
323                title: "A".to_string(),
324                category: None,
325                description: None,
326            },
327            to: NodeSummary {
328                id: "c".to_string(),
329                title: "C".to_string(),
330                category: None,
331                description: None,
332            },
333            path: vec![
334                PathStep {
335                    node: NodeSummary {
336                        id: "a".to_string(),
337                        title: "A".to_string(),
338                        category: None,
339                        description: None,
340                    },
341                    relationship_to_next: Some("prerequisite".to_string()),
342                },
343                PathStep {
344                    node: NodeSummary {
345                        id: "c".to_string(),
346                        title: "C".to_string(),
347                        category: None,
348                        description: None,
349                    },
350                    relationship_to_next: None,
351                },
352            ],
353            found: true,
354            length: 2,
355        };
356
357        let json = serde_json::to_string(&response).unwrap();
358        let parsed: PathResponse = serde_json::from_str(&json).unwrap();
359
360        assert!(parsed.found);
361        assert_eq!(parsed.path.len(), 2);
362    }
363
364    #[test]
365    fn test_prerequisites_response_serialization() {
366        let response = PrerequisitesResponse {
367            target: NodeSummary {
368                id: "target".to_string(),
369                title: "Target".to_string(),
370                category: None,
371                description: None,
372            },
373            prerequisites: vec![PrerequisiteInfo {
374                node: NodeSummary {
375                    id: "prereq".to_string(),
376                    title: "Prereq".to_string(),
377                    category: None,
378                    description: None,
379                },
380                depth: 1,
381            }],
382            count: 1,
383            has_cycles: false,
384        };
385
386        let json = serde_json::to_string(&response).unwrap();
387        let parsed: PrerequisitesResponse = serde_json::from_str(&json).unwrap();
388
389        assert_eq!(parsed.count, 1);
390        assert!(!parsed.has_cycles);
391    }
392
393    #[test]
394    fn test_neighborhood_response_serialization() {
395        let response = NeighborhoodResponse {
396            center: NodeSummary {
397                id: "center".to_string(),
398                title: "Center".to_string(),
399                category: None,
400                description: None,
401            },
402            nodes: vec![NeighborInfo {
403                node: NodeSummary {
404                    id: "neighbor".to_string(),
405                    title: "Neighbor".to_string(),
406                    category: None,
407                    description: None,
408                },
409                distance: 1,
410            }],
411            edges: vec![EdgeInfo {
412                from: "center".to_string(),
413                to: "neighbor".to_string(),
414                relationship: "relates_to".to_string(),
415                weight: 0.7,
416            }],
417            radius: 2,
418        };
419
420        let json = serde_json::to_string(&response).unwrap();
421        let parsed: NeighborhoodResponse = serde_json::from_str(&json).unwrap();
422
423        assert_eq!(parsed.radius, 2);
424        assert_eq!(parsed.nodes.len(), 1);
425        assert_eq!(parsed.edges.len(), 1);
426    }
427
428    #[test]
429    fn test_graph_info_response_serialization() {
430        let response = GraphInfoResponse {
431            node_count: 42,
432            edge_count: 100,
433            categories: vec![CategoryCount {
434                category: "harmony".to_string(),
435                count: 15,
436            }],
437            relationships: vec![RelationshipCount {
438                relationship: "prerequisite".to_string(),
439                count: 50,
440            }],
441        };
442
443        let json = serde_json::to_string(&response).unwrap();
444        let parsed: GraphInfoResponse = serde_json::from_str(&json).unwrap();
445
446        assert_eq!(parsed.node_count, 42);
447        assert_eq!(parsed.edge_count, 100);
448        assert_eq!(parsed.categories.len(), 1);
449        assert_eq!(parsed.relationships.len(), 1);
450    }
451}