1use std::collections::HashMap;
2use serde_json::Value;
3
4pub type NodeId = String;
5pub type EdgeId = String;
6
7pub mod cache;
8pub use cache::*;
9
10pub mod preview;
11pub use preview::*;
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Node {
15 pub id: NodeId,
16 pub label: Option<String>,
17 pub attrs: HashMap<String, Value>,
18}
19
20impl Node {
21 pub fn new(id: impl Into<NodeId>) -> Self {
22 Self { id: id.into(), label: None, attrs: HashMap::new() }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct Edge {
28 pub id: EdgeId,
29 pub source: NodeId,
30 pub target: NodeId,
31 pub attrs: HashMap<String, Value>,
32}
33
34impl Edge {
35 pub fn new(id: impl Into<EdgeId>, source: impl Into<NodeId>, target: impl Into<NodeId>) -> Self {
36 Self { id: id.into(), source: source.into(), target: target.into(), attrs: HashMap::new() }
37 }
38}
39
40#[derive(Debug, Clone, Default)]
41pub struct Graph {
42 nodes: HashMap<NodeId, Node>,
43 edges: HashMap<EdgeId, Edge>,
44 adjacency: HashMap<NodeId, Vec<EdgeId>>,
45}
46
47impl Graph {
48 pub fn add_node(&mut self, node: Node) {
49 self.adjacency.entry(node.id.clone()).or_default();
50 self.nodes.insert(node.id.clone(), node);
51 }
52
53 pub fn add_edge(&mut self, edge: Edge) {
54 self.link_adjacency(&edge);
55 self.edges.insert(edge.id.clone(), edge);
56 }
57
58 fn link_adjacency(&mut self, edge: &Edge) {
61 self.adjacency.entry(edge.source.clone()).or_default().push(edge.id.clone());
62 if edge.source != edge.target {
63 self.adjacency.entry(edge.target.clone()).or_default().push(edge.id.clone());
64 }
65 }
66
67 fn unlink_adjacency(&mut self, id: &EdgeId, source: &NodeId, target: &NodeId) {
69 for endpoint in [source, target] {
70 if let Some(list) = self.adjacency.get_mut(endpoint) {
71 list.retain(|e| e != id);
72 }
73 }
74 }
75
76 pub fn node(&self, id: &str) -> Option<&Node> { self.nodes.get(id) }
77 pub fn edge(&self, id: &str) -> Option<&Edge> { self.edges.get(id) }
78 pub fn node_count(&self) -> usize { self.nodes.len() }
79 pub fn edge_count(&self) -> usize { self.edges.len() }
80 pub fn nodes(&self) -> impl Iterator<Item = &Node> { self.nodes.values() }
81 pub fn edges(&self) -> impl Iterator<Item = &Edge> { self.edges.values() }
82
83 pub fn neighbors(&self, id: &str) -> Vec<NodeId> {
85 let mut out = Vec::new();
86 if let Some(edge_ids) = self.adjacency.get(id) {
87 for eid in edge_ids {
88 if let Some(e) = self.edges.get(eid) {
89 let other = if e.source == id { &e.target } else { &e.source };
90 out.push(other.clone());
91 }
92 }
93 }
94 out
95 }
96
97 pub fn degree(&self, id: &str) -> usize {
98 self.adjacency.get(id).map_or(0, Vec::len)
99 }
100
101 pub fn upsert_node(&mut self, n: Node) {
103 self.adjacency.entry(n.id.clone()).or_default();
104 self.nodes.insert(n.id.clone(), n);
105 }
106
107 pub fn upsert_edge(&mut self, e: Edge) {
112 if let Some(old) = self.edges.get(&e.id) {
113 if old.source == e.source && old.target == e.target {
114 self.edges.insert(e.id.clone(), e);
116 return;
117 }
118 let (os, ot) = (old.source.clone(), old.target.clone());
119 self.unlink_adjacency(&e.id, &os, &ot);
120 }
121 self.link_adjacency(&e);
122 self.edges.insert(e.id.clone(), e);
123 }
124
125 pub fn most_central(&self) -> Option<NodeId> {
128 let mut ids: Vec<&NodeId> = self.nodes.keys().collect();
129 ids.sort();
130 let mut best: Option<(&NodeId, usize)> = None;
131 for id in ids {
132 let d = self.degree(id);
133 if best.is_none_or(|(_, bd)| d > bd) {
135 best = Some((id, d));
136 }
137 }
138 best.map(|(id, _)| id.clone())
139 }
140}
141
142#[derive(Debug, Clone, Default)]
143pub struct NavState {
144 pub current: Option<NodeId>,
145 pub history: Vec<NodeId>,
146}
147
148impl NavState {
149 pub fn focus(&mut self, id: NodeId) {
151 if let Some(prev) = self.current.take() {
152 self.history.push(prev);
153 }
154 self.current = Some(id);
155 }
156
157 pub fn back(&mut self) -> Option<NodeId> {
159 let prev = self.history.pop()?;
160 self.current = Some(prev.clone());
161 Some(prev)
162 }
163}
164
165use serde::{Deserialize, Serialize};
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
172pub struct Cursor(pub String);
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct QueryParams {
176 pub limit: usize,
177 pub cursor: Option<Cursor>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum GroupBy {
185 Relationship,
186 Label,
187 Property(String),
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct Aggregate {
199 pub id: NodeId,
201 #[serde(default)]
206 pub parent: NodeId,
207 pub group_by: GroupBy,
208 pub value: String,
210 #[serde(default)]
216 pub relationships: Vec<String>,
217 pub count: u64,
218 pub query: QueryParams,
222}
223
224impl Aggregate {
225 pub fn display(&self) -> String {
228 format!("{} {}", self.count, self.value)
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct NeighborResult {
234 #[serde(default)]
235 pub nodes: Vec<Node>,
236 #[serde(default)]
240 pub edges: Vec<Edge>,
241 #[serde(default)]
242 pub aggregates: Vec<Aggregate>,
243 #[serde(default)]
244 pub next: Option<Cursor>,
245 #[serde(default)]
247 pub pending: bool,
248}
249
250pub trait DataProvider {
251 fn load(&self) -> Graph;
252 fn neighbors(&self, focus: &NodeId, params: &QueryParams) -> NeighborResult;
253}
254
255#[derive(Deserialize)]
256struct JsonNode {
257 id: String,
258 #[serde(default)]
259 label: Option<String>,
260 #[serde(default)]
261 attrs: HashMap<String, Value>,
262}
263
264#[derive(Deserialize)]
265struct JsonEdge {
266 #[serde(default)]
267 id: Option<String>,
268 source: String,
269 target: String,
270 #[serde(default)]
271 attrs: HashMap<String, Value>,
272}
273
274#[derive(Deserialize)]
275struct JsonGraph {
276 nodes: Vec<JsonNode>,
277 edges: Vec<JsonEdge>,
278}
279
280pub struct InMemoryProvider {
281 graph: Graph,
282}
283
284impl InMemoryProvider {
285 pub fn from_json(s: &str) -> Result<Self, String> {
286 use std::collections::HashSet;
287 let jg: JsonGraph = serde_json::from_str(s).map_err(|e| format!("parse: {e}"))?;
288 let mut g = Graph::default();
289
290 let mut node_ids: HashSet<String> = HashSet::new();
291 for n in jg.nodes {
292 if !node_ids.insert(n.id.clone()) {
293 return Err(format!("duplicate node id: {}", n.id));
294 }
295 g.add_node(Node { id: n.id, label: n.label, attrs: n.attrs });
296 }
297
298 let explicit_ids: HashSet<String> = jg.edges.iter().filter_map(|e| e.id.clone()).collect();
299 let mut seen_edge_ids: HashSet<String> = HashSet::new();
300 let mut auto = 0usize;
301 for e in jg.edges {
302 if !node_ids.contains(&e.source) {
303 return Err(format!("edge source not a declared node: {}", e.source));
304 }
305 if !node_ids.contains(&e.target) {
306 return Err(format!("edge target not a declared node: {}", e.target));
307 }
308 let id = match e.id {
309 Some(id) => {
310 if !seen_edge_ids.insert(id.clone()) {
311 return Err(format!("duplicate edge id: {id}"));
312 }
313 id
314 }
315 None => loop {
316 let cand = format!("e{auto}");
317 auto += 1;
318 if !explicit_ids.contains(&cand) && !seen_edge_ids.contains(&cand) {
319 seen_edge_ids.insert(cand.clone());
320 break cand;
321 }
322 },
323 };
324 g.add_edge(Edge { id, source: e.source, target: e.target, attrs: e.attrs });
325 }
326 Ok(Self { graph: g })
327 }
328
329 pub fn from_graph(graph: Graph) -> Self {
330 Self { graph }
331 }
332}
333
334impl DataProvider for InMemoryProvider {
335 fn load(&self) -> Graph {
336 self.graph.clone()
337 }
338
339 fn neighbors(&self, focus: &NodeId, params: &QueryParams) -> NeighborResult {
340 let mut ids = self.graph.neighbors(focus);
341 ids.sort();
342 ids.dedup();
343 let offset = params
344 .cursor
345 .as_ref()
346 .and_then(|c| c.0.parse::<usize>().ok())
347 .unwrap_or(0);
348 let limit = params.limit.max(1);
349 let nodes: Vec<Node> = ids
350 .iter()
351 .skip(offset)
352 .take(limit)
353 .filter_map(|id| self.graph.node(id).cloned())
354 .collect();
355 let consumed = (offset + limit).min(ids.len());
356 let next = if consumed < ids.len() {
357 Some(Cursor(consumed.to_string()))
358 } else {
359 None
360 };
361 NeighborResult { nodes, edges: Vec::new(), aggregates: Vec::new(), next, pending: false }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn add_and_get_nodes_and_edges() {
371 let mut g = Graph::default();
372 g.add_node(Node::new("a"));
373 g.add_node(Node::new("b"));
374 g.add_edge(Edge::new("e1", "a", "b"));
375 assert_eq!(g.node("a").unwrap().id, "a");
376 assert_eq!(g.edge("e1").unwrap().source, "a");
377 assert_eq!(g.node_count(), 2);
378 assert_eq!(g.edge_count(), 1);
379 }
380
381 #[test]
382 fn neighbors_are_undirected() {
383 let mut g = Graph::default();
384 g.add_node(Node::new("a"));
385 g.add_node(Node::new("b"));
386 g.add_node(Node::new("c"));
387 g.add_edge(Edge::new("e1", "a", "b"));
388 g.add_edge(Edge::new("e2", "c", "a"));
389 let mut n = g.neighbors("a");
390 n.sort();
391 assert_eq!(n, vec!["b".to_string(), "c".to_string()]);
392 }
393
394 #[test]
395 fn focus_pushes_previous_onto_history() {
396 let mut nav = NavState::default();
397 nav.focus("a".into());
398 nav.focus("b".into());
399 assert_eq!(nav.current.as_deref(), Some("b"));
400 assert_eq!(nav.history, vec!["a".to_string()]);
401 }
402
403 #[test]
404 fn back_pops_history() {
405 let mut nav = NavState::default();
406 nav.focus("a".into());
407 nav.focus("b".into());
408 assert_eq!(nav.back(), Some("a".to_string()));
409 assert_eq!(nav.current.as_deref(), Some("a"));
410 assert!(nav.history.is_empty());
411 assert_eq!(nav.back(), None);
412 }
413
414 #[test]
415 fn parses_json_graph() {
416 let json = r#"{
417 "nodes": [{"id":"a","label":"A"},{"id":"b"}],
418 "edges": [{"source":"a","target":"b"}]
419 }"#;
420 let p = InMemoryProvider::from_json(json).unwrap();
421 let g = p.load();
422 assert_eq!(g.node_count(), 2);
423 assert_eq!(g.node("a").unwrap().label.as_deref(), Some("A"));
424 assert_eq!(g.edge_count(), 1);
425 assert_eq!(g.edge("e0").unwrap().target, "b");
427 }
428
429 fn star(center: &str, leaves: &[&str]) -> Graph {
430 let mut g = Graph::default();
431 g.add_node(Node::new(center));
432 for (i, l) in leaves.iter().enumerate() {
433 g.add_node(Node::new(*l));
434 g.add_edge(Edge::new(format!("e{i}"), center, *l));
435 }
436 g
437 }
438
439 #[test]
440 fn neighbors_pages_with_cursor_no_gaps() {
441 let g = star("a", &["b", "c", "d", "e", "f"]);
443 let p = InMemoryProvider::from_graph(g);
444
445 let page1 = p.neighbors(&"a".into(), &QueryParams { limit: 2, cursor: None });
446 assert_eq!(page1.nodes.iter().map(|n| n.id.as_str()).collect::<Vec<_>>(), vec!["b", "c"]);
447 assert!(page1.aggregates.is_empty());
448 let c1 = page1.next.clone().expect("more remain after page1");
449
450 let page2 = p.neighbors(&"a".into(), &QueryParams { limit: 2, cursor: Some(c1) });
451 assert_eq!(page2.nodes.iter().map(|n| n.id.as_str()).collect::<Vec<_>>(), vec!["d", "e"]);
452 let c2 = page2.next.clone().expect("more remain after page2");
453
454 let page3 = p.neighbors(&"a".into(), &QueryParams { limit: 2, cursor: Some(c2) });
455 assert_eq!(page3.nodes.iter().map(|n| n.id.as_str()).collect::<Vec<_>>(), vec!["f"]);
456 assert!(page3.next.is_none(), "no more after the last page");
457 }
458
459 #[test]
460 fn neighbors_all_fit_no_cursor() {
461 let g = star("a", &["b", "c"]);
462 let p = InMemoryProvider::from_graph(g);
463 let res = p.neighbors(&"a".into(), &QueryParams { limit: 10, cursor: None });
464 assert_eq!(res.nodes.len(), 2);
465 assert!(res.next.is_none());
466 }
467
468 #[test]
469 fn most_central_picks_highest_degree() {
470 let g = star("h", &["a", "b", "c"]);
472 assert_eq!(g.degree("h"), 3);
473 assert_eq!(g.most_central(), Some("h".to_string()));
474 }
475
476 #[test]
477 fn most_central_breaks_ties_by_smallest_id() {
478 let mut g = Graph::default();
480 g.add_node(Node::new("y"));
481 g.add_node(Node::new("x"));
482 g.add_edge(Edge::new("e", "x", "y"));
483 assert_eq!(g.most_central(), Some("x".to_string()));
484 }
485
486 #[test]
487 fn most_central_empty_is_none() {
488 assert_eq!(Graph::default().most_central(), None);
489 }
490
491 #[test]
492 fn neighbors_paging_survives_dangling_edges() {
493 let mut g = Graph::default();
494 for id in ["a", "n1", "n3"] { g.add_node(Node::new(id)); }
495 g.add_edge(Edge::new("e1", "a", "n1"));
496 g.add_edge(Edge::new("e2", "a", "ghost")); g.add_edge(Edge::new("e3", "a", "n3"));
498 let p = InMemoryProvider::from_graph(g);
499 let mut seen = Vec::new();
500 let mut cursor = None;
501 for _ in 0..10 { let res = p.neighbors(&"a".into(), &QueryParams { limit: 1, cursor });
503 for n in &res.nodes { seen.push(n.id.clone()); }
504 cursor = res.next.clone();
505 if cursor.is_none() { break; }
506 }
507 seen.sort();
508 assert_eq!(seen, vec!["n1".to_string(), "n3".to_string()]); }
510
511 #[test]
512 fn from_json_rejects_dangling_edge_endpoints() {
513 let j = r#"{ "nodes": [{"id":"a"}], "edges": [{"source":"a","target":"ghost"}] }"#;
514 assert!(InMemoryProvider::from_json(j).is_err());
515 }
516
517 #[test]
518 fn from_json_rejects_duplicate_edge_ids() {
519 let j = r#"{ "nodes":[{"id":"a"},{"id":"b"},{"id":"c"}],
520 "edges":[{"id":"e","source":"a","target":"b"},{"id":"e","source":"b","target":"c"}] }"#;
521 assert!(InMemoryProvider::from_json(j).is_err());
522 }
523
524 #[test]
525 fn from_json_rejects_duplicate_node_ids() {
526 let j = r#"{ "nodes":[{"id":"a"},{"id":"a"}], "edges":[] }"#;
527 assert!(InMemoryProvider::from_json(j).is_err());
528 }
529
530 #[test]
531 fn auto_edge_ids_avoid_colliding_with_explicit_ids() {
532 let j = r#"{ "nodes":[{"id":"a"},{"id":"b"},{"id":"c"}],
534 "edges":[{"id":"e0","source":"a","target":"b"},{"source":"b","target":"c"}] }"#;
535 let g = InMemoryProvider::from_json(j).unwrap().load();
536 assert_eq!(g.edge_count(), 2);
537 assert_eq!(g.edge("e0").unwrap().target, "b"); assert_eq!(g.neighbors("c"), vec!["b".to_string()]);
540 }
541
542 #[test]
543 fn auto_edge_ids_avoid_explicit_id_declared_later() {
544 let j = r#"{ "nodes":[{"id":"a"},{"id":"b"},{"id":"c"}],
546 "edges":[{"source":"a","target":"b"},{"id":"e0","source":"b","target":"c"}] }"#;
547 let g = InMemoryProvider::from_json(j).unwrap().load();
548 assert_eq!(g.edge_count(), 2);
549 assert_eq!(g.edge("e0").unwrap().source, "b");
551 assert_eq!(g.edge("e0").unwrap().target, "c");
552 }
553
554 #[test]
555 fn neighbor_result_round_trips_as_json() {
556 let r = NeighborResult {
557 nodes: vec![Node { id: "a".into(), label: Some("A".into()), attrs: HashMap::new() }],
558 edges: vec![Edge { id: "e1".into(), source: "a".into(), target: "b".into(), attrs: HashMap::new() }],
559 aggregates: vec![Aggregate {
560 id: "agg:X".into(),
561 parent: "a".into(),
562 group_by: GroupBy::Label,
563 value: "X".into(),
564 relationships: vec![],
565 count: 12,
566 query: QueryParams { limit: 8, cursor: Some(Cursor("grp:X".into())) },
567 }],
568 next: Some(Cursor("off:8".into())),
569 pending: false,
570 };
571 let s = serde_json::to_string(&r).unwrap();
572 let back: NeighborResult = serde_json::from_str(&s).unwrap();
573 assert_eq!(back.nodes.len(), 1);
574 assert_eq!(back.edges.len(), 1);
575 assert_eq!(back.aggregates[0].count, 12);
576 assert_eq!(back.aggregates[0].query.cursor, Some(Cursor("grp:X".into())));
577 assert_eq!(back.next, Some(Cursor("off:8".into())));
578 assert!(!back.pending);
579 }
580
581 #[test]
582 fn neighbor_result_defaults_edges_and_pending() {
583 let back: NeighborResult =
585 serde_json::from_str(r#"{"nodes":[],"aggregates":[],"next":null}"#).unwrap();
586 assert!(back.edges.is_empty());
587 assert!(!back.pending);
588 }
589
590 #[test]
591 fn upsert_edge_with_new_endpoints_moves_its_adjacency() {
592 let mut g = Graph::default();
593 for id in ["a", "b", "c", "d"] { g.add_node(Node::new(id)); }
594 g.add_edge(Edge::new("e1", "a", "b"));
595 assert_eq!((g.degree("a"), g.degree("b")), (1, 1));
596
597 g.upsert_edge(Edge::new("e1", "c", "d"));
599 assert_eq!(g.edge_count(), 1);
600 assert_eq!(g.degree("a"), 0, "stale adjacency left on the old source");
601 assert_eq!(g.degree("b"), 0, "stale adjacency left on the old target");
602 assert_eq!(g.degree("c"), 1);
603 assert_eq!(g.degree("d"), 1);
604 assert_eq!(g.neighbors("a"), Vec::<String>::new());
605 assert_eq!(g.neighbors("c"), vec!["d".to_string()]);
606 }
607
608 #[test]
609 fn upsert_edge_with_the_same_endpoints_does_not_duplicate_adjacency() {
610 let mut g = Graph::default();
611 for id in ["a", "b"] { g.add_node(Node::new(id)); }
612 g.add_edge(Edge::new("e1", "a", "b"));
613 g.upsert_edge(Edge::new("e1", "a", "b"));
614 g.upsert_edge(Edge::new("e1", "a", "b"));
615 assert_eq!(g.degree("a"), 1);
616 assert_eq!(g.degree("b"), 1);
617 }
618
619 #[test]
620 fn self_loop_counts_once_toward_degree() {
621 let mut g = Graph::default();
622 g.add_node(Node::new("a"));
623 g.add_edge(Edge::new("loop", "a", "a"));
624 assert_eq!(g.degree("a"), 1, "a self-loop is one incident edge, not two");
625
626 for _ in 0..3 { g.upsert_edge(Edge::new("loop", "a", "a")); }
628 assert_eq!(g.degree("a"), 1);
629 assert_eq!(g.edge_count(), 1);
630 }
631
632 #[test]
633 fn upsert_edge_can_turn_an_edge_into_a_self_loop_and_back() {
634 let mut g = Graph::default();
635 for id in ["a", "b"] { g.add_node(Node::new(id)); }
636 g.add_edge(Edge::new("e1", "a", "b"));
637 g.upsert_edge(Edge::new("e1", "a", "a"));
638 assert_eq!(g.degree("a"), 1);
639 assert_eq!(g.degree("b"), 0);
640 g.upsert_edge(Edge::new("e1", "a", "b"));
641 assert_eq!(g.degree("a"), 1);
642 assert_eq!(g.degree("b"), 1);
643 }
644
645 #[test]
646 fn neighbors_handles_bad_and_past_end_cursors() {
647 let g = star("a", &["b", "c"]);
648 let p = InMemoryProvider::from_graph(g);
649 let garbage = p.neighbors(&"a".into(), &QueryParams { limit: 5, cursor: Some(Cursor("xyz".into())) });
650 assert_eq!(garbage.nodes.len(), 2); let past = p.neighbors(&"a".into(), &QueryParams { limit: 5, cursor: Some(Cursor("99".into())) });
652 assert!(past.nodes.is_empty());
653 assert!(past.next.is_none());
654 }
655
656 #[test]
657 fn aggregate_display_reads_naturally_for_each_grouping() {
658 let agg = |gb: GroupBy, value: &str, count: u64| Aggregate {
659 id: "agg:x".into(),
660 parent: String::new(),
661 group_by: gb,
662 value: value.into(),
663 relationships: vec![],
664 count,
665 query: QueryParams { limit: 8, cursor: None },
666 };
667 assert_eq!(agg(GroupBy::Label, "Sorcery", 32).display(), "32 Sorcery");
668 assert_eq!(agg(GroupBy::Relationship, "OF_TYPE", 4).display(), "4 OF_TYPE");
669 assert_eq!(agg(GroupBy::Property("rarity".into()), "rare", 1).display(), "1 rare");
670 }
671
672 #[test]
673 fn aggregate_deserializes_without_parent_or_relationships() {
674 let json = r#"{
678 "id": "agg:Sorcery",
679 "group_by": "label",
680 "value": "Sorcery",
681 "count": 32,
682 "query": { "limit": 8, "cursor": "grp:Sorcery:0" }
683 }"#;
684 let a: Aggregate = serde_json::from_str(json).unwrap();
685 assert_eq!(a.parent, "", "absent parent defaults empty, to be stamped on receipt");
686 assert!(a.relationships.is_empty(), "empty means UNSTATED, never 'no relationships'");
687 assert_eq!(a.group_by, GroupBy::Label);
688 assert_eq!(a.value, "Sorcery");
689 assert_eq!(a.query.cursor.as_ref().unwrap().0, "grp:Sorcery:0");
690 }
691
692 #[test]
693 fn group_by_round_trips_including_the_property_variant() {
694 for gb in [GroupBy::Relationship, GroupBy::Label, GroupBy::Property("rarity".into())] {
695 let s = serde_json::to_string(&gb).unwrap();
696 let back: GroupBy = serde_json::from_str(&s).unwrap();
697 assert_eq!(back, gb, "round-trip failed for {s}");
698 }
699 assert_eq!(serde_json::to_string(&GroupBy::Label).unwrap(), r#""label""#);
701 assert_eq!(serde_json::to_string(&GroupBy::Relationship).unwrap(), r#""relationship""#);
702 assert_eq!(
703 serde_json::to_string(&GroupBy::Property("rarity".into())).unwrap(),
704 r#"{"property":"rarity"}"#
705 );
706 }
707
708 #[test]
709 fn a_wire_aggregate_cannot_forge_its_parent() {
710 let json = r#"{
714 "id": "agg:x", "parent": "lies", "group_by": "label",
715 "value": "X", "count": 1, "query": { "limit": 8, "cursor": null }
716 }"#;
717 let a: Aggregate = serde_json::from_str(json).unwrap();
718 assert_eq!(a.parent, "lies", "deserializes; callers MUST stamp over it");
719 }
720
721 #[test]
722 fn neighbor_result_tolerates_a_partial_host_payload() {
723 let r: NeighborResult = serde_json::from_str(r#"{"nodes":[]}"#).unwrap();
727 assert!(r.nodes.is_empty());
728 assert!(r.edges.is_empty());
729 assert!(r.aggregates.is_empty());
730 assert!(r.next.is_none());
731 assert!(!r.pending, "pending is client-side and must never arrive true");
732
733 let empty: NeighborResult = serde_json::from_str("{}").unwrap();
734 assert!(empty.nodes.is_empty());
735 }
736}