metalcraft_flows/
state.rs1use crate::nodes::InputSpec;
13use serde_json::{Map, Value};
14use std::collections::BTreeMap;
15
16pub fn lookup_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
21 let path = path.trim();
22 if path.is_empty() || path == "." {
23 return Some(root);
24 }
25 let mut cur = root;
26 for seg in path.split('.') {
27 if seg.is_empty() {
28 continue;
29 }
30 cur = cur.get(seg)?;
31 }
32 Some(cur)
33}
34
35#[derive(Debug, Clone, Default, PartialEq)]
37pub struct Variables {
38 root: Value,
39}
40
41impl Variables {
42 pub fn new() -> Self {
44 Self { root: Value::Object(Map::new()) }
45 }
46
47 pub fn from_value(value: Value) -> Self {
49 if value.is_object() {
50 Self { root: value }
51 } else {
52 Self::new()
53 }
54 }
55
56 pub fn seed_from_inputs(
61 inputs: &BTreeMap<String, InputSpec>,
62 args: &Value,
63 ) -> (Self, Vec<String>) {
64 let mut state = Self::new();
65 let mut missing = Vec::new();
66 let mut seeded = Map::new();
67 for (name, spec) in inputs {
68 let provided = args.get(name).cloned();
69 let value = match provided {
70 Some(v) => Some(v),
71 None => match &spec.default {
72 Some(d) => Some(d.clone()),
73 None => {
74 if spec.required {
75 missing.push(name.clone());
76 }
77 None
78 }
79 },
80 };
81 if let Some(v) = value {
82 state.set(name, v.clone());
83 seeded.insert(name.clone(), v);
84 }
85 }
86 state.set("_inputs", Value::Object(seeded));
87 (state, missing)
88 }
89
90 pub fn as_value(&self) -> &Value {
92 &self.root
93 }
94
95 pub fn into_value(self) -> Value {
97 self.root
98 }
99
100 pub fn get(&self, path: &str) -> Option<&Value> {
102 lookup_path(&self.root, path)
103 }
104
105 pub fn set(&mut self, name: &str, value: Value) {
107 if let Value::Object(map) = &mut self.root {
108 map.insert(name.to_string(), value);
109 }
110 }
111
112 pub fn set_last(&mut self, value: Value) {
114 self.set("_last", value);
115 }
116
117 pub fn set_path(&mut self, path: &str, value: Value) {
120 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
121 if segments.is_empty() {
122 return;
123 }
124 let mut cur = &mut self.root;
125 for seg in &segments[..segments.len() - 1] {
126 if !cur.is_object() {
127 *cur = Value::Object(Map::new());
128 }
129 let map = cur.as_object_mut().expect("just ensured object");
130 cur = map.entry(seg.to_string()).or_insert_with(|| Value::Object(Map::new()));
131 }
132 if !cur.is_object() {
133 *cur = Value::Object(Map::new());
134 }
135 cur.as_object_mut()
136 .expect("just ensured object")
137 .insert(segments[segments.len() - 1].to_string(), value);
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use serde_json::json;
145
146 #[test]
147 fn lookup_nested_and_root() {
148 let v = json!({ "a": { "b": 2 } });
149 assert_eq!(lookup_path(&v, "a.b"), Some(&json!(2)));
150 assert_eq!(lookup_path(&v, "."), Some(&v));
151 assert_eq!(lookup_path(&v, "a.missing"), None);
152 }
153
154 #[test]
155 fn set_and_get() {
156 let mut s = Variables::new();
157 s.set("temp", json!(18));
158 s.set_last(json!({ "celsius": 5 }));
159 assert_eq!(s.get("temp"), Some(&json!(18)));
160 assert_eq!(s.get("_last.celsius"), Some(&json!(5)));
161 }
162
163 #[test]
164 fn set_path_creates_intermediates() {
165 let mut s = Variables::new();
166 s.set_path("triage.severity", json!("P0"));
167 assert_eq!(s.get("triage.severity"), Some(&json!("P0")));
168 }
169
170 #[test]
171 fn seed_reports_missing_required() {
172 let mut inputs = BTreeMap::new();
173 inputs.insert("repo".to_string(), InputSpec { type_name: "string".into(), required: true, default: None });
174 inputs.insert("since".to_string(), InputSpec { type_name: "string".into(), required: false, default: Some(json!("24h")) });
175
176 let (state, missing) = Variables::seed_from_inputs(&inputs, &json!({ "repo": "acme/app" }));
177 assert!(missing.is_empty());
178 assert_eq!(state.get("repo"), Some(&json!("acme/app")));
179 assert_eq!(state.get("since"), Some(&json!("24h")));
180
181 let (_state2, missing2) = Variables::seed_from_inputs(&inputs, &json!({}));
182 assert_eq!(missing2, vec!["repo".to_string()]);
183 }
184}