state_engine/manifest/
mod.rs1use serde_json::Value;
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7use crate::common::DotArrayAccessor;
8use crate::ports::provided;
9
10pub struct Manifest {
11 manifest_dir: PathBuf,
12 cache: HashMap<String, Value>,
13 missing_keys: Vec<String>,
14}
15
16impl Manifest {
17 pub fn new(manifest_dir: &str) -> Self {
18 Self {
19 manifest_dir: PathBuf::from(manifest_dir),
20 cache: HashMap::new(),
21 missing_keys: Vec::new(),
22 }
23 }
24
25 pub fn get(&mut self, key: &str, default: Option<Value>) -> Value {
29 let mut parts = key.splitn(2, '.');
30 let file = parts.next().unwrap_or("").to_string();
31 let path = parts.next().unwrap_or("").to_string();
32
33 if let Err(_e) = self.load_file(&file) {
34 self.missing_keys.push(key.to_string());
35 return default.unwrap_or(Value::Null);
36 }
37
38 let value = if path.is_empty() {
39 self.cache.get(&file).cloned()
41 } else {
42 self.cache.get(&file).and_then(|data| {
44 let mut accessor = DotArrayAccessor::new();
45 accessor.get(data, &path).cloned()
46 })
47 };
48
49 match value {
50 Some(v) => {
51 self.remove_meta(&v)
52 }
53 None => {
54 self.missing_keys.push(key.to_string());
55 default.unwrap_or(Value::Null)
56 }
57 }
58 }
59
60 pub fn get_meta(&mut self, key: &str) -> HashMap<String, Value> {
64 use regex::Regex;
65
66 let mut parts = key.splitn(2, '.');
67 let file = parts.next().unwrap_or("").to_string();
68 let path = parts.next().unwrap_or("").to_string();
69
70 if self.load_file(&file).is_err() {
71 self.missing_keys.push(key.to_string());
72 return HashMap::new();
73 }
74
75 let Some(root) = self.cache.get(&file) else {
76 return HashMap::new();
77 };
78
79 let mut nodes = vec![root.clone()];
81 if !path.is_empty() {
82 let mut accessor = DotArrayAccessor::new();
83 let mut current = root;
84 for segment in path.split('.') {
85 current = match accessor.get(current, segment) {
86 Some(node) => node,
87 None => {
88 self.missing_keys.push(key.to_string());
89 return HashMap::new();
90 }
91 };
92 nodes.push(current.clone());
93 }
94 }
95
96 let mut meta: HashMap<String, Value> = HashMap::new();
97 let segments: Vec<&str> = if path.is_empty() {
98 vec![]
99 } else {
100 path.split('.').collect()
101 };
102
103 let mut meta_paths: HashMap<String, String> = HashMap::new();
105
106 for (depth, node) in nodes.iter().enumerate() {
108 let Value::Object(obj) = node else {
109 continue;
110 };
111
112 let node_path = if depth == 0 {
114 file.clone()
115 } else {
116 let node_segments: Vec<&str> = segments.iter().copied().take(depth).collect();
117 if node_segments.is_empty() {
118 file.clone()
119 } else {
120 format!("{}.{}", file, node_segments.join("."))
121 }
122 };
123
124 for (k, v) in obj {
126 if k.starts_with('_') {
127 if let Some(existing_value) = meta.get(k).cloned() {
129 if existing_value.is_object() && v.is_object() {
130 if let (Value::Object(existing_obj), Value::Object(new_obj)) = (&existing_value, v) {
131 let mut merged = existing_obj.clone();
132 for (child_key, child_value) in new_obj {
133 merged.insert(child_key.clone(), child_value.clone());
134 }
135 meta.insert(k.clone(), Value::Object(merged));
136 }
137 } else {
138 meta.insert(k.clone(), v.clone());
139 }
140 } else {
141 meta.insert(k.clone(), v.clone());
142 }
143
144 if k == "_load" {
146 if let Value::Object(load_obj) = v {
147 if load_obj.contains_key("map") {
148 meta_paths.insert("_load.map".to_string(), node_path.clone());
149 }
150 }
151 }
152 }
153 }
154 }
155
156 if let Some(map_parent) = meta_paths.get("_load.map") {
158 if let Some(Value::Object(load_obj)) = meta.get_mut("_load") {
159 if let Some(Value::Object(map_obj)) = load_obj.get("map").cloned() {
160 let mut qualified_map = serde_json::Map::new();
161 for (relative_key, db_column) in map_obj {
162 qualified_map.insert(format!("{}.{}", map_parent, relative_key), db_column);
163 }
164 load_obj.insert("map".to_string(), Value::Object(qualified_map));
165 }
166 }
167 }
168
169 self.load_file(&file).ok();
171 let re = Regex::new(r"\$\{([^}]+)\}").unwrap();
172 let parent_path = path.rfind('.').map_or(String::new(), |pos| path[..pos].to_string());
173
174 for (_meta_key, meta_value) in meta.iter_mut() {
175 self.qualify_value(meta_value, &re, &file, &parent_path);
176 }
177
178 meta
179 }
180
181 fn qualify_value(
183 &mut self,
184 value: &mut Value,
185 re: ®ex::Regex,
186 owner_file: &str,
187 parent_path: &str,
188 ) {
189 match value {
190 Value::String(s) => {
191 *s = re.replace_all(s, |caps: ®ex::Captures| {
192 let placeholder = &caps[1];
193
194 if let Some(owner_data) = self.cache.get(owner_file) {
196 if DotArrayAccessor::has(owner_data, placeholder) {
197 return caps[0].to_string();
198 }
199 }
200
201 let mut ph_parts = placeholder.splitn(2, '.');
203 let ph_file = ph_parts.next().unwrap_or("").to_string();
204 let ph_path = ph_parts.next().unwrap_or("").to_string();
205 self.load_file(&ph_file).ok();
206
207 if let Some(ph_data) = self.cache.get(&ph_file) {
208 if let Some(obj) = ph_data.as_object() {
209 if !obj.is_empty() && (ph_path.is_empty() || DotArrayAccessor::has(ph_data, &ph_path)) {
210 return caps[0].to_string();
211 }
212 }
213 }
214
215 if parent_path.is_empty() {
217 caps[0].to_string()
218 } else {
219 format!("${{{}.{}.{}}}", owner_file, parent_path, placeholder)
220 }
221 }).to_string();
222 }
223 Value::Object(obj) => {
224 for (_k, v) in obj.iter_mut() {
225 self.qualify_value(v, re, owner_file, parent_path);
226 }
227 }
228 Value::Array(arr) => {
229 for v in arr.iter_mut() {
230 self.qualify_value(v, re, owner_file, parent_path);
231 }
232 }
233 _ => {}
234 }
235 }
236
237 fn load_file(&mut self, file: &str) -> Result<(), String> {
241 if self.cache.contains_key(file) {
242 return Ok(());
243 }
244
245 let yml_path = self.manifest_dir.join(format!("{}.yml", file));
246 let yaml_path = self.manifest_dir.join(format!("{}.yaml", file));
247
248 let yml_exists = yml_path.exists();
249 let yaml_exists = yaml_path.exists();
250
251 if yml_exists && yaml_exists {
253 self.cache.insert(file.to_string(), Value::Object(serde_json::Map::new()));
254 return Err(format!(
255 "Ambiguous file: both '{}.yml' and '{}.yaml' exist. Please use only one extension.",
256 file, file
257 ));
258 }
259
260 let file_path = if yml_exists {
262 yml_path
263 } else if yaml_exists {
264 yaml_path
265 } else {
266 self.cache.insert(file.to_string(), Value::Object(serde_json::Map::new()));
267 return Err(format!("File not found: '{}.yml' or '{}.yaml'", file, file));
268 };
269
270 let content = fs::read_to_string(&file_path)
271 .map_err(|e| format!("Failed to read file: {}", e))?;
272
273 let yaml: serde_yaml_ng::Value = serde_yaml_ng::from_str(&content)
274 .map_err(|e| format!("Failed to parse YAML: {}", e))?;
275
276 let json_value = serde_json::to_value(&yaml)
277 .unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
278 self.cache.insert(file.to_string(), json_value);
279
280 Ok(())
281 }
282
283 fn remove_meta(&self, value: &Value) -> Value {
284 match value {
285 Value::Object(obj) => {
286 let mut filtered = serde_json::Map::new();
287 for (k, v) in obj {
288 if !k.starts_with('_') {
289 filtered.insert(k.clone(), self.remove_meta(v));
290 }
291 }
292 Value::Object(filtered)
293 }
294 Value::Array(arr) => {
295 Value::Array(arr.iter().map(|v| self.remove_meta(v)).collect())
296 }
297 _ => value.clone(),
298 }
299 }
300
301 pub fn get_missing_keys(&self) -> &[String] {
302 &self.missing_keys
303 }
304
305 pub fn clear_missing_keys(&mut self) {
306 self.missing_keys.clear();
307 }
308}
309
310impl provided::Manifest for Manifest {
312 fn get(&mut self, key: &str, default: Option<Value>) -> Value {
313 Manifest::get(self, key, default)
314 }
315
316 fn get_meta(&mut self, key: &str) -> HashMap<String, Value> {
317 Manifest::get_meta(self, key)
318 }
319
320 fn get_missing_keys(&self) -> &[String] {
321 Manifest::get_missing_keys(self)
322 }
323
324 fn clear_missing_keys(&mut self) {
325 Manifest::clear_missing_keys(self)
326 }
327}
328