Skip to main content

geode_client/
graph.rs

1//! Graph-native entity types: [`Node`], [`Edge`], and [`Path`].
2//!
3//! When a GQL query projects a node, edge, or path variable
4//! (e.g. `MATCH (n) RETURN n`), the driver surfaces the value as an
5//! object-shaped [`Value`] with well-known keys. The helpers in this
6//! module ([`as_node`], [`as_edge`], [`as_path`]) decode those objects
7//! into the typed structs below.
8//!
9//! These mirror the Go reference client's `Node`/`Edge`/`Path` types and
10//! `AsNode`/`AsEdge`/`AsPath` conversions.
11//!
12//! # Example
13//!
14//! ```no_run
15//! # use geode_client::{Client, graph};
16//! # async fn example() -> geode_client::Result<()> {
17//! # let client = Client::new("localhost", 3141).skip_verify(true);
18//! # let mut conn = client.connect().await?;
19//! let (page, _) = conn.query("MATCH (n:Person) RETURN n LIMIT 1").await?;
20//! if let Some(v) = page.rows[0].get("n") {
21//!     if let Some(node) = graph::as_node(v) {
22//!         println!("node {} labels {:?}", node.id, node.labels);
23//!     }
24//! }
25//! # Ok(())
26//! # }
27//! ```
28
29use std::collections::HashMap;
30
31use crate::types::Value;
32
33/// The typed representation of a Geode graph node value.
34///
35/// Produced by [`as_node`] from the object-shaped [`Value`] the driver
36/// returns when scanning a `NODE` column.
37#[derive(Debug, Clone, PartialEq)]
38pub struct Node {
39    /// The node's stable identifier.
40    pub id: i64,
41    /// The node's labels.
42    pub labels: Vec<String>,
43    /// The node's properties.
44    pub properties: HashMap<String, Value>,
45}
46
47/// The typed representation of a Geode graph edge (relationship) value.
48///
49/// Produced by [`as_edge`] from the object-shaped [`Value`] the driver
50/// returns when scanning an `EDGE` column.
51#[derive(Debug, Clone, PartialEq)]
52pub struct Edge {
53    /// The edge's stable identifier.
54    pub id: i64,
55    /// The identifier of the edge's start (source) node.
56    pub start_node: i64,
57    /// The identifier of the edge's end (target) node.
58    pub end_node: i64,
59    /// The edge type (relationship label).
60    pub edge_type: String,
61    /// The edge's properties.
62    pub properties: HashMap<String, Value>,
63}
64
65/// The typed representation of a Geode graph path value: the ordered
66/// sequence of nodes and edges along a traversal.
67///
68/// Produced by [`as_path`] from the object-shaped [`Value`] the driver
69/// returns when scanning a `PATH` column.
70#[derive(Debug, Clone, PartialEq)]
71pub struct Path {
72    /// The nodes along the path.
73    pub nodes: Vec<Node>,
74    /// The edges along the path.
75    pub edges: Vec<Edge>,
76}
77
78/// Convert a driver [`Value`] into a typed [`Node`].
79///
80/// Returns `None` if `v` is not a node object: it must be an object that
81/// has a `labels` key and does *not* have a `start_node` key (which would
82/// make it an edge).
83pub fn as_node(v: &Value) -> Option<Node> {
84    let obj = v.as_object().ok()?;
85    if !obj.contains_key("labels") {
86        return None;
87    }
88    if obj.contains_key("start_node") {
89        return None; // edges also have "id"
90    }
91    let id = obj.get("id").and_then(|v| v.as_int().ok()).unwrap_or(0);
92    let labels = obj
93        .get("labels")
94        .and_then(|v| v.as_array().ok())
95        .map(|arr| {
96            arr.iter()
97                .filter_map(|item| item.as_string().ok().map(String::from))
98                .collect()
99        })
100        .unwrap_or_default();
101    let properties = obj
102        .get("properties")
103        .and_then(|v| v.as_object().ok())
104        .cloned()
105        .unwrap_or_default();
106    Some(Node {
107        id,
108        labels,
109        properties,
110    })
111}
112
113/// Convert a driver [`Value`] into a typed [`Edge`].
114///
115/// Returns `None` if `v` is not an edge object: it must be an object that
116/// has both `start_node` and `end_node` keys.
117pub fn as_edge(v: &Value) -> Option<Edge> {
118    let obj = v.as_object().ok()?;
119    if !obj.contains_key("start_node") || !obj.contains_key("end_node") {
120        return None;
121    }
122    let id = obj.get("id").and_then(|v| v.as_int().ok()).unwrap_or(0);
123    let start_node = obj
124        .get("start_node")
125        .and_then(|v| v.as_int().ok())
126        .unwrap_or(0);
127    let end_node = obj
128        .get("end_node")
129        .and_then(|v| v.as_int().ok())
130        .unwrap_or(0);
131    let edge_type = obj
132        .get("type")
133        .and_then(|v| v.as_string().ok())
134        .map(String::from)
135        .unwrap_or_default();
136    let properties = obj
137        .get("properties")
138        .and_then(|v| v.as_object().ok())
139        .cloned()
140        .unwrap_or_default();
141    Some(Edge {
142        id,
143        start_node,
144        end_node,
145        edge_type,
146        properties,
147    })
148}
149
150/// Convert a driver [`Value`] into a typed [`Path`].
151///
152/// Returns `None` if `v` is not a path object: it must be an object that
153/// has both `nodes` and `edges` keys. Individual entries that fail to
154/// decode as a [`Node`]/[`Edge`] are skipped.
155pub fn as_path(v: &Value) -> Option<Path> {
156    let obj = v.as_object().ok()?;
157    if !obj.contains_key("nodes") || !obj.contains_key("edges") {
158        return None;
159    }
160    let nodes = obj
161        .get("nodes")
162        .and_then(|v| v.as_array().ok())
163        .map(|arr| arr.iter().filter_map(as_node).collect())
164        .unwrap_or_default();
165    let edges = obj
166        .get("edges")
167        .and_then(|v| v.as_array().ok())
168        .map(|arr| arr.iter().filter_map(as_edge).collect())
169        .unwrap_or_default();
170    Some(Path { nodes, edges })
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn node_value(id: i64, labels: &[&str], props: &[(&str, Value)]) -> Value {
178        let mut obj = HashMap::new();
179        obj.insert("id".to_string(), Value::int(id));
180        obj.insert(
181            "labels".to_string(),
182            Value::array(labels.iter().map(|l| Value::string(*l)).collect()),
183        );
184        let mut p = HashMap::new();
185        for (k, v) in props {
186            p.insert(k.to_string(), v.clone());
187        }
188        obj.insert("properties".to_string(), Value::object(p));
189        Value::object(obj)
190    }
191
192    fn edge_value(id: i64, start: i64, end: i64, typ: &str) -> Value {
193        let mut obj = HashMap::new();
194        obj.insert("id".to_string(), Value::int(id));
195        obj.insert("start_node".to_string(), Value::int(start));
196        obj.insert("end_node".to_string(), Value::int(end));
197        obj.insert("type".to_string(), Value::string(typ));
198        obj.insert("properties".to_string(), Value::object(HashMap::new()));
199        Value::object(obj)
200    }
201
202    #[test]
203    fn test_as_node_from_object() {
204        let v = node_value(42, &["Person", "User"], &[("name", Value::string("Alice"))]);
205        let node = as_node(&v).expect("should decode node");
206        assert_eq!(node.id, 42);
207        assert_eq!(node.labels, vec!["Person".to_string(), "User".to_string()]);
208        assert_eq!(
209            node.properties.get("name").unwrap().as_string().unwrap(),
210            "Alice"
211        );
212    }
213
214    #[test]
215    fn test_as_edge_from_object() {
216        let v = edge_value(100, 1, 2, "KNOWS");
217        let edge = as_edge(&v).expect("should decode edge");
218        assert_eq!(edge.id, 100);
219        assert_eq!(edge.start_node, 1);
220        assert_eq!(edge.end_node, 2);
221        assert_eq!(edge.edge_type, "KNOWS");
222        assert!(edge.properties.is_empty());
223    }
224
225    #[test]
226    fn test_as_path_from_object() {
227        let n1 = node_value(1, &["Person"], &[]);
228        let n2 = node_value(2, &["Person"], &[]);
229        let e1 = edge_value(10, 1, 2, "KNOWS");
230        let mut obj = HashMap::new();
231        obj.insert("nodes".to_string(), Value::array(vec![n1, n2]));
232        obj.insert("edges".to_string(), Value::array(vec![e1]));
233        let v = Value::object(obj);
234
235        let path = as_path(&v).expect("should decode path");
236        assert_eq!(path.nodes.len(), 2);
237        assert_eq!(path.edges.len(), 1);
238        assert_eq!(path.nodes[0].id, 1);
239        assert_eq!(path.nodes[1].id, 2);
240        assert_eq!(path.edges[0].edge_type, "KNOWS");
241    }
242
243    #[test]
244    fn test_as_node_rejects_scalar() {
245        assert!(as_node(&Value::int(5)).is_none());
246        assert!(as_node(&Value::string("x")).is_none());
247        assert!(as_node(&Value::null()).is_none());
248        // An edge object (has start_node) must not decode as a node.
249        assert!(as_node(&edge_value(1, 2, 3, "T")).is_none());
250    }
251
252    #[test]
253    fn test_as_edge_rejects_node() {
254        let v = node_value(1, &["Person"], &[]);
255        assert!(as_edge(&v).is_none());
256        assert!(as_edge(&Value::int(5)).is_none());
257    }
258
259    #[test]
260    fn test_as_path_rejects_non_path() {
261        assert!(as_path(&node_value(1, &["Person"], &[])).is_none());
262        assert!(as_path(&Value::int(5)).is_none());
263    }
264}