Skip to main content

graphddb_runtime/
relations.rs

1//! Multi-operation relation traversal / result assembly — a port of
2//! `python/graphddb_runtime/relations.py`.
3//!
4//! `resultPath` grammar: `$` (root) or `$` + `.`-separated tokens. A trailing
5//! `items` token means the write target is a hasMany connection (`{items,
6//! cursor}`) and the token before it is the property name; otherwise the final
7//! token is the property name for a single-value relation. `items` tokens in the
8//! interior iterate into connection elements.
9//!
10//! The runtime works on the assembled tree as a `serde_json::Value` (the same
11//! shape the output boundary uses), so parent collection walks JSON.
12
13use serde_json::Value as Json;
14
15use crate::errors::GraphDDBError;
16
17const ITEMS: &str = "items";
18
19/// Split a `resultPath` into `(parent_tokens, write_key, is_connection)`.
20pub fn parse_result_path(path: &str) -> Result<(Vec<String>, String, bool), GraphDDBError> {
21    if path == "$" || path.is_empty() {
22        return Err(GraphDDBError::new("root operation has no relation path"));
23    }
24    if !path.starts_with("$.") {
25        return Err(GraphDDBError::new(format!(
26            "unsupported resultPath '{path}'"
27        )));
28    }
29    let tokens: Vec<String> = path[2..].split('.').map(str::to_string).collect();
30    if tokens.last().map(String::as_str) == Some(ITEMS) {
31        if tokens.len() < 2 {
32            return Err(GraphDDBError::new(format!("malformed resultPath '{path}'")));
33        }
34        let write_key = tokens[tokens.len() - 2].clone();
35        let parent_tokens = tokens[..tokens.len() - 2].to_vec();
36        return Ok((parent_tokens, write_key, true));
37    }
38    let write_key = tokens[tokens.len() - 1].clone();
39    let parent_tokens = tokens[..tokens.len() - 1].to_vec();
40    Ok((parent_tokens, write_key, false))
41}
42
43/// Navigate `parent_tokens` from `root` to the list of parent nodes (mutable),
44/// expanding `items` tokens into connection elements and skipping null nodes.
45/// Returns mutable references so an operation can write onto each parent.
46pub fn collect_parents_mut<'a>(root: &'a mut Json, parent_tokens: &[String]) -> Vec<&'a mut Json> {
47    let mut nodes: Vec<&mut Json> = vec![root];
48    for token in parent_tokens {
49        let mut next: Vec<&mut Json> = Vec::new();
50        for node in nodes {
51            if token == ITEMS {
52                if let Some(items) = node.get_mut(ITEMS).and_then(Json::as_array_mut) {
53                    for el in items.iter_mut() {
54                        next.push(el);
55                    }
56                }
57            } else if let Some(child) = node.get_mut(token) {
58                if !child.is_null() {
59                    next.push(child);
60                }
61            }
62        }
63        nodes = next;
64    }
65    // Keep only object nodes.
66    nodes.into_iter().filter(|n| n.is_object()).collect()
67}
68
69/// The relation path (`ctx.relationPath`) for a fan-out op: the non-`items`
70/// tokens of its `resultPath`.
71pub fn relation_path(op: &Json) -> Vec<String> {
72    let path = op.get("resultPath").and_then(Json::as_str).unwrap_or("$");
73    if path == "$" || path.is_empty() {
74        return vec![];
75    }
76    path[2..]
77        .split('.')
78        .filter(|t| *t != "items")
79        .map(str::to_string)
80        .collect()
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use serde_json::json;
87
88    #[test]
89    fn parse_connection_and_single() {
90        assert_eq!(
91            parse_result_path("$.members.items").unwrap(),
92            (vec![], "members".to_string(), true)
93        );
94        assert_eq!(
95            parse_result_path("$.groups.items.group").unwrap(),
96            (
97                vec!["groups".to_string(), "items".to_string()],
98                "group".to_string(),
99                false
100            )
101        );
102    }
103
104    #[test]
105    fn collect_from_connection() {
106        let mut root = json!({"groups": {"items": [{"a": 1}, {"a": 2}], "cursor": null}});
107        let parents = collect_parents_mut(&mut root, &["groups".to_string(), "items".to_string()]);
108        assert_eq!(parents.len(), 2);
109    }
110}