1use super::template::{self, Data};
9use serde_json::{Map, Value, json};
10
11fn eval_expr(expr: &str, data: &Data) -> Result<Value, String> {
14 let t = expr.trim();
15 if t.contains("{{") {
16 return template::render_str(t, data);
17 }
18 let cel = t.strip_prefix("CEL:").unwrap_or(t).trim();
19 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
20 crate::cel::eval_value(cel, &vars).map_err(|e| format!("CEL: {e}"))
21}
22
23fn with_item(data: &Data, alias: &str, item: &Value, index: usize) -> Data {
24 let mut d = data.clone();
25 d.insert(alias.to_string(), item.clone());
26 d.insert("item".to_string(), item.clone());
27 d.insert("index".to_string(), json!(index));
28 d
29}
30
31fn as_array(over: &Value, what: &str) -> Result<Vec<Value>, String> {
32 match over {
33 Value::Array(a) => Ok(a.clone()),
34 Value::Object(o) => Ok(o
35 .iter()
36 .map(|(k, v)| json!({"key": k, "value": v}))
37 .collect()),
38 Value::Null => Ok(Vec::new()),
39 other => Err(format!(
40 "{what}: `over` must be an array (got {})",
41 type_name(other)
42 )),
43 }
44}
45
46fn type_name(v: &Value) -> &'static str {
47 match v {
48 Value::Null => "null",
49 Value::Bool(_) => "boolean",
50 Value::Number(_) => "number",
51 Value::String(_) => "string",
52 Value::Array(_) => "array",
53 Value::Object(_) => "object",
54 }
55}
56
57pub fn map(over: &Value, expr: &str, alias: &str, data: &Data) -> Result<Value, String> {
59 let items = as_array(over, "map")?;
60 let mut out = Vec::with_capacity(items.len());
61 for (i, it) in items.iter().enumerate() {
62 out.push(eval_expr(expr, &with_item(data, alias, it, i))?);
63 }
64 Ok(Value::Array(out))
65}
66
67pub fn filter(over: &Value, expr: &str, alias: &str, data: &Data) -> Result<Value, String> {
69 let items = as_array(over, "filter")?;
70 let mut out = Vec::new();
71 for (i, it) in items.iter().enumerate() {
72 let v = eval_expr(expr, &with_item(data, alias, it, i))?;
73 match v {
74 Value::Bool(true) => out.push(it.clone()),
75 Value::Bool(false) => {}
76 other => {
77 return Err(format!(
78 "filter: expr must yield a boolean (got {})",
79 type_name(&other)
80 ));
81 }
82 }
83 }
84 Ok(Value::Array(out))
85}
86
87pub fn reduce(
89 over: &Value,
90 expr: &str,
91 initial: Value,
92 alias: &str,
93 acc_alias: &str,
94 data: &Data,
95) -> Result<Value, String> {
96 let items = as_array(over, "reduce")?;
97 let mut acc = initial;
98 for (i, it) in items.iter().enumerate() {
99 let mut d = with_item(data, alias, it, i);
100 d.insert(acc_alias.to_string(), acc.clone());
101 d.insert("acc".to_string(), acc.clone());
102 acc = eval_expr(expr, &d)?;
103 }
104 Ok(acc)
105}
106
107pub fn sort(over: &Value, by: Option<&str>, order: Option<&str>) -> Result<Value, String> {
109 let mut items = as_array(over, "sort")?;
110 let desc = matches!(order, Some("desc") | Some("descending"));
111 let key = |v: &Value| -> Value {
112 match by {
113 None | Some("") => v.clone(),
114 Some(p) => path_of(v, p).unwrap_or(Value::Null),
115 }
116 };
117 items.sort_by(|a, b| {
118 let o = cmp_values(&key(a), &key(b));
119 if desc { o.reverse() } else { o }
120 });
121 Ok(Value::Array(items))
122}
123
124pub fn dedupe(over: &Value, by: Option<&str>) -> Result<Value, String> {
126 let items = as_array(over, "dedupe")?;
127 let mut seen: Vec<Value> = Vec::new();
128 let mut out = Vec::new();
129 for it in items {
130 let k = match by {
131 None | Some("") => it.clone(),
132 Some(p) => path_of(&it, p).unwrap_or(Value::Null),
133 };
134 if !seen.contains(&k) {
135 seen.push(k);
136 out.push(it);
137 }
138 }
139 Ok(Value::Array(out))
140}
141
142pub fn chunk(
145 value: &Value,
146 by: Option<&str>,
147 size: usize,
148 overlap: usize,
149) -> Result<Value, String> {
150 if size == 0 {
151 return Err("chunk: size must be > 0".into());
152 }
153 let overlap = overlap.min(size.saturating_sub(1));
154 match value {
155 Value::Array(a) => {
156 let mut out = Vec::new();
157 let mut start = 0;
158 while start < a.len() {
159 let end = (start + size).min(a.len());
160 out.push(Value::Array(a[start..end].to_vec()));
161 if end == a.len() {
162 break;
163 }
164 start = end - overlap;
165 }
166 Ok(Value::Array(out))
167 }
168 Value::String(s) => {
169 let mode = by.unwrap_or("chars");
170 let out: Vec<Value> = match mode {
171 "lines" => {
172 let lines: Vec<&str> = s.lines().collect();
173 windows(&lines, size, overlap)
174 .into_iter()
175 .map(|w| Value::String(w.join("\n")))
176 .collect()
177 }
178 "words" | "tokens" => {
179 let words: Vec<&str> = s.split_whitespace().collect();
181 let per = if mode == "tokens" {
182 ((size as f64) / 1.3).max(1.0) as usize
183 } else {
184 size
185 };
186 let ov = if mode == "tokens" {
187 ((overlap as f64) / 1.3) as usize
188 } else {
189 overlap
190 };
191 windows(&words, per, ov.min(per.saturating_sub(1)))
192 .into_iter()
193 .map(|w| Value::String(w.join(" ")))
194 .collect()
195 }
196 "chars" => {
197 let chars: Vec<char> = s.chars().collect();
198 windows(&chars, size, overlap)
199 .into_iter()
200 .map(|w| Value::String(w.into_iter().collect()))
201 .collect()
202 }
203 other => {
204 return Err(format!(
205 "chunk: by must be chars|lines|words|tokens (got {other:?})"
206 ));
207 }
208 };
209 Ok(Value::Array(out))
210 }
211 other => Err(format!(
212 "chunk: value must be a string or an array (got {})",
213 type_name(other)
214 )),
215 }
216}
217
218fn windows<T: Clone>(items: &[T], size: usize, overlap: usize) -> Vec<Vec<T>> {
219 let mut out = Vec::new();
220 let mut start = 0;
221 while start < items.len() {
222 let end = (start + size).min(items.len());
223 out.push(items[start..end].to_vec());
224 if end == items.len() {
225 break;
226 }
227 start = end - overlap;
228 }
229 out
230}
231
232pub fn parse(text: &str, format: Option<&str>) -> Result<Value, String> {
234 let f = format.unwrap_or("auto");
235 match f {
236 "json" => serde_json::from_str::<Value>(text).map_err(|e| format!("parse json: {e}")),
237 "yaml" => crate::config::yaml::parse(text).map_err(|e| format!("parse yaml: {e}")),
238 "lines" => Ok(Value::Array(
239 text.lines().map(|l| Value::String(l.to_string())).collect(),
240 )),
241 "csv" => Ok(parse_csv(text)),
242 "auto" => {
243 let t = text.trim();
244 if let Ok(v) = serde_json::from_str::<Value>(t) {
245 return Ok(v);
246 }
247 if let Ok(v) = crate::config::yaml::parse(t)
248 && !matches!(v, Value::String(_))
249 {
250 return Ok(v);
251 }
252 Ok(Value::Array(
253 text.lines().map(|l| Value::String(l.to_string())).collect(),
254 ))
255 }
256 other => Err(format!(
257 "parse: format must be json|yaml|csv|lines|auto (got {other:?})"
258 )),
259 }
260}
261
262fn parse_csv(text: &str) -> Value {
264 let rows: Vec<Vec<String>> = text
265 .lines()
266 .filter(|l| !l.trim().is_empty())
267 .map(csv_row)
268 .collect();
269 let Some((header, body)) = rows.split_first() else {
270 return json!([]);
271 };
272 Value::Array(
273 body.iter()
274 .map(|r| {
275 let mut o = Map::new();
276 for (i, h) in header.iter().enumerate() {
277 o.insert(
278 h.clone(),
279 r.get(i).map(|c| coerce_scalar(c)).unwrap_or(Value::Null),
280 );
281 }
282 Value::Object(o)
283 })
284 .collect(),
285 )
286}
287
288fn csv_row(line: &str) -> Vec<String> {
289 let mut out = Vec::new();
290 let mut cur = String::new();
291 let mut quoted = false;
292 let mut chars = line.chars().peekable();
293 while let Some(c) = chars.next() {
294 match c {
295 '"' if quoted && chars.peek() == Some(&'"') => {
296 cur.push('"');
297 chars.next();
298 }
299 '"' => quoted = !quoted,
300 ',' if !quoted => {
301 out.push(std::mem::take(&mut cur));
302 }
303 other => cur.push(other),
304 }
305 }
306 out.push(cur);
307 out
308}
309
310fn coerce_scalar(s: &str) -> Value {
311 if let Ok(i) = s.parse::<i64>() {
312 return json!(i);
313 }
314 if let Ok(f) = s.parse::<f64>() {
315 return json!(f);
316 }
317 match s {
318 "true" => json!(true),
319 "false" => json!(false),
320 _ => Value::String(s.to_string()),
321 }
322}
323
324pub fn path_of(v: &Value, path: &str) -> Option<Value> {
326 let mut cur = v;
327 for seg in path.split('.') {
328 cur = match cur {
329 Value::Object(m) => m.get(seg)?,
330 Value::Array(a) => a.get(seg.parse::<usize>().ok()?)?,
331 _ => return None,
332 };
333 }
334 Some(cur.clone())
335}
336
337pub fn cmp_values(a: &Value, b: &Value) -> std::cmp::Ordering {
339 use std::cmp::Ordering::*;
340 let rank = |v: &Value| match v {
341 Value::Null => 0,
342 Value::Bool(_) => 1,
343 Value::Number(_) => 2,
344 Value::String(_) => 3,
345 Value::Array(_) => 4,
346 Value::Object(_) => 5,
347 };
348 match (a, b) {
349 (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
350 (Value::Number(x), Value::Number(y)) => {
351 x.as_f64().partial_cmp(&y.as_f64()).unwrap_or(Equal)
352 }
353 (Value::String(x), Value::String(y)) => x.cmp(y),
354 (Value::Array(x), Value::Array(y)) => {
355 for (p, q) in x.iter().zip(y.iter()) {
356 let o = cmp_values(p, q);
357 if o != Equal {
358 return o;
359 }
360 }
361 x.len().cmp(&y.len())
362 }
363 _ => rank(a).cmp(&rank(b)),
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[cfg(feature = "cel")]
372 fn data() -> Data {
373 let mut d = Data::new();
374 d.insert("vars".into(), json!({"min": 2}));
375 d
376 }
377
378 #[cfg(feature = "cel")]
379 #[test]
380 fn map_filter_reduce_with_cel_and_templates() {
381 let d = data();
382 assert_eq!(
383 map(&json!([1, 2, 3]), "item * 2", "item", &d).unwrap(),
384 json!([2, 4, 6])
385 );
386 assert_eq!(
387 map(
388 &json!([{"n": 1}, {"n": 5}]),
389 "{{item.n}}-{{index}}",
390 "item",
391 &d
392 )
393 .unwrap(),
394 json!(["1-0", "5-1"])
395 );
396 assert_eq!(
397 filter(&json!([1, 2, 3, 4]), "CEL: x > vars.min", "x", &d).unwrap(),
398 json!([3, 4])
399 );
400 assert!(
401 filter(&json!([1]), "item", "item", &d).is_err(),
402 "non-boolean"
403 );
404 assert_eq!(
405 reduce(&json!([1, 2, 3]), "acc + item", json!(0), "item", "acc", &d).unwrap(),
406 json!(6)
407 );
408 assert_eq!(
409 reduce(
410 &json!(["a", "b"]),
411 "total + \"|\" + s",
412 json!(""),
413 "s",
414 "total",
415 &d
416 )
417 .unwrap(),
418 json!("|a|b")
419 );
420 assert_eq!(
422 map(
423 &json!({"a": 1, "b": 2}),
424 "item.key + \"=\" + string(item.value)",
425 "item",
426 &d
427 )
428 .unwrap(),
429 json!(["a=1", "b=2"])
430 );
431 }
432
433 #[test]
434 fn sort_dedupe_chunk_parse() {
435 assert_eq!(
436 sort(&json!([3, 1, 2]), None, None).unwrap(),
437 json!([1, 2, 3])
438 );
439 assert_eq!(
440 sort(
441 &json!([{"n": 3, "s": "c"}, {"n": 1, "s": "a"}]),
442 Some("n"),
443 Some("desc")
444 )
445 .unwrap(),
446 json!([{"n": 3, "s": "c"}, {"n": 1, "s": "a"}])
447 );
448 assert_eq!(
449 sort(&json!(["b", null, 2, "a", true]), None, None).unwrap(),
450 json!([null, true, 2, "a", "b"])
451 );
452 assert_eq!(
453 dedupe(&json!([1, 2, 1, 3, 2]), None).unwrap(),
454 json!([1, 2, 3])
455 );
456 assert_eq!(
457 dedupe(
458 &json!([{"id": 1, "x": "a"}, {"id": 1, "x": "b"}, {"id": 2}]),
459 Some("id")
460 )
461 .unwrap(),
462 json!([{"id": 1, "x": "a"}, {"id": 2}])
463 );
464 assert_eq!(
465 chunk(&json!([1, 2, 3, 4, 5]), None, 2, 0).unwrap(),
466 json!([[1, 2], [3, 4], [5]])
467 );
468 assert_eq!(
469 chunk(&json!([1, 2, 3, 4, 5]), None, 3, 1).unwrap(),
470 json!([[1, 2, 3], [3, 4, 5]])
471 );
472 assert_eq!(
473 chunk(&json!("abcdefg"), Some("chars"), 3, 0).unwrap(),
474 json!(["abc", "def", "g"])
475 );
476 assert_eq!(
477 chunk(&json!("l1\nl2\nl3"), Some("lines"), 2, 0).unwrap(),
478 json!(["l1\nl2", "l3"])
479 );
480 assert_eq!(
481 chunk(&json!("a b c d"), Some("words"), 2, 0).unwrap(),
482 json!(["a b", "c d"])
483 );
484 assert!(chunk(&json!("x"), None, 0, 0).is_err());
485 assert_eq!(parse("{\"a\": 1}", None).unwrap(), json!({"a": 1}));
486 assert_eq!(
487 parse("a: 1\nb: [x, y]", Some("yaml")).unwrap(),
488 json!({"a": 1, "b": ["x", "y"]})
489 );
490 assert_eq!(parse("x\ny", Some("lines")).unwrap(), json!(["x", "y"]));
491 assert_eq!(
492 parse("name,age\n\"Doe, J\",42\nAnn,7", Some("csv")).unwrap(),
493 json!([{"name": "Doe, J", "age": 42}, {"name": "Ann", "age": 7}])
494 );
495 assert_eq!(parse("plain text", None).unwrap(), json!(["plain text"]));
496 assert!(parse("x", Some("xml")).is_err());
497 assert_eq!(
498 path_of(&json!({"a": {"b": [10, 20]}}), "a.b.1"),
499 Some(json!(20))
500 );
501 }
502}