a3s_flow/nodes/
context_get.rs1use async_trait::async_trait;
24use serde_json::Value;
25
26use crate::error::{FlowError, Result};
27use crate::node::{ExecContext, Node};
28
29pub struct ContextGetNode;
31
32#[async_trait]
33impl Node for ContextGetNode {
34 fn node_type(&self) -> &str {
35 "context-get"
36 }
37
38 async fn execute(&self, ctx: ExecContext) -> Result<Value> {
39 let keys = ctx.data["keys"].as_array().ok_or_else(|| {
40 FlowError::InvalidDefinition("context-get: missing or non-array data.keys".into())
41 })?;
42
43 let context = ctx.context.read().unwrap();
44 let mut out = serde_json::Map::new();
45
46 for key_val in keys {
47 let key = key_val.as_str().ok_or_else(|| {
48 FlowError::InvalidDefinition(
49 "context-get: each entry in data.keys must be a string".into(),
50 )
51 })?;
52 let value = context.get(key).cloned().unwrap_or(Value::Null);
53 out.insert(key.to_string(), value);
54 }
55
56 Ok(Value::Object(out))
57 }
58}
59
60#[cfg(test)]
63mod tests {
64 use super::*;
65 use serde_json::json;
66 use std::collections::HashMap;
67 use std::sync::{Arc, RwLock};
68
69 fn ctx_with_context(data: Value, shared: Arc<RwLock<HashMap<String, Value>>>) -> ExecContext {
70 ExecContext {
71 data,
72 context: shared,
73 ..Default::default()
74 }
75 }
76
77 #[tokio::test]
78 async fn reads_existing_keys() {
79 let shared = Arc::new(RwLock::new(HashMap::from([
80 ("user_id".into(), json!("u_42")),
81 ("count".into(), json!(7)),
82 ])));
83 let node = ContextGetNode;
84 let out = node
85 .execute(ctx_with_context(
86 json!({ "keys": ["user_id", "count"] }),
87 Arc::clone(&shared),
88 ))
89 .await
90 .unwrap();
91
92 assert_eq!(out["user_id"], json!("u_42"));
93 assert_eq!(out["count"], json!(7));
94 }
95
96 #[tokio::test]
97 async fn missing_key_returns_null() {
98 let shared = Arc::new(RwLock::new(HashMap::new()));
99 let node = ContextGetNode;
100 let out = node
101 .execute(ctx_with_context(
102 json!({ "keys": ["nonexistent"] }),
103 Arc::clone(&shared),
104 ))
105 .await
106 .unwrap();
107
108 assert_eq!(out["nonexistent"], json!(null));
109 }
110
111 #[tokio::test]
112 async fn empty_keys_returns_empty_object() {
113 let shared = Arc::new(RwLock::new(HashMap::new()));
114 let node = ContextGetNode;
115 let out = node
116 .execute(ctx_with_context(json!({ "keys": [] }), Arc::clone(&shared)))
117 .await
118 .unwrap();
119
120 assert_eq!(out, json!({}));
121 }
122
123 #[tokio::test]
124 async fn missing_keys_field_returns_error() {
125 let node = ContextGetNode;
126 let err = node
127 .execute(ExecContext {
128 data: json!({}),
129 ..Default::default()
130 })
131 .await
132 .unwrap_err();
133 assert!(matches!(err, FlowError::InvalidDefinition(_)));
134 }
135
136 #[tokio::test]
137 async fn non_string_key_entry_returns_error() {
138 let shared = Arc::new(RwLock::new(HashMap::new()));
139 let node = ContextGetNode;
140 let err = node
141 .execute(ctx_with_context(
142 json!({ "keys": [42] }),
143 Arc::clone(&shared),
144 ))
145 .await
146 .unwrap_err();
147 assert!(matches!(err, FlowError::InvalidDefinition(_)));
148 }
149}