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