graphddb_runtime 0.7.5

Rust runtime for GraphDDB — interprets the language-neutral IR (manifest.json + operations.json) and executes the validated access patterns against DynamoDB.
Documentation
//! Multi-operation relation traversal / result assembly — a port of
//! `python/graphddb_runtime/relations.py`.
//!
//! `resultPath` grammar: `$` (root) or `$` + `.`-separated tokens. A trailing
//! `items` token means the write target is a hasMany connection (`{items,
//! cursor}`) and the token before it is the property name; otherwise the final
//! token is the property name for a single-value relation. `items` tokens in the
//! interior iterate into connection elements.
//!
//! The runtime works on the assembled tree as a `serde_json::Value` (the same
//! shape the output boundary uses), so parent collection walks JSON.

use serde_json::Value as Json;

use crate::errors::GraphDDBError;

const ITEMS: &str = "items";

/// Split a `resultPath` into `(parent_tokens, write_key, is_connection)`.
pub fn parse_result_path(path: &str) -> Result<(Vec<String>, String, bool), GraphDDBError> {
    if path == "$" || path.is_empty() {
        return Err(GraphDDBError::new("root operation has no relation path"));
    }
    if !path.starts_with("$.") {
        return Err(GraphDDBError::new(format!(
            "unsupported resultPath '{path}'"
        )));
    }
    let tokens: Vec<String> = path[2..].split('.').map(str::to_string).collect();
    if tokens.last().map(String::as_str) == Some(ITEMS) {
        if tokens.len() < 2 {
            return Err(GraphDDBError::new(format!("malformed resultPath '{path}'")));
        }
        let write_key = tokens[tokens.len() - 2].clone();
        let parent_tokens = tokens[..tokens.len() - 2].to_vec();
        return Ok((parent_tokens, write_key, true));
    }
    let write_key = tokens[tokens.len() - 1].clone();
    let parent_tokens = tokens[..tokens.len() - 1].to_vec();
    Ok((parent_tokens, write_key, false))
}

/// Navigate `parent_tokens` from `root` to the list of parent nodes (mutable),
/// expanding `items` tokens into connection elements and skipping null nodes.
/// Returns mutable references so an operation can write onto each parent.
pub fn collect_parents_mut<'a>(root: &'a mut Json, parent_tokens: &[String]) -> Vec<&'a mut Json> {
    let mut nodes: Vec<&mut Json> = vec![root];
    for token in parent_tokens {
        let mut next: Vec<&mut Json> = Vec::new();
        for node in nodes {
            if token == ITEMS {
                if let Some(items) = node.get_mut(ITEMS).and_then(Json::as_array_mut) {
                    for el in items.iter_mut() {
                        next.push(el);
                    }
                }
            } else if let Some(child) = node.get_mut(token) {
                if !child.is_null() {
                    next.push(child);
                }
            }
        }
        nodes = next;
    }
    // Keep only object nodes.
    nodes.into_iter().filter(|n| n.is_object()).collect()
}

/// The relation path (`ctx.relationPath`) for a fan-out op: the non-`items`
/// tokens of its `resultPath`.
pub fn relation_path(op: &Json) -> Vec<String> {
    let path = op.get("resultPath").and_then(Json::as_str).unwrap_or("$");
    if path == "$" || path.is_empty() {
        return vec![];
    }
    path[2..]
        .split('.')
        .filter(|t| *t != "items")
        .map(str::to_string)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn parse_connection_and_single() {
        assert_eq!(
            parse_result_path("$.members.items").unwrap(),
            (vec![], "members".to_string(), true)
        );
        assert_eq!(
            parse_result_path("$.groups.items.group").unwrap(),
            (
                vec!["groups".to_string(), "items".to_string()],
                "group".to_string(),
                false
            )
        );
    }

    #[test]
    fn collect_from_connection() {
        let mut root = json!({"groups": {"items": [{"a": 1}, {"a": 2}], "cursor": null}});
        let parents = collect_parents_mut(&mut root, &["groups".to_string(), "items".to_string()]);
        assert_eq!(parents.len(), 2);
    }
}