1use serde_json::Value;
23use smallvec::SmallVec;
24use std::fmt;
25
26#[derive(Debug, Clone, PartialEq)]
28pub enum TransformOp {
29 Upper,
31 Lower,
32 Trim,
33 TrimStart,
34 TrimEnd,
35
36 Length,
38 First,
39 Last,
40 FirstN(usize),
41 LastN(usize),
42 Keys,
43 Values,
44 Flatten,
45 Reverse,
46 Sort,
47 Unique,
48 Compact, ToString,
52 ToNumber,
53 ToBool,
54 ToJson,
55 ParseJson,
56
57 Round(Option<u32>),
59 Abs,
60 Ceil,
61 Floor,
62
63 Default(Value),
65 TypeOf,
66 Join(String),
67 Split(String),
68 Shell,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct TransformExpr {
74 pub ops: SmallVec<[TransformOp; 2]>,
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct TransformParseError {
80 pub input: String,
81 pub reason: String,
82}
83
84impl fmt::Display for TransformParseError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(
87 f,
88 "[NIKA-151] Transform parse error in '{}': {}",
89 self.input, self.reason
90 )
91 }
92}
93
94impl std::error::Error for TransformParseError {}
95
96#[derive(Debug, Clone, PartialEq)]
98pub enum TransformError {
99 TypeMismatch {
101 op: &'static str,
102 expected: &'static str,
103 got: String,
104 },
105 NullInput { op: &'static str },
107}
108
109impl fmt::Display for TransformError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 TransformError::TypeMismatch { op, expected, got } => {
113 write!(
114 f,
115 "[NIKA-152] Transform '{}' failed: expected {}, got {}",
116 op, expected, got
117 )
118 }
119 TransformError::NullInput { op } => {
120 write!(
121 f,
122 "[NIKA-153] Transform '{}' received null — use default() to handle",
123 op
124 )
125 }
126 }
127 }
128}
129
130impl std::error::Error for TransformError {}
131
132impl TransformExpr {
137 pub fn parse(input: &str) -> Result<Self, TransformParseError> {
141 let trimmed = input.trim();
142 if trimmed.is_empty() {
143 return Ok(TransformExpr {
144 ops: SmallVec::new(),
145 });
146 }
147
148 let ops: SmallVec<[TransformOp; 2]> = trimmed
149 .split('|')
150 .map(str::trim)
151 .filter(|s| !s.is_empty())
152 .map(|s| parse_single_op(s, input))
153 .collect::<Result<_, _>>()?;
154
155 Ok(TransformExpr { ops })
156 }
157
158 pub fn apply(&self, value: &Value) -> Result<Value, TransformError> {
160 let mut current = value.clone();
161 for op in &self.ops {
162 current = op.apply(¤t)?;
163 }
164 Ok(current)
165 }
166
167 pub fn is_empty(&self) -> bool {
169 self.ops.is_empty()
170 }
171}
172
173impl TransformOp {
178 pub fn apply(&self, value: &Value) -> Result<Value, TransformError> {
180 match self {
181 TransformOp::Upper => match value {
183 Value::Null => Err(TransformError::NullInput { op: "upper" }),
184 Value::String(s) => Ok(Value::String(s.to_uppercase())),
185 _ => Err(type_mismatch("upper", "string", value)),
186 },
187 TransformOp::Lower => match value {
188 Value::Null => Err(TransformError::NullInput { op: "lower" }),
189 Value::String(s) => Ok(Value::String(s.to_lowercase())),
190 _ => Err(type_mismatch("lower", "string", value)),
191 },
192 TransformOp::Trim => match value {
193 Value::Null => Err(TransformError::NullInput { op: "trim" }),
194 Value::String(s) => Ok(Value::String(s.trim().to_string())),
195 _ => Err(type_mismatch("trim", "string", value)),
196 },
197 TransformOp::TrimStart => match value {
198 Value::Null => Err(TransformError::NullInput { op: "trim_start" }),
199 Value::String(s) => Ok(Value::String(s.trim_start().to_string())),
200 _ => Err(type_mismatch("trim_start", "string", value)),
201 },
202 TransformOp::TrimEnd => match value {
203 Value::Null => Err(TransformError::NullInput { op: "trim_end" }),
204 Value::String(s) => Ok(Value::String(s.trim_end().to_string())),
205 _ => Err(type_mismatch("trim_end", "string", value)),
206 },
207
208 TransformOp::Length => match value {
210 Value::Null => Ok(Value::Null), Value::Array(arr) => Ok(Value::Number(arr.len().into())),
212 Value::String(s) => Ok(Value::Number(s.chars().count().into())),
213 Value::Object(obj) => Ok(Value::Number(obj.len().into())),
214 _ => Err(type_mismatch("length", "array, string, or object", value)),
215 },
216 TransformOp::First => match value {
217 Value::Null => Err(TransformError::NullInput { op: "first" }),
218 Value::Array(arr) => Ok(arr.first().cloned().unwrap_or(Value::Null)),
219 _ => Err(type_mismatch("first", "array", value)),
220 },
221 TransformOp::Last => match value {
222 Value::Null => Err(TransformError::NullInput { op: "last" }),
223 Value::Array(arr) => Ok(arr.last().cloned().unwrap_or(Value::Null)),
224 _ => Err(type_mismatch("last", "array", value)),
225 },
226 TransformOp::FirstN(n) => match value {
227 Value::Null => Err(TransformError::NullInput { op: "first" }),
228 Value::Array(arr) => {
229 let taken: Vec<Value> = arr.iter().take(*n).cloned().collect();
230 Ok(Value::Array(taken))
231 }
232 Value::String(s) => {
233 let truncated: String = s.chars().take(*n).collect();
235 Ok(Value::String(truncated))
236 }
237 Value::Object(_) => {
238 let json = serde_json::to_string(value).unwrap_or_default();
240 let truncated: String = json.chars().take(*n).collect();
241 Ok(Value::String(truncated))
242 }
243 _ => Err(type_mismatch("first", "array, string, or object", value)),
244 },
245 TransformOp::LastN(n) => match value {
246 Value::Null => Err(TransformError::NullInput { op: "last" }),
247 Value::Array(arr) => {
248 let skip = arr.len().saturating_sub(*n);
249 let taken: Vec<Value> = arr.iter().skip(skip).cloned().collect();
250 Ok(Value::Array(taken))
251 }
252 _ => Err(type_mismatch("last", "array", value)),
253 },
254 TransformOp::Keys => match value {
255 Value::Null => Ok(Value::Null), Value::Object(obj) => {
257 let keys: Vec<Value> = obj.keys().map(|k| Value::String(k.clone())).collect();
258 Ok(Value::Array(keys))
259 }
260 _ => Err(type_mismatch("keys", "object", value)),
261 },
262 TransformOp::Values => match value {
263 Value::Null => Err(TransformError::NullInput { op: "values" }),
264 Value::Object(obj) => {
265 let vals: Vec<Value> = obj.values().cloned().collect();
266 Ok(Value::Array(vals))
267 }
268 _ => Err(type_mismatch("values", "object", value)),
269 },
270 TransformOp::Flatten => match value {
271 Value::Null => Err(TransformError::NullInput { op: "flatten" }),
272 Value::Array(arr) => {
273 let mut flat = Vec::new();
274 for item in arr {
275 match item {
276 Value::Array(inner) => flat.extend(inner.iter().cloned()),
277 other => flat.push(other.clone()),
278 }
279 }
280 Ok(Value::Array(flat))
281 }
282 _ => Err(type_mismatch("flatten", "array", value)),
283 },
284 TransformOp::Reverse => match value {
285 Value::Null => Err(TransformError::NullInput { op: "reverse" }),
286 Value::Array(arr) => {
287 let mut rev = arr.clone();
288 rev.reverse();
289 Ok(Value::Array(rev))
290 }
291 _ => Err(type_mismatch("reverse", "array", value)),
292 },
293 TransformOp::Sort => match value {
294 Value::Null => Err(TransformError::NullInput { op: "sort" }),
295 Value::Array(arr) => {
296 let mut sorted = arr.clone();
297 sorted.sort_by(|a, b| match (a.as_f64(), b.as_f64()) {
298 (Some(x), Some(y)) => {
299 x.partial_cmp(&y).unwrap_or(std::cmp::Ordering::Equal)
300 }
301 (Some(_), None) => std::cmp::Ordering::Less,
302 (None, Some(_)) => std::cmp::Ordering::Greater,
303 _ => a.to_string().cmp(&b.to_string()),
304 });
305 Ok(Value::Array(sorted))
306 }
307 _ => Err(type_mismatch("sort", "array", value)),
308 },
309 TransformOp::Unique => match value {
310 Value::Null => Err(TransformError::NullInput { op: "unique" }),
311 Value::Array(arr) => {
312 let mut seen = Vec::new();
313 let mut unique = Vec::new();
314 for item in arr {
315 let s = item.to_string();
316 if !seen.contains(&s) {
317 seen.push(s);
318 unique.push(item.clone());
319 }
320 }
321 Ok(Value::Array(unique))
322 }
323 _ => Err(type_mismatch("unique", "array", value)),
324 },
325 TransformOp::Compact => match value {
326 Value::Null => Err(TransformError::NullInput { op: "compact" }),
327 Value::Array(arr) => {
328 let compacted: Vec<Value> =
329 arr.iter().filter(|v| !v.is_null()).cloned().collect();
330 Ok(Value::Array(compacted))
331 }
332 _ => Err(type_mismatch("compact", "array", value)),
333 },
334
335 TransformOp::ToString => match value {
337 Value::Null => Ok(Value::Null), Value::String(_) => Ok(value.clone()),
339 Value::Number(n) => Ok(Value::String(n.to_string())),
340 Value::Bool(b) => Ok(Value::String(b.to_string())),
341 _ => Ok(Value::String(value.to_string())),
342 },
343 TransformOp::ToNumber => match value {
344 Value::Null => Err(TransformError::NullInput { op: "to_number" }),
345 Value::Number(_) => Ok(value.clone()),
346 Value::String(s) => {
347 if let Ok(n) = s.parse::<i64>() {
348 Ok(Value::Number(n.into()))
349 } else if let Ok(f) = s.parse::<f64>() {
350 Ok(serde_json::Number::from_f64(f)
351 .map(Value::Number)
352 .unwrap_or(Value::Null))
353 } else {
354 Err(TransformError::TypeMismatch {
355 op: "to_number",
356 expected: "numeric string",
357 got: format!("\"{}\"", s),
358 })
359 }
360 }
361 Value::Bool(b) => Ok(Value::Number(if *b { 1 } else { 0 }.into())),
362 _ => Err(type_mismatch("to_number", "string, number, or bool", value)),
363 },
364 TransformOp::ToBool => match value {
365 Value::Null => Err(TransformError::NullInput { op: "to_bool" }),
366 Value::Bool(_) => Ok(value.clone()),
367 Value::Number(n) => Ok(Value::Bool(n.as_f64().map(|f| f != 0.0).unwrap_or(false))),
368 Value::String(s) => match s.as_str() {
369 "true" | "1" | "yes" => Ok(Value::Bool(true)),
370 "false" | "0" | "no" | "" => Ok(Value::Bool(false)),
371 _ => Err(TransformError::TypeMismatch {
372 op: "to_bool",
373 expected: "truthy/falsy value",
374 got: format!("\"{}\"", s),
375 }),
376 },
377 _ => Err(type_mismatch("to_bool", "string, number, or bool", value)),
378 },
379 TransformOp::ToJson => match value {
380 Value::Null => Ok(Value::Null), _ => Ok(Value::String(
382 serde_json::to_string(value).unwrap_or_default(),
383 )),
384 },
385 TransformOp::ParseJson => match value {
386 Value::Null => Err(TransformError::NullInput { op: "parse_json" }),
387 Value::String(s) => {
388 let cleaned = strip_markdown_code_block(s);
390 serde_json::from_str(&cleaned).map_err(|_| TransformError::TypeMismatch {
391 op: "parse_json",
392 expected: "valid JSON string",
393 got: format!("\"{}\"", truncate(s, 50)),
394 })
395 }
396 Value::Array(_) | Value::Object(_) | Value::Number(_) | Value::Bool(_) => {
400 Ok(value.clone())
401 }
402 },
403
404 TransformOp::Round(decimals) => match value {
406 Value::Null => Err(TransformError::NullInput { op: "round" }),
407 Value::Number(n) => {
408 let f = n.as_f64().unwrap_or(0.0);
409 let d = decimals.unwrap_or(0);
410 let factor = 10f64.powi(d as i32);
411 let rounded = (f * factor).round() / factor;
412 Ok(serde_json::Number::from_f64(rounded)
413 .map(Value::Number)
414 .unwrap_or(Value::Null))
415 }
416 _ => Err(type_mismatch("round", "number", value)),
417 },
418 TransformOp::Abs => match value {
419 Value::Null => Err(TransformError::NullInput { op: "abs" }),
420 Value::Number(n) => {
421 if let Some(i) = n.as_i64() {
422 Ok(Value::Number(i.unsigned_abs().into()))
423 } else if let Some(f) = n.as_f64() {
424 Ok(serde_json::Number::from_f64(f.abs())
425 .map(Value::Number)
426 .unwrap_or(Value::Null))
427 } else {
428 Ok(value.clone())
429 }
430 }
431 _ => Err(type_mismatch("abs", "number", value)),
432 },
433 TransformOp::Ceil => match value {
434 Value::Null => Err(TransformError::NullInput { op: "ceil" }),
435 Value::Number(n) => {
436 let f = n.as_f64().unwrap_or(0.0);
437 Ok(Value::Number((f.ceil() as i64).into()))
438 }
439 _ => Err(type_mismatch("ceil", "number", value)),
440 },
441 TransformOp::Floor => match value {
442 Value::Null => Err(TransformError::NullInput { op: "floor" }),
443 Value::Number(n) => {
444 let f = n.as_f64().unwrap_or(0.0);
445 Ok(Value::Number((f.floor() as i64).into()))
446 }
447 _ => Err(type_mismatch("floor", "number", value)),
448 },
449
450 TransformOp::Default(default_val) => {
452 if value.is_null() {
453 Ok(default_val.clone())
454 } else {
455 Ok(value.clone())
456 }
457 }
458 TransformOp::TypeOf => {
459 let name = value_type_name(value);
460 Ok(Value::String(name.to_string()))
461 }
462 TransformOp::Join(sep) => match value {
463 Value::Null => Err(TransformError::NullInput { op: "join" }),
464 Value::Array(arr) => {
465 let strings: Vec<String> = arr
466 .iter()
467 .map(|v| match v {
468 Value::String(s) => s.clone(),
469 other => other.to_string(),
470 })
471 .collect();
472 Ok(Value::String(strings.join(sep)))
473 }
474 _ => Err(type_mismatch("join", "array", value)),
475 },
476 TransformOp::Split(sep) => match value {
477 Value::Null => Err(TransformError::NullInput { op: "split" }),
478 Value::String(s) => {
479 let parts: Vec<Value> = s
480 .split(sep.as_str())
481 .map(|p| Value::String(p.to_string()))
482 .collect();
483 Ok(Value::Array(parts))
484 }
485 _ => Err(type_mismatch("split", "string", value)),
486 },
487 TransformOp::Shell => {
488 match value {
490 Value::String(s) => Ok(Value::String(shell_escape(s))),
491 _ => Ok(Value::String(shell_escape(&value.to_string()))),
492 }
493 }
494 }
495 }
496}
497
498fn parse_single_op(input: &str, full_input: &str) -> Result<TransformOp, TransformParseError> {
506 let trimmed = input.trim();
507
508 if let Some(paren_pos) = trimmed.find('(') {
510 let name = trimmed[..paren_pos].trim();
511 let rest = &trimmed[paren_pos + 1..];
512 let arg = rest
513 .strip_suffix(')')
514 .ok_or_else(|| TransformParseError {
515 input: full_input.to_string(),
516 reason: format!("unclosed parenthesis in '{}'", trimmed),
517 })?
518 .trim();
519
520 match name {
521 "first" => {
522 let n: usize = arg.parse().map_err(|_| TransformParseError {
523 input: full_input.to_string(),
524 reason: format!("invalid argument for first(): '{}'", arg),
525 })?;
526 Ok(TransformOp::FirstN(n))
527 }
528 "last" => {
529 let n: usize = arg.parse().map_err(|_| TransformParseError {
530 input: full_input.to_string(),
531 reason: format!("invalid argument for last(): '{}'", arg),
532 })?;
533 Ok(TransformOp::LastN(n))
534 }
535 "round" => {
536 let d: u32 = arg.parse().map_err(|_| TransformParseError {
537 input: full_input.to_string(),
538 reason: format!("invalid argument for round(): '{}'", arg),
539 })?;
540 Ok(TransformOp::Round(Some(d)))
541 }
542 "join" => {
543 let sep = strip_quotes(arg);
544 Ok(TransformOp::Join(sep.to_string()))
545 }
546 "split" => {
547 let sep = strip_quotes(arg);
548 Ok(TransformOp::Split(sep.to_string()))
549 }
550 "default" => {
551 let val = parse_default_value(arg).map_err(|reason| TransformParseError {
552 input: full_input.to_string(),
553 reason,
554 })?;
555 Ok(TransformOp::Default(val))
556 }
557 _ => Err(TransformParseError {
558 input: full_input.to_string(),
559 reason: format!("unknown transform: '{}'", name),
560 }),
561 }
562 } else {
563 match trimmed {
565 "upper" => Ok(TransformOp::Upper),
566 "lower" => Ok(TransformOp::Lower),
567 "trim" => Ok(TransformOp::Trim),
568 "trim_start" => Ok(TransformOp::TrimStart),
569 "trim_end" => Ok(TransformOp::TrimEnd),
570 "length" => Ok(TransformOp::Length),
571 "first" => Ok(TransformOp::First),
572 "last" => Ok(TransformOp::Last),
573 "keys" => Ok(TransformOp::Keys),
574 "values" => Ok(TransformOp::Values),
575 "flatten" => Ok(TransformOp::Flatten),
576 "reverse" => Ok(TransformOp::Reverse),
577 "sort" => Ok(TransformOp::Sort),
578 "unique" => Ok(TransformOp::Unique),
579 "compact" => Ok(TransformOp::Compact),
580 "to_string" => Ok(TransformOp::ToString),
581 "to_number" => Ok(TransformOp::ToNumber),
582 "to_bool" => Ok(TransformOp::ToBool),
583 "to_json" => Ok(TransformOp::ToJson),
584 "parse_json" => Ok(TransformOp::ParseJson),
585 "round" => Ok(TransformOp::Round(None)),
586 "abs" => Ok(TransformOp::Abs),
587 "ceil" => Ok(TransformOp::Ceil),
588 "floor" => Ok(TransformOp::Floor),
589 "type_of" => Ok(TransformOp::TypeOf),
590 "shell" => Ok(TransformOp::Shell),
591 _ => Err(TransformParseError {
592 input: full_input.to_string(),
593 reason: format!("unknown transform: '{}'", trimmed),
594 }),
595 }
596 }
597}
598
599fn value_type_name(value: &Value) -> &'static str {
605 match value {
606 Value::Null => "null",
607 Value::Bool(_) => "boolean",
608 Value::Number(_) => "number",
609 Value::String(_) => "string",
610 Value::Array(_) => "array",
611 Value::Object(_) => "object",
612 }
613}
614
615fn type_mismatch(op: &'static str, expected: &'static str, got: &Value) -> TransformError {
617 TransformError::TypeMismatch {
618 op,
619 expected,
620 got: value_type_name(got).to_string(),
621 }
622}
623
624fn strip_quotes(s: &str) -> &str {
626 let trimmed = s.trim();
627 if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
628 || (trimmed.starts_with('"') && trimmed.ends_with('"'))
629 {
630 &trimmed[1..trimmed.len() - 1]
631 } else {
632 trimmed
633 }
634}
635
636fn parse_default_value(arg: &str) -> Result<Value, String> {
638 let trimmed = arg.trim();
639
640 if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
642 || (trimmed.starts_with('"') && trimmed.ends_with('"'))
643 {
644 return Ok(Value::String(trimmed[1..trimmed.len() - 1].to_string()));
645 }
646
647 if trimmed == "null" {
649 return Ok(Value::Null);
650 }
651
652 if trimmed == "true" {
654 return Ok(Value::Bool(true));
655 }
656 if trimmed == "false" {
657 return Ok(Value::Bool(false));
658 }
659
660 if let Ok(n) = trimmed.parse::<i64>() {
662 return Ok(Value::Number(n.into()));
663 }
664
665 if let Ok(f) = trimmed.parse::<f64>() {
667 if let Some(n) = serde_json::Number::from_f64(f) {
668 return Ok(Value::Number(n));
669 }
670 }
671
672 if (trimmed.starts_with('{') && trimmed.ends_with('}'))
674 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
675 {
676 return serde_json::from_str(trimmed).map_err(|e| format!("invalid JSON default: {}", e));
677 }
678
679 Ok(Value::String(trimmed.to_string()))
681}
682
683fn truncate(s: &str, max: usize) -> String {
685 if s.len() <= max {
686 s.to_string()
687 } else {
688 let mut end = max;
690 while end > 0 && !s.is_char_boundary(end) {
691 end -= 1;
692 }
693 format!("{}...", &s[..end])
694 }
695}
696
697fn strip_markdown_code_block(s: &str) -> String {
700 let trimmed = s.trim();
701 if trimmed.starts_with("```") {
702 let after_fence = if let Some(newline_pos) = trimmed.find('\n') {
704 &trimmed[newline_pos + 1..]
705 } else {
706 return trimmed.to_string();
707 };
708 if let Some(stripped) = after_fence.strip_suffix("```") {
710 stripped.trim().to_string()
711 } else {
712 after_fence.trim().to_string()
713 }
714 } else {
715 trimmed.to_string()
716 }
717}
718
719fn shell_escape(s: &str) -> String {
721 format!("'{}'", s.replace('\'', "'\\''"))
723}
724
725impl fmt::Display for TransformOp {
726 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
727 match self {
728 TransformOp::Upper => write!(f, "upper"),
729 TransformOp::Lower => write!(f, "lower"),
730 TransformOp::Trim => write!(f, "trim"),
731 TransformOp::TrimStart => write!(f, "trim_start"),
732 TransformOp::TrimEnd => write!(f, "trim_end"),
733 TransformOp::Length => write!(f, "length"),
734 TransformOp::First => write!(f, "first"),
735 TransformOp::Last => write!(f, "last"),
736 TransformOp::FirstN(n) => write!(f, "first({})", n),
737 TransformOp::LastN(n) => write!(f, "last({})", n),
738 TransformOp::Keys => write!(f, "keys"),
739 TransformOp::Values => write!(f, "values"),
740 TransformOp::Flatten => write!(f, "flatten"),
741 TransformOp::Reverse => write!(f, "reverse"),
742 TransformOp::Sort => write!(f, "sort"),
743 TransformOp::Unique => write!(f, "unique"),
744 TransformOp::Compact => write!(f, "compact"),
745 TransformOp::ToString => write!(f, "to_string"),
746 TransformOp::ToNumber => write!(f, "to_number"),
747 TransformOp::ToBool => write!(f, "to_bool"),
748 TransformOp::ToJson => write!(f, "to_json"),
749 TransformOp::ParseJson => write!(f, "parse_json"),
750 TransformOp::Round(None) => write!(f, "round"),
751 TransformOp::Round(Some(d)) => write!(f, "round({})", d),
752 TransformOp::Abs => write!(f, "abs"),
753 TransformOp::Ceil => write!(f, "ceil"),
754 TransformOp::Floor => write!(f, "floor"),
755 TransformOp::Default(v) => write!(f, "default({})", v),
756 TransformOp::TypeOf => write!(f, "type_of"),
757 TransformOp::Join(sep) => write!(f, "join('{}')", sep),
758 TransformOp::Split(sep) => write!(f, "split('{}')", sep),
759 TransformOp::Shell => write!(f, "shell"),
760 }
761 }
762}
763
764#[cfg(test)]
769mod tests {
770 use super::*;
771 use serde_json::json;
772
773 #[test]
778 fn parse_upper() {
779 let expr = TransformExpr::parse("upper").unwrap();
780 assert_eq!(expr.ops.as_slice(), &[TransformOp::Upper]);
781 }
782
783 #[test]
784 fn parse_lower() {
785 let expr = TransformExpr::parse("lower").unwrap();
786 assert_eq!(expr.ops.as_slice(), &[TransformOp::Lower]);
787 }
788
789 #[test]
790 fn parse_trim() {
791 let expr = TransformExpr::parse("trim").unwrap();
792 assert_eq!(expr.ops.as_slice(), &[TransformOp::Trim]);
793 }
794
795 #[test]
796 fn parse_length() {
797 let expr = TransformExpr::parse("length").unwrap();
798 assert_eq!(expr.ops.as_slice(), &[TransformOp::Length]);
799 }
800
801 #[test]
802 fn parse_first() {
803 let expr = TransformExpr::parse("first").unwrap();
804 assert_eq!(expr.ops.as_slice(), &[TransformOp::First]);
805 }
806
807 #[test]
808 fn parse_first_n() {
809 let expr = TransformExpr::parse("first(3)").unwrap();
810 assert_eq!(expr.ops.as_slice(), &[TransformOp::FirstN(3)]);
811 }
812
813 #[test]
814 fn parse_last_n() {
815 let expr = TransformExpr::parse("last(5)").unwrap();
816 assert_eq!(expr.ops.as_slice(), &[TransformOp::LastN(5)]);
817 }
818
819 #[test]
820 fn parse_join() {
821 let expr = TransformExpr::parse("join(', ')").unwrap();
822 assert_eq!(expr.ops.as_slice(), &[TransformOp::Join(", ".to_string())]);
823 }
824
825 #[test]
826 fn parse_split() {
827 let expr = TransformExpr::parse("split('/')").unwrap();
828 assert_eq!(expr.ops.as_slice(), &[TransformOp::Split("/".to_string())]);
829 }
830
831 #[test]
832 fn parse_default_string() {
833 let expr = TransformExpr::parse("default('N/A')").unwrap();
834 assert_eq!(
835 expr.ops.as_slice(),
836 &[TransformOp::Default(Value::String("N/A".to_string()))]
837 );
838 }
839
840 #[test]
841 fn parse_default_number() {
842 let expr = TransformExpr::parse("default(42)").unwrap();
843 assert_eq!(expr.ops.as_slice(), &[TransformOp::Default(json!(42))]);
844 }
845
846 #[test]
847 fn parse_round() {
848 let expr = TransformExpr::parse("round(2)").unwrap();
849 assert_eq!(expr.ops.as_slice(), &[TransformOp::Round(Some(2))]);
850 }
851
852 #[test]
853 fn parse_round_no_arg() {
854 let expr = TransformExpr::parse("round").unwrap();
855 assert_eq!(expr.ops.as_slice(), &[TransformOp::Round(None)]);
856 }
857
858 #[test]
859 fn parse_shell() {
860 let expr = TransformExpr::parse("shell").unwrap();
861 assert_eq!(expr.ops.as_slice(), &[TransformOp::Shell]);
862 }
863
864 #[test]
865 fn parse_to_json() {
866 let expr = TransformExpr::parse("to_json").unwrap();
867 assert_eq!(expr.ops.as_slice(), &[TransformOp::ToJson]);
868 }
869
870 #[test]
871 fn parse_parse_json() {
872 let expr = TransformExpr::parse("parse_json").unwrap();
873 assert_eq!(expr.ops.as_slice(), &[TransformOp::ParseJson]);
874 }
875
876 #[test]
877 fn parse_unknown() {
878 let err = TransformExpr::parse("bogus").unwrap_err();
879 assert!(err.reason.contains("unknown transform"));
880 }
881
882 #[test]
883 fn parse_pipeline() {
884 let expr = TransformExpr::parse("sort | unique | first(3)").unwrap();
885 assert_eq!(
886 expr.ops.as_slice(),
887 &[
888 TransformOp::Sort,
889 TransformOp::Unique,
890 TransformOp::FirstN(3),
891 ]
892 );
893 }
894
895 #[test]
896 fn parse_empty() {
897 let expr = TransformExpr::parse("").unwrap();
898 assert!(expr.is_empty());
899 }
900
901 #[test]
902 fn parse_single() {
903 let expr = TransformExpr::parse("upper").unwrap();
904 assert_eq!(expr.ops.len(), 1);
905 }
906
907 #[test]
912 fn apply_upper_string() {
913 let result = TransformOp::Upper.apply(&json!("hello")).unwrap();
914 assert_eq!(result, json!("HELLO"));
915 }
916
917 #[test]
918 fn apply_upper_non_string() {
919 let err = TransformOp::Upper.apply(&json!(42)).unwrap_err();
920 assert!(matches!(err, TransformError::TypeMismatch { .. }));
921 }
922
923 #[test]
924 fn apply_upper_null() {
925 let err = TransformOp::Upper.apply(&Value::Null).unwrap_err();
926 assert!(matches!(err, TransformError::NullInput { .. }));
927 }
928
929 #[test]
930 fn apply_lower_string() {
931 let result = TransformOp::Lower.apply(&json!("HELLO")).unwrap();
932 assert_eq!(result, json!("hello"));
933 }
934
935 #[test]
936 fn apply_trim() {
937 let result = TransformOp::Trim.apply(&json!(" hello ")).unwrap();
938 assert_eq!(result, json!("hello"));
939 }
940
941 #[test]
942 fn apply_trim_start() {
943 let result = TransformOp::TrimStart.apply(&json!(" hello ")).unwrap();
944 assert_eq!(result, json!("hello "));
945 }
946
947 #[test]
948 fn apply_trim_end() {
949 let result = TransformOp::TrimEnd.apply(&json!(" hello ")).unwrap();
950 assert_eq!(result, json!(" hello"));
951 }
952
953 #[test]
958 fn apply_length_array() {
959 let result = TransformOp::Length.apply(&json!([1, 2, 3])).unwrap();
960 assert_eq!(result, json!(3));
961 }
962
963 #[test]
964 fn apply_length_string() {
965 let result = TransformOp::Length.apply(&json!("abc")).unwrap();
966 assert_eq!(result, json!(3));
967 }
968
969 #[test]
970 fn apply_length_object() {
971 let result = TransformOp::Length.apply(&json!({"a": 1, "b": 2})).unwrap();
972 assert_eq!(result, json!(2));
973 }
974
975 #[test]
976 fn apply_length_null() {
977 let result = TransformOp::Length.apply(&Value::Null).unwrap();
978 assert_eq!(result, Value::Null); }
980
981 #[test]
982 fn apply_first_array() {
983 let result = TransformOp::First.apply(&json!([1, 2, 3])).unwrap();
984 assert_eq!(result, json!(1));
985 }
986
987 #[test]
988 fn apply_first_empty() {
989 let result = TransformOp::First.apply(&json!([])).unwrap();
990 assert_eq!(result, Value::Null);
991 }
992
993 #[test]
994 fn apply_last_array() {
995 let result = TransformOp::Last.apply(&json!([1, 2, 3])).unwrap();
996 assert_eq!(result, json!(3));
997 }
998
999 #[test]
1000 fn apply_first_n() {
1001 let result = TransformOp::FirstN(3)
1002 .apply(&json!([1, 2, 3, 4, 5]))
1003 .unwrap();
1004 assert_eq!(result, json!([1, 2, 3]));
1005 }
1006
1007 #[test]
1008 fn apply_last_n() {
1009 let result = TransformOp::LastN(2)
1010 .apply(&json!([1, 2, 3, 4, 5]))
1011 .unwrap();
1012 assert_eq!(result, json!([4, 5]));
1013 }
1014
1015 #[test]
1016 fn apply_keys() {
1017 let result = TransformOp::Keys.apply(&json!({"a": 1, "b": 2})).unwrap();
1018 assert_eq!(result, json!(["a", "b"]));
1020 }
1021
1022 #[test]
1023 fn apply_keys_null() {
1024 let result = TransformOp::Keys.apply(&Value::Null).unwrap();
1025 assert_eq!(result, Value::Null); }
1027
1028 #[test]
1029 fn apply_values() {
1030 let result = TransformOp::Values.apply(&json!({"a": 1, "b": 2})).unwrap();
1031 assert_eq!(result, json!([1, 2]));
1032 }
1033
1034 #[test]
1035 fn apply_sort() {
1036 let result = TransformOp::Sort.apply(&json!([3, 1, 2])).unwrap();
1037 assert_eq!(result, json!([1, 2, 3]));
1038 }
1039
1040 #[test]
1041 fn apply_unique() {
1042 let result = TransformOp::Unique.apply(&json!([1, 2, 2, 3])).unwrap();
1043 assert_eq!(result, json!([1, 2, 3]));
1044 }
1045
1046 #[test]
1047 fn apply_compact() {
1048 let result = TransformOp::Compact
1049 .apply(&json!([1, null, 2, null]))
1050 .unwrap();
1051 assert_eq!(result, json!([1, 2]));
1052 }
1053
1054 #[test]
1055 fn apply_flatten() {
1056 let result = TransformOp::Flatten.apply(&json!([[1, 2], [3]])).unwrap();
1057 assert_eq!(result, json!([1, 2, 3]));
1058 }
1059
1060 #[test]
1061 fn apply_reverse() {
1062 let result = TransformOp::Reverse.apply(&json!([1, 2, 3])).unwrap();
1063 assert_eq!(result, json!([3, 2, 1]));
1064 }
1065
1066 #[test]
1071 fn apply_to_string() {
1072 let result = TransformOp::ToString.apply(&json!(42)).unwrap();
1073 assert_eq!(result, json!("42"));
1074 }
1075
1076 #[test]
1077 fn apply_to_string_null() {
1078 let result = TransformOp::ToString.apply(&Value::Null).unwrap();
1079 assert_eq!(result, Value::Null); }
1081
1082 #[test]
1083 fn apply_to_number() {
1084 let result = TransformOp::ToNumber.apply(&json!("42")).unwrap();
1085 assert_eq!(result, json!(42));
1086 }
1087
1088 #[test]
1089 fn apply_to_number_float() {
1090 let result = TransformOp::ToNumber.apply(&json!("3.12")).unwrap();
1091 assert_eq!(result, json!(3.12));
1092 }
1093
1094 #[test]
1095 fn apply_to_bool_number() {
1096 assert_eq!(TransformOp::ToBool.apply(&json!(1)).unwrap(), json!(true));
1097 assert_eq!(TransformOp::ToBool.apply(&json!(0)).unwrap(), json!(false));
1098 }
1099
1100 #[test]
1101 fn apply_to_bool_string() {
1102 assert_eq!(
1103 TransformOp::ToBool.apply(&json!("true")).unwrap(),
1104 json!(true)
1105 );
1106 assert_eq!(
1107 TransformOp::ToBool.apply(&json!("false")).unwrap(),
1108 json!(false)
1109 );
1110 }
1111
1112 #[test]
1113 fn apply_to_json() {
1114 let result = TransformOp::ToJson.apply(&json!([1, 2])).unwrap();
1115 assert_eq!(result, json!("[1,2]"));
1116 }
1117
1118 #[test]
1119 fn apply_parse_json() {
1120 let result = TransformOp::ParseJson.apply(&json!(r#"{"a":1}"#)).unwrap();
1121 assert_eq!(result, json!({"a": 1}));
1122 }
1123
1124 #[test]
1129 fn apply_round() {
1130 let result = TransformOp::Round(Some(2)).apply(&json!(4.56789)).unwrap();
1131 assert_eq!(result, json!(4.57));
1132 }
1133
1134 #[test]
1135 fn apply_round_no_decimals() {
1136 let result = TransformOp::Round(None).apply(&json!(3.7)).unwrap();
1137 assert_eq!(result, json!(4.0));
1138 }
1139
1140 #[test]
1141 fn apply_abs() {
1142 let result = TransformOp::Abs.apply(&json!(-5)).unwrap();
1143 assert_eq!(result, json!(5));
1144 }
1145
1146 #[test]
1147 fn apply_abs_float() {
1148 let result = TransformOp::Abs.apply(&json!(-3.12)).unwrap();
1149 assert_eq!(result, json!(3.12));
1150 }
1151
1152 #[test]
1153 fn apply_ceil() {
1154 let result = TransformOp::Ceil.apply(&json!(3.2)).unwrap();
1155 assert_eq!(result, json!(4));
1156 }
1157
1158 #[test]
1159 fn apply_floor() {
1160 let result = TransformOp::Floor.apply(&json!(3.8)).unwrap();
1161 assert_eq!(result, json!(3));
1162 }
1163
1164 #[test]
1169 fn apply_join() {
1170 let result = TransformOp::Join(", ".to_string())
1171 .apply(&json!(["a", "b"]))
1172 .unwrap();
1173 assert_eq!(result, json!("a, b"));
1174 }
1175
1176 #[test]
1177 fn apply_split() {
1178 let result = TransformOp::Split("/".to_string())
1179 .apply(&json!("a/b/c"))
1180 .unwrap();
1181 assert_eq!(result, json!(["a", "b", "c"]));
1182 }
1183
1184 #[test]
1185 fn apply_default_with_null() {
1186 let result = TransformOp::Default(json!("N/A"))
1187 .apply(&Value::Null)
1188 .unwrap();
1189 assert_eq!(result, json!("N/A"));
1190 }
1191
1192 #[test]
1193 fn apply_default_with_value() {
1194 let result = TransformOp::Default(json!("N/A"))
1195 .apply(&json!("hello"))
1196 .unwrap();
1197 assert_eq!(result, json!("hello"));
1198 }
1199
1200 #[test]
1201 fn apply_typeof() {
1202 assert_eq!(
1203 TransformOp::TypeOf.apply(&json!(42)).unwrap(),
1204 json!("number")
1205 );
1206 assert_eq!(
1207 TransformOp::TypeOf.apply(&json!("x")).unwrap(),
1208 json!("string")
1209 );
1210 assert_eq!(
1211 TransformOp::TypeOf.apply(&Value::Null).unwrap(),
1212 json!("null")
1213 );
1214 assert_eq!(
1215 TransformOp::TypeOf.apply(&json!(true)).unwrap(),
1216 json!("boolean")
1217 );
1218 assert_eq!(
1219 TransformOp::TypeOf.apply(&json!([1])).unwrap(),
1220 json!("array")
1221 );
1222 assert_eq!(
1223 TransformOp::TypeOf.apply(&json!({"a": 1})).unwrap(),
1224 json!("object")
1225 );
1226 }
1227
1228 #[test]
1229 fn apply_shell() {
1230 let result = TransformOp::Shell.apply(&json!("hello world")).unwrap();
1231 assert_eq!(result, json!("'hello world'"));
1232 }
1233
1234 #[test]
1239 fn pipeline_sort_unique() {
1240 let expr = TransformExpr::parse("sort | unique").unwrap();
1241 let result = expr.apply(&json!([3, 1, 2, 1])).unwrap();
1242 assert_eq!(result, json!([1, 2, 3]));
1243 }
1244
1245 #[test]
1246 fn pipeline_sort_first_n() {
1247 let expr = TransformExpr::parse("sort | first(2)").unwrap();
1248 let result = expr.apply(&json!([3, 1, 2])).unwrap();
1249 assert_eq!(result, json!([1, 2]));
1250 }
1251
1252 #[test]
1253 fn pipeline_upper_trim() {
1254 let expr = TransformExpr::parse("trim | upper").unwrap();
1255 let result = expr.apply(&json!(" hello ")).unwrap();
1256 assert_eq!(result, json!("HELLO"));
1257 }
1258
1259 #[test]
1260 fn pipeline_empty() {
1261 let expr = TransformExpr::parse("").unwrap();
1262 let result = expr.apply(&json!("unchanged")).unwrap();
1263 assert_eq!(result, json!("unchanged"));
1264 }
1265
1266 #[test]
1267 fn pipeline_single() {
1268 let expr = TransformExpr::parse("upper").unwrap();
1269 assert_eq!(expr.ops.len(), 1);
1270 }
1271
1272 #[test]
1273 fn pipeline_default_then_upper() {
1274 let expr = TransformExpr::parse("default('unknown') | upper").unwrap();
1275 let result = expr.apply(&Value::Null).unwrap();
1276 assert_eq!(result, json!("UNKNOWN"));
1277 }
1278
1279 #[test]
1284 fn display_ops() {
1285 assert_eq!(TransformOp::Upper.to_string(), "upper");
1286 assert_eq!(TransformOp::FirstN(3).to_string(), "first(3)");
1287 assert_eq!(
1288 TransformOp::Join(", ".to_string()).to_string(),
1289 "join(', ')"
1290 );
1291 assert_eq!(TransformOp::Round(Some(2)).to_string(), "round(2)");
1292 assert_eq!(TransformOp::Round(None).to_string(), "round");
1293 assert_eq!(
1294 TransformOp::Default(json!("N/A")).to_string(),
1295 "default(\"N/A\")"
1296 );
1297 }
1298
1299 #[test]
1304 fn error_display_parse() {
1305 let err = TransformParseError {
1306 input: "bogus".to_string(),
1307 reason: "unknown transform: 'bogus'".to_string(),
1308 };
1309 assert!(err.to_string().contains("NIKA-151"));
1310 }
1311
1312 #[test]
1313 fn error_display_type_mismatch() {
1314 let err = TransformError::TypeMismatch {
1315 op: "upper",
1316 expected: "string",
1317 got: "number".to_string(),
1318 };
1319 assert!(err.to_string().contains("NIKA-152"));
1320 }
1321
1322 #[test]
1323 fn error_display_null_input() {
1324 let err = TransformError::NullInput { op: "sort" };
1325 assert!(err.to_string().contains("NIKA-153"));
1326 }
1327
1328 #[test]
1333 fn parse_default_bool() {
1334 let expr = TransformExpr::parse("default(true)").unwrap();
1335 assert_eq!(expr.ops.as_slice(), &[TransformOp::Default(json!(true))]);
1336 }
1337
1338 #[test]
1339 fn parse_default_null() {
1340 let expr = TransformExpr::parse("default(null)").unwrap();
1341 assert_eq!(expr.ops.as_slice(), &[TransformOp::Default(Value::Null)]);
1342 }
1343
1344 #[test]
1345 fn parse_default_array() {
1346 let expr = TransformExpr::parse("default([])").unwrap();
1347 assert_eq!(expr.ops.as_slice(), &[TransformOp::Default(json!([]))]);
1348 }
1349
1350 #[test]
1351 fn first_n_larger_than_array() {
1352 let result = TransformOp::FirstN(10).apply(&json!([1, 2, 3])).unwrap();
1353 assert_eq!(result, json!([1, 2, 3])); }
1355
1356 #[test]
1357 fn last_n_larger_than_array() {
1358 let result = TransformOp::LastN(10).apply(&json!([1, 2, 3])).unwrap();
1359 assert_eq!(result, json!([1, 2, 3]));
1360 }
1361
1362 #[test]
1363 fn flatten_mixed() {
1364 let result = TransformOp::Flatten
1365 .apply(&json!([[1, 2], 3, [4]]))
1366 .unwrap();
1367 assert_eq!(result, json!([1, 2, 3, 4]));
1368 }
1369
1370 #[test]
1371 fn unclosed_paren() {
1372 let err = TransformExpr::parse("first(3").unwrap_err();
1373 assert!(err.reason.contains("unclosed parenthesis"));
1374 }
1375
1376 #[test]
1377 fn join_mixed_types() {
1378 let result = TransformOp::Join(", ".to_string())
1379 .apply(&json!(["a", 1, true]))
1380 .unwrap();
1381 assert_eq!(result, json!("a, 1, true"));
1382 }
1383
1384 #[test]
1385 fn parse_json_invalid() {
1386 let err = TransformOp::ParseJson
1387 .apply(&json!("not json"))
1388 .unwrap_err();
1389 assert!(matches!(err, TransformError::TypeMismatch { .. }));
1390 }
1391
1392 #[test]
1393 fn to_number_invalid() {
1394 let err = TransformOp::ToNumber.apply(&json!("abc")).unwrap_err();
1395 assert!(matches!(err, TransformError::TypeMismatch { .. }));
1396 }
1397
1398 #[test]
1399 fn to_bool_invalid_string() {
1400 let err = TransformOp::ToBool.apply(&json!("maybe")).unwrap_err();
1401 assert!(matches!(err, TransformError::TypeMismatch { .. }));
1402 }
1403
1404 #[test]
1409 fn first_n_on_object_serializes_and_truncates() {
1410 let obj = json!({"links": [1, 2, 3], "count": 3});
1412 let result = TransformOp::FirstN(10).apply(&obj).unwrap();
1413 assert!(result.is_string());
1415 let s = result.as_str().unwrap();
1416 assert_eq!(s.len(), 10);
1417 }
1418
1419 #[test]
1420 fn first_n_on_object_full() {
1421 let obj = json!({"a": 1});
1423 let result = TransformOp::FirstN(1000).apply(&obj).unwrap();
1424 assert!(result.is_string());
1425 assert_eq!(result.as_str().unwrap(), r#"{"a":1}"#);
1426 }
1427
1428 #[test]
1429 fn first_n_on_string_truncates() {
1430 let result = TransformOp::FirstN(5).apply(&json!("hello world")).unwrap();
1431 assert_eq!(result, json!("hello"));
1432 }
1433
1434 #[test]
1435 fn parse_json_idempotent_on_array() {
1436 let arr = json!([1, 2, 3]);
1438 let result = TransformOp::ParseJson.apply(&arr).unwrap();
1439 assert_eq!(result, json!([1, 2, 3]));
1440 }
1441
1442 #[test]
1443 fn parse_json_idempotent_on_object() {
1444 let obj = json!({"key": "value"});
1446 let result = TransformOp::ParseJson.apply(&obj).unwrap();
1447 assert_eq!(result, json!({"key": "value"}));
1448 }
1449
1450 #[test]
1451 fn parse_json_idempotent_on_number_and_bool() {
1452 assert_eq!(TransformOp::ParseJson.apply(&json!(42)).unwrap(), json!(42));
1454 assert_eq!(
1455 TransformOp::ParseJson.apply(&json!(true)).unwrap(),
1456 json!(true)
1457 );
1458 }
1459
1460 #[test]
1461 fn parse_json_strips_markdown_code_block() {
1462 let input = json!("```json\n{\"name\": \"test\"}\n```");
1463 let result = TransformOp::ParseJson.apply(&input).unwrap();
1464 assert_eq!(result, json!({"name": "test"}));
1465 }
1466
1467 #[test]
1468 fn parse_json_strips_generic_code_block() {
1469 let input = json!("```\n[1, 2, 3]\n```");
1470 let result = TransformOp::ParseJson.apply(&input).unwrap();
1471 assert_eq!(result, json!([1, 2, 3]));
1472 }
1473
1474 #[test]
1475 fn parse_json_handles_bare_json() {
1476 let input = json!("{\"key\": \"value\"}");
1477 let result = TransformOp::ParseJson.apply(&input).unwrap();
1478 assert_eq!(result, json!({"key": "value"}));
1479 }
1480
1481 #[test]
1482 fn parse_json_strips_whitespace_around_code_block() {
1483 let input = json!(" ```json\n [\"a\", \"b\"]\n ``` ");
1484 let result = TransformOp::ParseJson.apply(&input).unwrap();
1485 assert_eq!(result, json!(["a", "b"]));
1486 }
1487
1488 #[test]
1489 fn to_json_then_length_returns_char_count() {
1490 let obj = json!({"countries": ["FR", "US"]});
1492 let json_str = TransformOp::ToJson.apply(&obj).unwrap();
1493 assert!(json_str.is_string());
1494 let length = TransformOp::Length.apply(&json_str).unwrap();
1495 assert!(length.as_u64().unwrap() > 1);
1497 }
1498
1499 #[test]
1501 fn regression_bug30_length_unicode_chars_not_bytes() {
1502 let result = TransformOp::Length.apply(&json!("日本語")).unwrap();
1504 assert_eq!(
1505 result,
1506 json!(3),
1507 "|length on Unicode string must count chars, not bytes"
1508 );
1509 }
1510
1511 #[test]
1513 fn regression_bug30_length_unicode_emoji() {
1514 let result = TransformOp::Length.apply(&json!("👋🌍")).unwrap();
1516 assert_eq!(result, json!(2), "|length on emoji string must count chars");
1517 }
1518
1519 #[test]
1521 fn regression_bug30_length_ascii_unchanged() {
1522 let result = TransformOp::Length.apply(&json!("abc")).unwrap();
1523 assert_eq!(result, json!(3), "|length on ASCII string is still correct");
1524 }
1525
1526 #[test]
1528 fn regression_bug46_sort_numeric_ordering() {
1529 let result = TransformOp::Sort.apply(&json!([1, 10, 2, 20, 3])).unwrap();
1530 assert_eq!(
1531 result,
1532 json!([1, 2, 3, 10, 20]),
1533 "|sort on numbers must use numeric ordering, not lexicographic"
1534 );
1535 }
1536
1537 #[test]
1539 fn regression_bug46_sort_mixed_types() {
1540 let result = TransformOp::Sort.apply(&json!([10, 2, "b", "a"])).unwrap();
1541 assert_eq!(result, json!([2, 10, "a", "b"]));
1542 }
1543
1544 #[test]
1546 fn regression_bug46_sort_strings_unchanged() {
1547 let result = TransformOp::Sort
1548 .apply(&json!(["banana", "apple", "cherry"]))
1549 .unwrap();
1550 assert_eq!(result, json!(["apple", "banana", "cherry"]));
1551 }
1552
1553 #[test]
1555 fn regression_bug46_sort_floats() {
1556 let result = TransformOp::Sort
1557 .apply(&json!([1.5, 0.1, 2.3, 0.9]))
1558 .unwrap();
1559 assert_eq!(result, json!([0.1, 0.9, 1.5, 2.3]));
1560 }
1561}