robinpath_modules/modules/
yaml_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 rp.register_builtin("yaml.parse", |args, _| {
5 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6 Ok(parse_yaml(&s))
7 });
8
9 rp.register_builtin("yaml.stringify", |args, _| {
10 let val = args.first().cloned().unwrap_or(Value::Null);
11 let indent = args.get(1).map(|v| v.to_number() as usize).unwrap_or(2);
12 Ok(Value::String(stringify_yaml(&val, 0, indent)))
13 });
14
15 rp.register_builtin("yaml.parseFile", |args, _| {
16 let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
17 match std::fs::read_to_string(&path) {
18 Ok(content) => Ok(parse_yaml(&content)),
19 Err(e) => Err(format!("yaml.parseFile error: {}", e)),
20 }
21 });
22
23 rp.register_builtin("yaml.writeFile", |args, _| {
24 let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
25 let val = args.get(1).cloned().unwrap_or(Value::Null);
26 let indent = args.get(2).map(|v| v.to_number() as usize).unwrap_or(2);
27 let yaml_str = stringify_yaml(&val, 0, indent);
28 match std::fs::write(&path, &yaml_str) {
29 Ok(()) => Ok(Value::Bool(true)),
30 Err(e) => Err(format!("yaml.writeFile error: {}", e)),
31 }
32 });
33
34 rp.register_builtin("yaml.isValid", |args, _| {
35 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36 let parsed = parse_yaml(&s);
38 Ok(Value::Bool(!matches!(parsed, Value::Null) || s.trim() == "null" || s.trim() == "~"))
39 });
40
41 rp.register_builtin("yaml.get", |args, _| {
42 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
43 let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
44 let parsed = parse_yaml(&s);
45 Ok(get_by_path(&parsed, &path))
46 });
47
48 rp.register_builtin("yaml.toJSON", |args, _| {
49 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50 let parsed = parse_yaml(&s);
51 let json_val: serde_json::Value = parsed.into();
52 Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
53 });
54
55 rp.register_builtin("yaml.fromJSON", |args, _| {
56 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
57 match serde_json::from_str::<serde_json::Value>(&s) {
58 Ok(v) => {
59 let val = Value::from(v);
60 Ok(Value::String(stringify_yaml(&val, 0, 2)))
61 }
62 Err(e) => Err(format!("yaml.fromJSON error: {}", e)),
63 }
64 });
65
66 rp.register_builtin("yaml.parseAll", |args, _| {
67 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
68 let docs: Vec<Value> = s.split("\n---")
69 .map(|doc| parse_yaml(doc.trim_start_matches("---").trim()))
70 .collect();
71 Ok(Value::Array(docs))
72 });
73}
74
75fn parse_yaml(s: &str) -> Value {
76 let trimmed = s.trim();
77 if trimmed.is_empty() {
78 return Value::Null;
79 }
80 if (trimmed.starts_with('{') && trimmed.ends_with('}'))
82 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
83 {
84 if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
85 return Value::from(v);
86 }
87 }
88 parse_yaml_lines(&trimmed.lines().collect::<Vec<_>>(), 0).0
90}
91
92fn parse_yaml_lines(lines: &[&str], base_indent: usize) -> (Value, usize) {
93 let mut obj = indexmap::IndexMap::new();
94 let mut i = 0;
95 let mut is_array = false;
96 let mut arr = Vec::new();
97
98 while i < lines.len() {
99 let line = lines[i];
100 let stripped = line.trim();
101 if stripped.is_empty() || stripped.starts_with('#') {
102 i += 1;
103 continue;
104 }
105
106 let indent = line.len() - line.trim_start().len();
107 if indent < base_indent {
108 break;
109 }
110
111 if stripped.starts_with("- ") {
112 is_array = true;
113 let item_str = stripped[2..].trim();
114 if item_str.contains(": ") {
115 let mut item_obj = indexmap::IndexMap::new();
117 let (k, v) = split_kv(item_str);
118 item_obj.insert(k, parse_yaml_value(v));
119 let item_indent = indent + 2;
121 i += 1;
122 while i < lines.len() {
123 let next = lines[i];
124 let next_stripped = next.trim();
125 let next_indent = next.len() - next.trim_start().len();
126 if next_stripped.is_empty() || next_stripped.starts_with('#') {
127 i += 1;
128 continue;
129 }
130 if next_indent >= item_indent && !next_stripped.starts_with("- ") {
131 let (nk, nv) = split_kv(next_stripped);
132 item_obj.insert(nk, parse_yaml_value(nv));
133 i += 1;
134 } else {
135 break;
136 }
137 }
138 arr.push(Value::Object(item_obj));
139 } else {
140 arr.push(parse_yaml_value(item_str));
141 i += 1;
142 }
143 continue;
144 }
145
146 if stripped.contains(": ") || stripped.ends_with(':') {
147 let (key, value_str) = split_kv(stripped);
148 if value_str.is_empty() {
149 i += 1;
151 let child_indent = if i < lines.len() {
152 lines[i].len() - lines[i].trim_start().len()
153 } else {
154 indent + 2
155 };
156 let (child_val, consumed) = parse_yaml_lines(&lines[i..], child_indent);
157 obj.insert(key, child_val);
158 i += consumed;
159 } else {
160 obj.insert(key, parse_yaml_value(value_str));
161 i += 1;
162 }
163 } else {
164 i += 1;
165 }
166 }
167
168 if is_array {
169 (Value::Array(arr), i)
170 } else if obj.is_empty() {
171 (Value::Null, i)
172 } else {
173 (Value::Object(obj), i)
174 }
175}
176
177fn split_kv(s: &str) -> (String, &str) {
178 if let Some(pos) = s.find(": ") {
179 (s[..pos].trim().to_string(), s[pos + 2..].trim())
180 } else if s.ends_with(':') {
181 (s[..s.len() - 1].trim().to_string(), "")
182 } else {
183 (s.to_string(), "")
184 }
185}
186
187fn parse_yaml_value(s: &str) -> Value {
188 let trimmed = s.trim();
189 match trimmed {
190 "true" | "yes" | "on" => Value::Bool(true),
191 "false" | "no" | "off" => Value::Bool(false),
192 "null" | "~" | "" => Value::Null,
193 _ => {
194 if let Ok(n) = trimmed.parse::<f64>() {
196 Value::Number(n)
197 } else {
198 let unquoted = trimmed.trim_matches('"').trim_matches('\'');
200 Value::String(unquoted.to_string())
201 }
202 }
203 }
204}
205
206fn stringify_yaml(val: &Value, depth: usize, indent: usize) -> String {
207 let pad = " ".repeat(depth * indent);
208 match val {
209 Value::Object(obj) => {
210 let mut lines = Vec::new();
211 for (k, v) in obj {
212 match v {
213 Value::Object(_) => {
214 lines.push(format!("{}{}:", pad, k));
215 lines.push(stringify_yaml(v, depth + 1, indent));
216 }
217 Value::Array(arr) => {
218 lines.push(format!("{}{}:", pad, k));
219 let item_pad = " ".repeat((depth + 1) * indent);
220 for item in arr {
221 match item {
222 Value::Object(_) | Value::Array(_) => {
223 lines.push(format!("{}- ", item_pad));
224 lines.push(stringify_yaml(item, depth + 2, indent));
225 }
226 _ => lines.push(format!("{}- {}", item_pad, format_yaml_value(item))),
227 }
228 }
229 }
230 _ => lines.push(format!("{}{}: {}", pad, k, format_yaml_value(v))),
231 }
232 }
233 lines.join("\n")
234 }
235 Value::Array(arr) => {
236 let mut lines = Vec::new();
237 for item in arr {
238 lines.push(format!("{}- {}", pad, format_yaml_value(item)));
239 }
240 lines.join("\n")
241 }
242 _ => format!("{}{}", pad, format_yaml_value(val)),
243 }
244}
245
246fn format_yaml_value(val: &Value) -> String {
247 match val {
248 Value::String(s) => {
249 if s.contains(':') || s.contains('#') || s.contains('\n') || s.starts_with(' ') {
250 format!("\"{}\"", s.replace('"', "\\\""))
251 } else {
252 s.clone()
253 }
254 }
255 Value::Number(n) => {
256 if *n == (*n as i64) as f64 {
257 format!("{}", *n as i64)
258 } else {
259 format!("{}", n)
260 }
261 }
262 Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
263 Value::Null => "null".to_string(),
264 _ => val.to_display_string(),
265 }
266}
267
268fn get_by_path(val: &Value, path: &str) -> Value {
269 let parts: Vec<&str> = path.split('.').collect();
270 let mut current = val.clone();
271 for part in parts {
272 if let Value::Object(obj) = ¤t {
273 current = obj.get(part).cloned().unwrap_or(Value::Null);
274 } else {
275 return Value::Null;
276 }
277 }
278 current
279}