runtara_agents/agents/
transform.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Transform agents for workflow execution
4//!
5//! This module provides data transformation operations that can be used in workflows:
6//! - Property extraction and manipulation (get/set values by path)
7//! - Array filtering and sorting
8//! - Array flattening (flat-map)
9//! - JSON parsing and serialization
10//! - Field mapping between objects
11//!
12//! All operations accept Rust data structures directly (no CloudEvents wrapper)
13
14use runtara_agent_macro::{CapabilityInput, CapabilityOutput, capability};
15use runtara_dsl::agent_meta::EnumVariants;
16use serde::{Deserialize, Deserializer, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19use strum::VariantNames;
20
21/// Custom deserializer that treats null as an empty Vec
22fn deserialize_value_or_empty_vec<'de, D>(deserializer: D) -> Result<Vec<Value>, D::Error>
23where
24    D: Deserializer<'de>,
25{
26    let opt: Option<Vec<Value>> = Option::deserialize(deserializer)?;
27    Ok(opt.unwrap_or_default())
28}
29
30// ============================================================================
31// Input/Output Types
32// ============================================================================
33
34/// Input for extracting property values from an array
35#[derive(Debug, Deserialize, CapabilityInput)]
36#[capability_input(display_name = "Extract Property Input")]
37pub struct ExtractInput {
38    /// The array of objects to extract from
39    #[field(
40        display_name = "Input Array",
41        description = "The array of objects to extract property values from",
42        example = r#"[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]"#
43    )]
44    pub value: Vec<Value>,
45
46    /// JSONPath to the property to extract
47    #[field(
48        display_name = "Property Path",
49        description = "The property path to extract from each item (JSONPath syntax)",
50        example = "name"
51    )]
52    pub property_path: String,
53}
54
55/// Input for getting a value from an object by property path
56#[derive(Debug, Deserialize, CapabilityInput)]
57#[capability_input(display_name = "Get Value Input")]
58pub struct GetValueByPathInput {
59    /// The object to extract from
60    #[field(
61        display_name = "Input Value",
62        description = "The object to extract a property value from",
63        example = r#"{"user": {"name": "Alice", "age": 30}}"#
64    )]
65    pub value: Option<Value>,
66
67    /// JSONPath to the property
68    #[field(
69        display_name = "Property Path",
70        description = "The property path to extract (JSONPath syntax)",
71        example = "user.name"
72    )]
73    pub property_path: Option<String>,
74}
75
76/// Input for setting a value in an object at a property path
77#[derive(Debug, Deserialize, CapabilityInput)]
78#[capability_input(display_name = "Set Value Input")]
79pub struct SetValueByPathInput {
80    /// The target object to modify
81    #[field(
82        display_name = "Target Object",
83        description = "The object to set a property value in",
84        example = r#"{"user": {"name": "Alice"}}"#
85    )]
86    pub target: Option<Value>,
87
88    /// JSONPath to the property to set
89    #[field(
90        display_name = "Property Path",
91        description = "The property path to set (JSONPath syntax, creates nested objects if needed)",
92        example = "user.age"
93    )]
94    pub property_path: Option<String>,
95
96    /// The value to set
97    #[field(
98        display_name = "Value",
99        description = "The value to set at the property path",
100        example = "30"
101    )]
102    pub value: Option<Value>,
103}
104
105/// Input for filtering out empty/null/blank values from an array
106#[derive(Debug, Deserialize, CapabilityInput)]
107#[capability_input(display_name = "Filter Non-Values Input")]
108pub struct FilterNoValueInput {
109    /// The array to filter
110    #[field(
111        display_name = "Input Array",
112        description = "The array of items to filter",
113        example = r#"[{"x": 1}, {"x": null}, {"x": ""}]"#
114    )]
115    pub value: Vec<Value>,
116
117    /// Optional property path to check
118    #[field(
119        display_name = "Property Path",
120        description = "The property path to check in each item (if omitted, checks the item itself)",
121        example = "x"
122    )]
123    #[serde(default)]
124    pub property_path: Option<String>,
125
126    /// Remove items with empty strings
127    #[field(
128        display_name = "Filter Empty Strings",
129        description = "Remove items where the value is an empty string (\"\")",
130        example = "false",
131        default = "false"
132    )]
133    #[serde(default)]
134    pub filter_empty_strings: bool,
135
136    /// Remove items with null values
137    #[field(
138        display_name = "Filter Null Values",
139        description = "Remove items where the value is null",
140        example = "true",
141        default = "false"
142    )]
143    #[serde(default)]
144    pub filter_null_values: bool,
145
146    /// Remove items with blank strings
147    #[field(
148        display_name = "Filter Blank Strings",
149        description = "Remove items where the value is a whitespace-only string",
150        example = "false",
151        default = "false"
152    )]
153    #[serde(default)]
154    pub filter_blank_strings: bool,
155
156    /// Remove items with zero values
157    #[field(
158        display_name = "Filter Zero Values",
159        description = "Remove items where the value is 0 or \"0\"",
160        example = "false",
161        default = "false"
162    )]
163    #[serde(default)]
164    pub filter_zero_values: bool,
165}
166
167/// Input for selecting the first truthy value from an array
168#[derive(Debug, Deserialize, CapabilityInput)]
169#[capability_input(display_name = "Select First Input")]
170pub struct SelectFirstInput {
171    /// The array to select from
172    #[field(
173        display_name = "Input Array",
174        description = "The array to select the first truthy value from (skips null, empty strings, 0, false)",
175        example = r#"[null, "", 0, "hello", "world"]"#
176    )]
177    pub value: Option<Vec<Value>>,
178}
179
180/// Input for parsing a JSON string into a value
181#[derive(Debug, Deserialize, CapabilityInput)]
182#[capability_input(display_name = "Parse JSON Input")]
183pub struct FromJsonStringInput {
184    /// The JSON string to parse
185    #[field(
186        display_name = "JSON String",
187        description = "The JSON string to parse into a value",
188        example = r#""{\"name\":\"Alice\",\"age\":30}""#
189    )]
190    pub value: Option<String>,
191}
192
193/// Input for converting a value to a JSON string
194#[derive(Debug, Deserialize, CapabilityInput)]
195#[capability_input(display_name = "Stringify JSON Input")]
196pub struct ToJsonStringInput {
197    /// The value to convert
198    #[field(
199        display_name = "Input Value",
200        description = "The value to serialize to a JSON string",
201        example = r#"{"name": "Alice", "age": 30}"#
202    )]
203    pub value: Value,
204}
205
206/// Condition for filtering array items
207#[derive(Debug, Deserialize, Clone, PartialEq, VariantNames)]
208#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
209#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
210pub enum MatchCondition {
211    /// Keep items where property value is in match_values
212    Includes,
213    /// Keep items where property value is NOT in match_values
214    Excludes,
215    /// Keep items where property value starts with one of match_values
216    StartsWith,
217    /// Keep items where property value ends with one of match_values
218    EndsWith,
219    /// Keep items where property value contains one of match_values
220    Contains,
221}
222
223impl EnumVariants for MatchCondition {
224    fn variant_names() -> &'static [&'static str] {
225        Self::VARIANTS
226    }
227}
228
229/// Input for filtering an array based on property values
230#[derive(Debug, Deserialize, CapabilityInput)]
231#[capability_input(display_name = "Filter Array Input")]
232pub struct FilterInput {
233    /// The array to filter
234    #[field(
235        display_name = "Input Array",
236        description = "The array of items to filter",
237        example = r#"[{"status": "active"}, {"status": "inactive"}]"#
238    )]
239    #[serde(default, deserialize_with = "deserialize_value_or_empty_vec")]
240    pub value: Vec<Value>,
241
242    /// JSONPath to the property to check
243    #[field(
244        display_name = "Property Path",
245        description = "The property path to extract from each item (JSONPath syntax). Use \"$\" or \"\" to filter the array values directly.",
246        example = "status"
247    )]
248    pub property_path: String,
249
250    /// Values to match against
251    #[field(
252        display_name = "Match Values",
253        description = "The value(s) to compare against (single value or array)",
254        example = r#"["active", "pending"]"#
255    )]
256    pub match_values: Value,
257
258    /// How to match the values
259    #[field(
260        display_name = "Match Condition",
261        description = "Whether to include or exclude matching items",
262        example = "INCLUDES",
263        enum_type = "MatchCondition"
264    )]
265    pub match_condition: MatchCondition,
266}
267
268/// Input for sorting an array
269#[derive(Debug, Deserialize, CapabilityInput)]
270#[capability_input(display_name = "Sort Array Input")]
271pub struct SortInput {
272    /// The array to sort
273    #[field(
274        display_name = "Input Array",
275        description = "The array to sort",
276        example = r#"[{"age": 35}, {"age": 30}, {"age": 25}]"#
277    )]
278    pub value: Vec<Value>,
279
280    /// Optional property path to sort by
281    #[field(
282        display_name = "Property Path",
283        description = "The property path to sort by (if omitted, sorts the items directly)",
284        example = "age"
285    )]
286    pub property_path: Option<String>,
287
288    /// Sort in ascending order
289    #[field(
290        display_name = "Ascending Order",
291        description = "Whether to sort in ascending order (true) or descending order (false)",
292        example = "true",
293        default = "true"
294    )]
295    #[serde(default = "default_ascending")]
296    pub ascending: bool,
297}
298
299fn default_ascending() -> bool {
300    true
301}
302
303/// Input for mapping fields from source to target object
304#[derive(Debug, Deserialize, CapabilityInput)]
305#[capability_input(display_name = "Map Fields Input")]
306pub struct MapFieldsInput {
307    /// The source object to map from
308    #[field(
309        display_name = "Source Data",
310        description = "The source object containing data to map",
311        example = r#"{"firstName": "Alice", "userAge": 30, "email": "alice@example.com"}"#
312    )]
313    pub source_data: Value,
314
315    /// Field mappings from source to target
316    #[field(
317        display_name = "Field Mappings",
318        description = "Map of source field paths to target field names",
319        example = r#"{"firstName": "name", "userAge": "age"}"#
320    )]
321    pub mappings: HashMap<String, String>,
322}
323
324/// Input for grouping array items by a key
325#[derive(Debug, Deserialize, CapabilityInput)]
326#[capability_input(display_name = "Group By Input")]
327pub struct GroupByInput {
328    /// The array to group
329    #[field(
330        display_name = "Input Array",
331        description = "The array of items to group",
332        example = r#"[{"name": "Alice", "status": "active"}, {"name": "Bob", "status": "inactive"}]"#
333    )]
334    pub value: Value,
335
336    /// The property path to group by
337    #[field(
338        display_name = "Group Key",
339        description = "The property path to use as the grouping key (JSONPath syntax)",
340        example = "status"
341    )]
342    pub key: String,
343
344    /// Return as map instead of array
345    #[field(
346        display_name = "Return As Map",
347        description = "Return grouped items as a map (key -> items) instead of an array of arrays",
348        example = "true",
349        default = "false"
350    )]
351    #[serde(default)]
352    pub as_map: bool,
353}
354
355/// Input for appending an item to an array
356#[derive(Debug, Deserialize, CapabilityInput)]
357#[capability_input(display_name = "Append Input")]
358pub struct AppendInput {
359    /// The array to append to
360    #[field(
361        display_name = "Array",
362        description = "The array to append an item to (can contain objects or primitive values)",
363        example = r#"[{"name": "Alice"}, {"name": "Bob"}]"#
364    )]
365    pub array: Vec<Value>,
366
367    /// The item to append
368    #[field(
369        display_name = "Item",
370        description = "The item to append to the array (can be an object or primitive value)",
371        example = r#"{"name": "Charlie"}"#
372    )]
373    pub item: Value,
374}
375
376/// Input for flat mapping an array (extracting nested arrays and flattening)
377#[derive(Debug, Deserialize, CapabilityInput)]
378#[capability_input(display_name = "Flat Map Input")]
379pub struct FlatMapInput {
380    /// The array of objects containing nested arrays
381    #[field(
382        display_name = "Input Array",
383        description = "The array of objects to flat map",
384        example = r#"[{"items": [1, 2]}, {"items": [3, 4]}]"#
385    )]
386    pub value: Vec<Value>,
387
388    /// JSONPath to the nested array property to extract and flatten
389    #[field(
390        display_name = "Property Path",
391        description = "The property path to the nested array in each item (JSONPath syntax)",
392        example = "items"
393    )]
394    pub property_path: String,
395}
396
397// ============================================================================
398// Output Types
399// ============================================================================
400
401/// Output from extract operation
402#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
403#[capability_output(display_name = "Extract Output")]
404pub struct ExtractOutput {
405    /// The extracted values
406    #[field(
407        display_name = "Values",
408        description = "Array of extracted property values"
409    )]
410    pub values: Vec<Value>,
411
412    /// Number of values extracted
413    #[field(display_name = "Count", description = "Number of values extracted")]
414    pub count: usize,
415}
416
417/// Output from filter operations (filter, filter_non_values)
418#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
419#[capability_output(display_name = "Filter Output")]
420pub struct FilterOutput {
421    /// The filtered items
422    #[field(
423        display_name = "Items",
424        description = "Array of items that passed the filter"
425    )]
426    pub items: Vec<Value>,
427
428    /// Number of items after filtering
429    #[field(
430        display_name = "Count",
431        description = "Number of items that passed the filter"
432    )]
433    pub count: usize,
434
435    /// Number of items removed by the filter
436    #[field(
437        display_name = "Removed Count",
438        description = "Number of items removed by the filter"
439    )]
440    pub removed_count: usize,
441}
442
443/// Output from sort operation
444#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
445#[capability_output(display_name = "Sort Output")]
446pub struct SortOutput {
447    /// The sorted items
448    #[field(display_name = "Items", description = "Array of sorted items")]
449    pub items: Vec<Value>,
450
451    /// Number of items
452    #[field(
453        display_name = "Count",
454        description = "Number of items in the sorted array"
455    )]
456    pub count: usize,
457}
458
459/// Output from group_by operation
460#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
461#[capability_output(display_name = "Group By Output")]
462pub struct GroupByOutput {
463    /// Grouped items (as map or array based on as_map flag)
464    #[field(
465        display_name = "Groups",
466        description = "Grouped items - either a map (key -> items) or array of arrays"
467    )]
468    pub groups: Value,
469
470    /// Number of unique groups
471    #[field(
472        display_name = "Group Count",
473        description = "Number of unique groups created"
474    )]
475    pub group_count: usize,
476}
477
478/// Output from map_fields operation
479#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
480#[capability_output(display_name = "Map Fields Output")]
481pub struct MapFieldsOutput {
482    /// The mapped result object
483    #[field(
484        display_name = "Result",
485        description = "Object with mapped field values"
486    )]
487    pub result: HashMap<String, Value>,
488
489    /// Number of fields mapped
490    #[field(
491        display_name = "Field Count",
492        description = "Number of fields successfully mapped"
493    )]
494    pub field_count: usize,
495}
496
497/// Output from append operation
498#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
499#[capability_output(display_name = "Append Output")]
500pub struct AppendOutput {
501    /// The array with appended item
502    #[field(
503        display_name = "Array",
504        description = "Array with the new item appended"
505    )]
506    pub array: Vec<Value>,
507
508    /// New length of the array
509    #[field(
510        display_name = "Length",
511        description = "New length of the array after appending"
512    )]
513    pub length: usize,
514}
515
516/// Output from flat_map operation
517#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
518#[capability_output(display_name = "Flat Map Output")]
519pub struct FlatMapOutput {
520    /// The flattened items
521    #[field(
522        display_name = "Items",
523        description = "Flattened array of all nested items"
524    )]
525    pub items: Vec<Value>,
526
527    /// Number of items after flattening
528    #[field(
529        display_name = "Count",
530        description = "Total number of items in the flattened array"
531    )]
532    pub count: usize,
533}
534
535/// Input for array-length operation
536#[derive(Debug, Deserialize, CapabilityInput)]
537#[capability_input(display_name = "Array Length Input")]
538pub struct ArrayLengthInput {
539    /// The array or value to get the length of
540    #[field(
541        display_name = "Value",
542        description = "Array, string, or object to get the length/size of",
543        example = r#"[1, 2, 3]"#
544    )]
545    pub value: Value,
546}
547
548/// Output from array-length operation
549#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
550#[capability_output(display_name = "Array Length Output")]
551pub struct ArrayLengthOutput {
552    /// The length/size of the value
553    #[field(
554        display_name = "Length",
555        description = "Length of array/string, or number of keys in object"
556    )]
557    pub length: usize,
558
559    /// Whether the value was an array
560    #[field(
561        display_name = "Is Array",
562        description = "True if the value was an array"
563    )]
564    pub is_array: bool,
565}
566
567/// Output from to_json_string operation
568#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
569#[capability_output(display_name = "JSON String Output")]
570pub struct ToJsonStringOutput {
571    /// The JSON string
572    #[field(display_name = "JSON", description = "The serialized JSON string")]
573    pub json: String,
574
575    /// Length of the JSON string
576    #[field(
577        display_name = "Length",
578        description = "Length of the JSON string in characters"
579    )]
580    pub length: usize,
581}
582
583// ============================================================================
584// Operations
585// ============================================================================
586
587/// Extracts values from an array of objects based on a property path
588#[capability(
589    module = "transform",
590    display_name = "Extract Property",
591    description = "Extract property values from an array of objects based on a property path"
592)]
593pub fn extract(input: ExtractInput) -> Result<ExtractOutput, String> {
594    if input.value.is_empty() || input.property_path.is_empty() {
595        let count = input.value.len();
596        return Ok(ExtractOutput {
597            values: input.value,
598            count,
599        });
600    }
601
602    let values: Vec<Value> = input
603        .value
604        .iter()
605        .map(|item| get_property_value(item, &input.property_path))
606        .collect();
607    let count = values.len();
608    Ok(ExtractOutput { values, count })
609}
610
611/// Gets a value from an object by property path
612#[capability(
613    module = "transform",
614    display_name = "Get Value By Path",
615    description = "Get a value from an object using a JSONPath-like property path"
616)]
617pub fn get_value_by_path(input: GetValueByPathInput) -> Result<Value, String> {
618    let result = match (input.value, input.property_path) {
619        (Some(value), Some(path)) if !path.is_empty() => get_property_value(&value, &path),
620        _ => Value::Null,
621    };
622    Ok(result)
623}
624
625/// Sets a value in an object at the specified property path
626#[capability(
627    module = "transform",
628    display_name = "Set Value By Path",
629    description = "Set a value in an object at a specified JSONPath-like property path"
630)]
631pub fn set_value_by_path(input: SetValueByPathInput) -> Result<Value, String> {
632    let result = match (input.target, input.property_path, input.value) {
633        (Some(target), Some(path), value) if !path.is_empty() => {
634            set_property_value(target, &path, value.unwrap_or(Value::Null))
635        }
636        (Some(target), _, _) => target,
637        _ => Value::Null,
638    };
639    Ok(result)
640}
641
642/// Filters an array removing elements with no values based on criteria
643#[capability(
644    module = "transform",
645    display_name = "Filter Non-Values",
646    description = "Filter an array removing elements with null, empty, blank, or zero values"
647)]
648pub fn filter_non_values(input: FilterNoValueInput) -> Result<FilterOutput, String> {
649    let original_count = input.value.len();
650    if input.value.is_empty() {
651        return Ok(FilterOutput {
652            items: input.value,
653            count: 0,
654            removed_count: 0,
655        });
656    }
657
658    let items: Vec<Value> = input
659        .value
660        .into_iter()
661        .filter(|item| {
662            let property_value = if let Some(ref path) = input.property_path {
663                get_property_value(item, path)
664            } else {
665                item.clone()
666            };
667
668            // Filter null values if specified
669            if input.filter_null_values && property_value.is_null() {
670                return false;
671            }
672
673            // Filter empty strings if specified
674            if input.filter_empty_strings
675                && let Some(s) = property_value.as_str()
676                && s.is_empty()
677            {
678                return false;
679            }
680
681            // Filter blank strings if specified
682            if input.filter_blank_strings
683                && let Some(s) = property_value.as_str()
684                && s.trim().is_empty()
685            {
686                return false;
687            }
688
689            // Filter zero values if specified
690            if input.filter_zero_values {
691                if let Some(n) = property_value.as_f64()
692                    && n == 0.0
693                {
694                    return false;
695                }
696                if let Some(s) = property_value.as_str()
697                    && s == "0"
698                {
699                    return false;
700                }
701            }
702
703            true
704        })
705        .collect();
706    let count = items.len();
707    Ok(FilterOutput {
708        items,
709        count,
710        removed_count: original_count - count,
711    })
712}
713
714/// Returns the first truthy value from an array
715#[capability(
716    module = "transform",
717    display_name = "Select First",
718    description = "Select the first truthy value from an array (skips null, empty, zero, false)"
719)]
720pub fn select_first(input: SelectFirstInput) -> Result<Value, String> {
721    let Some(values) = input.value else {
722        return Ok(Value::Null);
723    };
724
725    if values.is_empty() {
726        return Ok(Value::Null);
727    }
728
729    for item in values {
730        // Skip null values
731        if item.is_null() {
732            continue;
733        }
734
735        // Filter empty strings
736        if let Some(s) = item.as_str()
737            && (s.is_empty() || s.trim().is_empty())
738        {
739            continue;
740        }
741
742        // Filter zero values
743        if let Some(n) = item.as_f64()
744            && n == 0.0
745        {
746            continue;
747        }
748
749        // Filter false booleans
750        if let Some(b) = item.as_bool()
751            && !b
752        {
753            continue;
754        }
755
756        return Ok(item);
757    }
758
759    Ok(Value::Null)
760}
761
762/// Parses a JSON string into a Value
763#[capability(
764    module = "transform",
765    display_name = "From JSON String",
766    description = "Parse a JSON string into a structured value"
767)]
768pub fn from_json_string(input: FromJsonStringInput) -> Result<Value, String> {
769    match input.value {
770        Some(json_str) if !json_str.is_empty() => {
771            serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
772        }
773        _ => Ok(Value::Null),
774    }
775}
776
777/// Converts a Value to a JSON string
778#[capability(
779    module = "transform",
780    display_name = "To JSON String",
781    description = "Convert a value to a JSON string"
782)]
783pub fn to_json_string(input: ToJsonStringInput) -> Result<ToJsonStringOutput, String> {
784    let json = serde_json::to_string(&input.value)
785        .map_err(|e| format!("Failed to serialize JSON: {}", e))?;
786    let length = json.len();
787    Ok(ToJsonStringOutput { json, length })
788}
789
790/// Filters an array based on property values matching filter criteria
791#[capability(
792    module = "transform",
793    display_name = "Filter Array",
794    description = "Filter an array based on property values matching or excluding specified values"
795)]
796pub fn filter(input: FilterInput) -> Result<FilterOutput, String> {
797    let original_count = input.value.len();
798    if input.value.is_empty() {
799        return Ok(FilterOutput {
800            items: input.value,
801            count: 0,
802            removed_count: 0,
803        });
804    }
805
806    // Convert match_values to a list
807    let match_values_list: Vec<Value> = match input.match_values {
808        Value::Array(arr) => arr,
809        other => vec![other],
810    };
811
812    // Check if we should filter the actual values (property_path is "$" or empty)
813    let filter_actual_values = input.property_path.is_empty() || input.property_path == "$";
814
815    let filtered: Vec<Value> = input
816        .value
817        .into_iter()
818        .filter(|item| {
819            let property_value = if filter_actual_values {
820                item.clone()
821            } else {
822                get_property_value(item, &input.property_path)
823            };
824
825            match input.match_condition {
826                MatchCondition::Includes => {
827                    matches_filter_values(&property_value, &match_values_list)
828                }
829                MatchCondition::Excludes => {
830                    !matches_filter_values(&property_value, &match_values_list)
831                }
832                MatchCondition::StartsWith => {
833                    matches_string_filter(&property_value, &match_values_list, |s, v| {
834                        s.starts_with(v)
835                    })
836                }
837                MatchCondition::EndsWith => {
838                    matches_string_filter(&property_value, &match_values_list, |s, v| {
839                        s.ends_with(v)
840                    })
841                }
842                MatchCondition::Contains => {
843                    matches_string_filter(&property_value, &match_values_list, |s, v| s.contains(v))
844                }
845            }
846        })
847        .collect();
848
849    let count = filtered.len();
850    Ok(FilterOutput {
851        items: filtered,
852        count,
853        removed_count: original_count - count,
854    })
855}
856
857/// Sorts an array based on a property path
858#[capability(
859    module = "transform",
860    display_name = "Sort Array",
861    description = "Sort an array of items, optionally by a property path"
862)]
863pub fn sort(input: SortInput) -> Result<SortOutput, String> {
864    if input.value.is_empty() {
865        return Ok(SortOutput {
866            items: input.value,
867            count: 0,
868        });
869    }
870
871    let mut sorted_list = input.value;
872
873    sorted_list.sort_by(|a, b| {
874        let value1 = if let Some(ref path) = input.property_path {
875            get_property_value(a, path)
876        } else {
877            a.clone()
878        };
879
880        let value2 = if let Some(ref path) = input.property_path {
881            get_property_value(b, path)
882        } else {
883            b.clone()
884        };
885
886        let cmp = compare_values(&value1, &value2);
887
888        if input.ascending { cmp } else { cmp.reverse() }
889    });
890
891    let count = sorted_list.len();
892    Ok(SortOutput {
893        items: sorted_list,
894        count,
895    })
896}
897
898/// Maps fields from source object to target object based on mappings
899#[capability(
900    module = "transform",
901    display_name = "Map Fields",
902    description = "Map fields from a source object to a target object using field path mappings"
903)]
904pub fn map_fields(input: MapFieldsInput) -> Result<MapFieldsOutput, String> {
905    let mut result = HashMap::new();
906
907    for (source_field, target_field) in input.mappings {
908        let value = get_property_value(&input.source_data, &source_field);
909        if !value.is_null() {
910            result.insert(target_field, value);
911        }
912    }
913
914    let field_count = result.len();
915    Ok(MapFieldsOutput {
916        result,
917        field_count,
918    })
919}
920
921/// Groups an array of items by a specified key (JSONPath)
922/// Returns either a Map<key, Vec<items>> or Array<Vec<items>> based on as_map flag
923#[capability(
924    module = "transform",
925    display_name = "Group By",
926    description = "Group array items by a property key, returning either a map or array of groups"
927)]
928pub fn group_by(input: GroupByInput) -> Result<GroupByOutput, String> {
929    // Validate input - only arrays are supported
930    let collection = match &input.value {
931        Value::Array(arr) => arr,
932        Value::Null => {
933            return Err("Unsupported value. Expected array or collection.".to_string());
934        }
935        _ => {
936            return Err("Unsupported value. Expected array or collection.".to_string());
937        }
938    };
939
940    // Return empty result for empty arrays
941    if collection.is_empty() {
942        return Ok(GroupByOutput {
943            groups: if input.as_map {
944                Value::Object(serde_json::Map::new())
945            } else {
946                Value::Array(vec![])
947            },
948            group_count: 0,
949        });
950    }
951
952    // Prepare the JSON path (add $. prefix if not present)
953    let json_path = if input.key.starts_with("$.") {
954        input.key.clone()
955    } else {
956        format!("$.{}", input.key)
957    };
958
959    // Group items by the value at the specified JSON path
960    // Use a HashMap with string keys (since JSON object keys must be strings)
961    let mut grouped: HashMap<String, Vec<Value>> = HashMap::new();
962
963    for item in collection {
964        // Extract the key value using JsonPath
965        let key_value = get_property_value(item, &json_path);
966
967        // Skip items where the key value is null
968        if key_value.is_null() {
969            continue;
970        }
971
972        // Convert key to string for HashMap key
973        let key_str = match &key_value {
974            Value::String(s) => s.clone(),
975            Value::Number(n) => n.to_string(),
976            Value::Bool(b) => b.to_string(),
977            _ => serde_json::to_string(&key_value).map_err(|e| e.to_string())?,
978        };
979
980        grouped
981            .entry(key_str)
982            .or_insert_with(Vec::new)
983            .push(item.clone());
984    }
985
986    // Return based on as_map flag
987    let group_count = grouped.len();
988    if input.as_map {
989        // Convert HashMap to JSON Object
990        let mut map = serde_json::Map::new();
991        for (key, values) in grouped {
992            map.insert(key, Value::Array(values));
993        }
994        Ok(GroupByOutput {
995            groups: Value::Object(map),
996            group_count,
997        })
998    } else {
999        // Return just the values (array of arrays)
1000        let arrays: Vec<Value> = grouped.into_values().map(Value::Array).collect();
1001        Ok(GroupByOutput {
1002            groups: Value::Array(arrays),
1003            group_count,
1004        })
1005    }
1006}
1007
1008/// Appends an item to an array
1009/// Returns a new array with the item appended to the end
1010#[capability(
1011    module = "transform",
1012    display_name = "Append",
1013    description = "Append an item to the end of an array"
1014)]
1015pub fn append(input: AppendInput) -> Result<AppendOutput, String> {
1016    let mut array = input.array;
1017    array.push(input.item);
1018    let length = array.len();
1019    Ok(AppendOutput { array, length })
1020}
1021
1022/// Extracts nested arrays from each item and flattens them into a single array
1023#[capability(
1024    module = "transform",
1025    display_name = "Flat Map",
1026    description = "Extract nested arrays from each item by property path and flatten into a single array"
1027)]
1028pub fn flat_map(input: FlatMapInput) -> Result<FlatMapOutput, String> {
1029    if input.value.is_empty() || input.property_path.is_empty() {
1030        return Ok(FlatMapOutput {
1031            items: vec![],
1032            count: 0,
1033        });
1034    }
1035
1036    let mut items = Vec::new();
1037
1038    for item in input.value {
1039        let nested = get_property_value(&item, &input.property_path);
1040        if let Some(arr) = nested.as_array() {
1041            items.extend(arr.iter().cloned());
1042        }
1043    }
1044
1045    let count = items.len();
1046    Ok(FlatMapOutput { items, count })
1047}
1048
1049/// Gets the length/size of an array, string, or object
1050#[capability(
1051    module = "transform",
1052    display_name = "Array Length",
1053    description = "Get the length of an array, string, or number of keys in an object"
1054)]
1055pub fn array_length(input: ArrayLengthInput) -> Result<ArrayLengthOutput, String> {
1056    let (length, is_array) = match &input.value {
1057        Value::Array(arr) => (arr.len(), true),
1058        Value::String(s) => (s.len(), false),
1059        Value::Object(obj) => (obj.len(), false),
1060        Value::Null => (0, false),
1061        _ => (0, false),
1062    };
1063
1064    Ok(ArrayLengthOutput { length, is_array })
1065}
1066
1067// ============================================================================
1068// Helper Functions
1069// ============================================================================
1070
1071/// Gets a property value from a JSON object using a JSONPath-like syntax
1072fn get_property_value(obj: &Value, property_path: &str) -> Value {
1073    if property_path.is_empty() {
1074        return obj.clone();
1075    }
1076
1077    let path = property_path.strip_prefix("$.").unwrap_or(property_path);
1078    let parts: Vec<&str> = path.split('.').collect();
1079
1080    let mut current = obj;
1081    for part in parts {
1082        current = match current {
1083            Value::Object(map) => map.get(part).unwrap_or(&Value::Null),
1084            Value::Array(arr) => {
1085                // Try to parse as array index
1086                if let Ok(index) = part.parse::<usize>() {
1087                    arr.get(index).unwrap_or(&Value::Null)
1088                } else {
1089                    &Value::Null
1090                }
1091            }
1092            _ => &Value::Null,
1093        };
1094
1095        if current.is_null() {
1096            return Value::Null;
1097        }
1098    }
1099
1100    current.clone()
1101}
1102
1103/// Sets a property value in a JSON object using a JSONPath-like syntax
1104fn set_property_value(obj: Value, property_path: &str, value: Value) -> Value {
1105    if property_path.is_empty() {
1106        return obj;
1107    }
1108
1109    let path = property_path.strip_prefix("$.").unwrap_or(property_path);
1110    let parts: Vec<&str> = path.split('.').collect();
1111
1112    if let Value::Object(mut map) = obj {
1113        if parts.len() == 1 {
1114            // Simple property - just set it
1115            map.insert(parts[0].to_string(), value);
1116            return Value::Object(map);
1117        } else {
1118            // Nested path - navigate or create intermediate maps
1119            let mut current_map = map.clone();
1120            set_nested_value(&mut current_map, &parts, value);
1121            return Value::Object(current_map);
1122        }
1123    }
1124
1125    obj
1126}
1127
1128fn set_nested_value(map: &mut serde_json::Map<String, Value>, parts: &[&str], value: Value) {
1129    if parts.is_empty() {
1130        return;
1131    }
1132
1133    if parts.len() == 1 {
1134        map.insert(parts[0].to_string(), value);
1135        return;
1136    }
1137
1138    let key = parts[0];
1139    let rest = &parts[1..];
1140
1141    let next = map
1142        .entry(key.to_string())
1143        .or_insert_with(|| Value::Object(serde_json::Map::new()));
1144
1145    if let Value::Object(nested_map) = next {
1146        set_nested_value(nested_map, rest, value);
1147    }
1148}
1149
1150/// Checks if a property value matches any of the filter values
1151fn matches_filter_values(property_value: &Value, filter_values: &[Value]) -> bool {
1152    if property_value.is_null() {
1153        return false;
1154    }
1155
1156    // If property value is an array, check if any element matches
1157    if let Some(arr) = property_value.as_array() {
1158        for element in arr {
1159            if filter_values.contains(element) {
1160                return true;
1161            }
1162        }
1163        return false;
1164    }
1165
1166    // Direct match
1167    filter_values.contains(property_value)
1168}
1169
1170/// Matches a string value against filter values using a custom comparison function
1171fn matches_string_filter<F>(property_value: &Value, filter_values: &[Value], compare: F) -> bool
1172where
1173    F: Fn(&str, &str) -> bool,
1174{
1175    // Get property value as string
1176    let property_str = match property_value {
1177        Value::String(s) => s.as_str(),
1178        _ => return false,
1179    };
1180
1181    // Check if any filter value matches
1182    for filter_value in filter_values {
1183        let filter_str = match filter_value {
1184            Value::String(s) => s.as_str(),
1185            _ => continue,
1186        };
1187        if compare(property_str, filter_str) {
1188            return true;
1189        }
1190    }
1191    false
1192}
1193
1194/// Compares two JSON values for sorting
1195fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
1196    use std::cmp::Ordering;
1197
1198    match (a, b) {
1199        (Value::Null, Value::Null) => Ordering::Equal,
1200        (Value::Null, _) => Ordering::Greater, // Nulls go to the end
1201        (_, Value::Null) => Ordering::Less,
1202
1203        // Compare numbers
1204        (Value::Number(n1), Value::Number(n2)) => {
1205            let f1 = n1.as_f64().unwrap_or(0.0);
1206            let f2 = n2.as_f64().unwrap_or(0.0);
1207            f1.partial_cmp(&f2).unwrap_or(Ordering::Equal)
1208        }
1209
1210        // Compare strings
1211        (Value::String(s1), Value::String(s2)) => s1.cmp(s2),
1212
1213        // Compare booleans
1214        (Value::Bool(b1), Value::Bool(b2)) => b1.cmp(b2),
1215
1216        // Mixed types - convert to string and compare
1217        _ => a.to_string().cmp(&b.to_string()),
1218    }
1219}
1220
1221// ============================================================================
1222// Tests
1223// ============================================================================
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228    use serde_json::json;
1229
1230    #[test]
1231    fn test_extract() {
1232        let input = ExtractInput {
1233            value: vec![
1234                json!({"name": "Alice", "age": 30}),
1235                json!({"name": "Bob", "age": 25}),
1236            ],
1237            property_path: "name".to_string(),
1238        };
1239
1240        let result = extract(input).unwrap();
1241        assert_eq!(result.values, vec![json!("Alice"), json!("Bob")]);
1242        assert_eq!(result.count, 2);
1243    }
1244
1245    #[test]
1246    fn test_get_value_by_path() {
1247        let input = GetValueByPathInput {
1248            value: Some(json!({"user": {"name": "Alice", "age": 30}})),
1249            property_path: Some("user.name".to_string()),
1250        };
1251
1252        let result = get_value_by_path(input).unwrap();
1253        assert_eq!(result, json!("Alice"));
1254    }
1255
1256    #[test]
1257    fn test_get_value_by_path_null() {
1258        let input = GetValueByPathInput {
1259            value: None,
1260            property_path: Some("user.name".to_string()),
1261        };
1262
1263        let result = get_value_by_path(input).unwrap();
1264        assert_eq!(result, Value::Null);
1265    }
1266
1267    #[test]
1268    fn test_set_value_by_path() {
1269        let input = SetValueByPathInput {
1270            target: Some(json!({"user": {"name": "Alice"}})),
1271            property_path: Some("user.age".to_string()),
1272            value: Some(json!(30)),
1273        };
1274
1275        let result = set_value_by_path(input).unwrap();
1276        assert_eq!(result, json!({"user": {"name": "Alice", "age": 30}}));
1277    }
1278
1279    #[test]
1280    fn test_filter_non_values_null() {
1281        let input = FilterNoValueInput {
1282            value: vec![json!({"x": 1}), json!({"x": null}), json!({"x": 2})],
1283            property_path: Some("x".to_string()),
1284            filter_empty_strings: false,
1285            filter_null_values: true,
1286            filter_blank_strings: false,
1287            filter_zero_values: false,
1288        };
1289
1290        let result = filter_non_values(input).unwrap();
1291        assert_eq!(result.count, 2);
1292        assert_eq!(result.items.len(), 2);
1293    }
1294
1295    #[test]
1296    fn test_filter_non_values_empty_strings() {
1297        let input = FilterNoValueInput {
1298            value: vec![
1299                json!({"x": "hello"}),
1300                json!({"x": ""}),
1301                json!({"x": "world"}),
1302            ],
1303            property_path: Some("x".to_string()),
1304            filter_empty_strings: true,
1305            filter_null_values: false,
1306            filter_blank_strings: false,
1307            filter_zero_values: false,
1308        };
1309
1310        let result = filter_non_values(input).unwrap();
1311        assert_eq!(result.count, 2);
1312        assert_eq!(result.items.len(), 2);
1313    }
1314
1315    #[test]
1316    fn test_select_first() {
1317        let input = SelectFirstInput {
1318            value: Some(vec![json!(null), json!(""), json!(0), json!("hello")]),
1319        };
1320
1321        let result = select_first(input).unwrap();
1322        assert_eq!(result, json!("hello"));
1323    }
1324
1325    #[test]
1326    fn test_from_json_string() {
1327        let input = FromJsonStringInput {
1328            value: Some(r#"{"name":"Alice","age":30}"#.to_string()),
1329        };
1330
1331        let result = from_json_string(input).unwrap();
1332        assert_eq!(result, json!({"name": "Alice", "age": 30}));
1333    }
1334
1335    #[test]
1336    fn test_to_json_string() {
1337        let input = ToJsonStringInput {
1338            value: json!({"name": "Alice", "age": 30}),
1339        };
1340
1341        let result = to_json_string(input).unwrap();
1342        assert!(result.json.contains("Alice"));
1343        assert!(result.json.contains("30"));
1344        assert!(result.length > 0);
1345    }
1346
1347    #[test]
1348    fn test_filter_includes() {
1349        let input = FilterInput {
1350            value: vec![
1351                json!({"status": "active"}),
1352                json!({"status": "inactive"}),
1353                json!({"status": "pending"}),
1354            ],
1355            property_path: "status".to_string(),
1356            match_values: json!(["active", "pending"]),
1357            match_condition: MatchCondition::Includes,
1358        };
1359
1360        let result = filter(input).unwrap();
1361        assert_eq!(result.count, 2);
1362        assert_eq!(result.items.len(), 2);
1363    }
1364
1365    #[test]
1366    fn test_filter_excludes() {
1367        let input = FilterInput {
1368            value: vec![
1369                json!({"status": "active"}),
1370                json!({"status": "inactive"}),
1371                json!({"status": "pending"}),
1372            ],
1373            property_path: "status".to_string(),
1374            match_values: json!(["inactive"]),
1375            match_condition: MatchCondition::Excludes,
1376        };
1377
1378        let result = filter(input).unwrap();
1379        assert_eq!(result.count, 2);
1380        assert_eq!(result.items.len(), 2);
1381    }
1382
1383    #[test]
1384    fn test_filter_primitive_array_with_dollar_sign() {
1385        let input = FilterInput {
1386            value: vec![json!("active"), json!("pending"), json!("inactive")],
1387            property_path: "$".to_string(),
1388            match_values: json!(["active"]),
1389            match_condition: MatchCondition::Excludes,
1390        };
1391
1392        let result = filter(input).unwrap();
1393        assert_eq!(result.count, 2);
1394        assert_eq!(result.items, vec![json!("pending"), json!("inactive")]);
1395    }
1396
1397    #[test]
1398    fn test_filter_primitive_array_with_empty_string() {
1399        let input = FilterInput {
1400            value: vec![json!("active"), json!("pending"), json!("inactive")],
1401            property_path: "".to_string(),
1402            match_values: json!(["active"]),
1403            match_condition: MatchCondition::Excludes,
1404        };
1405
1406        let result = filter(input).unwrap();
1407        assert_eq!(result.count, 2);
1408        assert_eq!(result.items, vec![json!("pending"), json!("inactive")]);
1409    }
1410
1411    #[test]
1412    fn test_filter_primitive_array_includes() {
1413        let input = FilterInput {
1414            value: vec![
1415                json!("active"),
1416                json!("pending"),
1417                json!("inactive"),
1418                json!("archived"),
1419            ],
1420            property_path: "$".to_string(),
1421            match_values: json!(["active", "pending"]),
1422            match_condition: MatchCondition::Includes,
1423        };
1424
1425        let result = filter(input).unwrap();
1426        assert_eq!(result.count, 2);
1427        assert_eq!(result.items, vec![json!("active"), json!("pending")]);
1428    }
1429
1430    #[test]
1431    fn test_filter_primitive_numbers() {
1432        let input = FilterInput {
1433            value: vec![json!(1), json!(2), json!(3), json!(4), json!(5)],
1434            property_path: "$".to_string(),
1435            match_values: json!([2, 4]),
1436            match_condition: MatchCondition::Excludes,
1437        };
1438
1439        let result = filter(input).unwrap();
1440        assert_eq!(result.count, 3);
1441        assert_eq!(result.items, vec![json!(1), json!(3), json!(5)]);
1442    }
1443
1444    #[test]
1445    fn test_sort_ascending() {
1446        let input = SortInput {
1447            value: vec![
1448                json!({"name": "Charlie", "age": 35}),
1449                json!({"name": "Alice", "age": 30}),
1450                json!({"name": "Bob", "age": 25}),
1451            ],
1452            property_path: Some("age".to_string()),
1453            ascending: true,
1454        };
1455
1456        let result = sort(input).unwrap();
1457        assert_eq!(result.count, 3);
1458        assert_eq!(result.items[0], json!({"name": "Bob", "age": 25}));
1459        assert_eq!(result.items[2], json!({"name": "Charlie", "age": 35}));
1460    }
1461
1462    #[test]
1463    fn test_sort_descending() {
1464        let input = SortInput {
1465            value: vec![json!(3), json!(1), json!(2)],
1466            property_path: None,
1467            ascending: false,
1468        };
1469
1470        let result = sort(input).unwrap();
1471        assert_eq!(result.items, vec![json!(3), json!(2), json!(1)]);
1472        assert_eq!(result.count, 3);
1473    }
1474
1475    #[test]
1476    fn test_map_fields() {
1477        let mut mappings = HashMap::new();
1478        mappings.insert("firstName".to_string(), "name".to_string());
1479        mappings.insert("userAge".to_string(), "age".to_string());
1480
1481        let input = MapFieldsInput {
1482            source_data: json!({"firstName": "Alice", "userAge": 30, "email": "alice@example.com"}),
1483            mappings,
1484        };
1485
1486        let result = map_fields(input).unwrap();
1487        assert_eq!(result.result.get("name"), Some(&json!("Alice")));
1488        assert_eq!(result.result.get("age"), Some(&json!(30)));
1489        assert_eq!(result.result.get("email"), None);
1490        assert_eq!(result.field_count, 2);
1491    }
1492
1493    #[test]
1494    fn test_group_by_as_map() {
1495        let input = GroupByInput {
1496            value: json!([
1497                {"name": "Alice", "status": "active"},
1498                {"name": "Bob", "status": "inactive"},
1499                {"name": "Charlie", "status": "active"},
1500                {"name": "David", "status": "pending"}
1501            ]),
1502            key: "status".to_string(),
1503            as_map: true,
1504        };
1505
1506        let result = group_by(input).unwrap();
1507        assert!(result.groups.is_object());
1508        assert_eq!(result.group_count, 3);
1509
1510        let obj = result.groups.as_object().unwrap();
1511        assert_eq!(obj.get("active").unwrap().as_array().unwrap().len(), 2);
1512        assert_eq!(obj.get("inactive").unwrap().as_array().unwrap().len(), 1);
1513        assert_eq!(obj.get("pending").unwrap().as_array().unwrap().len(), 1);
1514    }
1515
1516    #[test]
1517    fn test_group_by_as_array() {
1518        let input = GroupByInput {
1519            value: json!([
1520                {"name": "Alice", "status": "active"},
1521                {"name": "Bob", "status": "inactive"},
1522                {"name": "Charlie", "status": "active"}
1523            ]),
1524            key: "status".to_string(),
1525            as_map: false,
1526        };
1527
1528        let result = group_by(input).unwrap();
1529        assert!(result.groups.is_array());
1530        assert_eq!(result.group_count, 2);
1531
1532        let arr = result.groups.as_array().unwrap();
1533        assert_eq!(arr.len(), 2); // Two groups: active and inactive
1534
1535        // Check that we have arrays of expected sizes (2 active, 1 inactive)
1536        let sizes: Vec<usize> = arr.iter().map(|v| v.as_array().unwrap().len()).collect();
1537        assert!(sizes.contains(&2));
1538        assert!(sizes.contains(&1));
1539    }
1540
1541    #[test]
1542    fn test_group_by_nested_path() {
1543        let input = GroupByInput {
1544            value: json!([
1545                {"name": "Alice", "details": {"category": "A"}},
1546                {"name": "Bob", "details": {"category": "B"}},
1547                {"name": "Charlie", "details": {"category": "A"}}
1548            ]),
1549            key: "details.category".to_string(),
1550            as_map: true,
1551        };
1552
1553        let result = group_by(input).unwrap();
1554        assert_eq!(result.group_count, 2);
1555        let obj = result.groups.as_object().unwrap();
1556        assert_eq!(obj.get("A").unwrap().as_array().unwrap().len(), 2);
1557        assert_eq!(obj.get("B").unwrap().as_array().unwrap().len(), 1);
1558    }
1559
1560    #[test]
1561    fn test_group_by_skip_null_keys() {
1562        let input = GroupByInput {
1563            value: json!([
1564                {"name": "Alice", "status": "active"},
1565                {"name": "Bob"},  // No status field
1566                {"name": "Charlie", "status": null},  // Null status
1567                {"name": "David", "status": "active"}
1568            ]),
1569            key: "status".to_string(),
1570            as_map: true,
1571        };
1572
1573        let result = group_by(input).unwrap();
1574        assert_eq!(result.group_count, 1);
1575        let obj = result.groups.as_object().unwrap();
1576
1577        // Only items with non-null status should be grouped
1578        assert_eq!(obj.get("active").unwrap().as_array().unwrap().len(), 2);
1579        assert_eq!(obj.len(), 1); // Only "active" group exists
1580    }
1581
1582    #[test]
1583    fn test_group_by_empty_array() {
1584        let input = GroupByInput {
1585            value: json!([]),
1586            key: "status".to_string(),
1587            as_map: true,
1588        };
1589
1590        let result = group_by(input).unwrap();
1591        assert!(result.groups.is_object());
1592        assert_eq!(result.group_count, 0);
1593        assert_eq!(result.groups.as_object().unwrap().len(), 0);
1594    }
1595
1596    #[test]
1597    fn test_group_by_numeric_keys() {
1598        let input = GroupByInput {
1599            value: json!([
1600                {"name": "Alice", "age": 30},
1601                {"name": "Bob", "age": 25},
1602                {"name": "Charlie", "age": 30}
1603            ]),
1604            key: "age".to_string(),
1605            as_map: true,
1606        };
1607
1608        let result = group_by(input).unwrap();
1609        assert_eq!(result.group_count, 2);
1610        let obj = result.groups.as_object().unwrap();
1611
1612        // Numeric keys are converted to strings
1613        assert_eq!(obj.get("30").unwrap().as_array().unwrap().len(), 2);
1614        assert_eq!(obj.get("25").unwrap().as_array().unwrap().len(), 1);
1615    }
1616
1617    #[test]
1618    fn test_group_by_invalid_input() {
1619        let input = GroupByInput {
1620            value: json!({"not": "an array"}),
1621            key: "status".to_string(),
1622            as_map: true,
1623        };
1624
1625        let result = group_by(input);
1626        assert!(result.is_err());
1627        assert!(result.unwrap_err().contains("Expected array"));
1628    }
1629
1630    #[test]
1631    fn test_append_object_to_array() {
1632        let input = AppendInput {
1633            array: vec![
1634                json!({"name": "Alice", "age": 30}),
1635                json!({"name": "Bob", "age": 25}),
1636            ],
1637            item: json!({"name": "Charlie", "age": 35}),
1638        };
1639
1640        let result = append(input).unwrap();
1641        assert_eq!(result.length, 3);
1642        assert_eq!(result.array[2], json!({"name": "Charlie", "age": 35}));
1643    }
1644
1645    #[test]
1646    fn test_append_string_to_array() {
1647        let input = AppendInput {
1648            array: vec![json!("apple"), json!("banana")],
1649            item: json!("cherry"),
1650        };
1651
1652        let result = append(input).unwrap();
1653        assert_eq!(result.length, 3);
1654        assert_eq!(result.array[2], json!("cherry"));
1655    }
1656
1657    #[test]
1658    fn test_append_number_to_array() {
1659        let input = AppendInput {
1660            array: vec![json!(1), json!(2), json!(3)],
1661            item: json!(4),
1662        };
1663
1664        let result = append(input).unwrap();
1665        assert_eq!(result.length, 4);
1666        assert_eq!(result.array[3], json!(4));
1667    }
1668
1669    #[test]
1670    fn test_append_to_empty_array() {
1671        let input = AppendInput {
1672            array: vec![],
1673            item: json!({"name": "Alice"}),
1674        };
1675
1676        let result = append(input).unwrap();
1677        assert_eq!(result.length, 1);
1678        assert_eq!(result.array[0], json!({"name": "Alice"}));
1679    }
1680
1681    #[test]
1682    fn test_append_null_to_array() {
1683        let input = AppendInput {
1684            array: vec![json!(1), json!(2)],
1685            item: json!(null),
1686        };
1687
1688        let result = append(input).unwrap();
1689        assert_eq!(result.length, 3);
1690        assert_eq!(result.array[2], json!(null));
1691    }
1692
1693    #[test]
1694    fn test_append_array_to_array() {
1695        let input = AppendInput {
1696            array: vec![json!([1, 2]), json!([3, 4])],
1697            item: json!([5, 6]),
1698        };
1699
1700        let result = append(input).unwrap();
1701        assert_eq!(result.length, 3);
1702        assert_eq!(result.array[2], json!([5, 6]));
1703    }
1704
1705    #[test]
1706    fn test_append_mixed_types() {
1707        let input = AppendInput {
1708            array: vec![json!(1), json!("string"), json!({"key": "value"})],
1709            item: json!(true),
1710        };
1711
1712        let result = append(input).unwrap();
1713        assert_eq!(result.length, 4);
1714        assert_eq!(result.array[0], json!(1));
1715        assert_eq!(result.array[1], json!("string"));
1716        assert_eq!(result.array[2], json!({"key": "value"}));
1717        assert_eq!(result.array[3], json!(true));
1718    }
1719
1720    #[test]
1721    fn test_flat_map_basic() {
1722        let input = FlatMapInput {
1723            value: vec![
1724                json!({"items": [1, 2, 3]}),
1725                json!({"items": [4, 5]}),
1726                json!({"items": [6]}),
1727            ],
1728            property_path: "items".to_string(),
1729        };
1730
1731        let result = flat_map(input).unwrap();
1732        assert_eq!(result.count, 6);
1733        assert_eq!(
1734            result.items,
1735            vec![json!(1), json!(2), json!(3), json!(4), json!(5), json!(6)]
1736        );
1737    }
1738
1739    #[test]
1740    fn test_flat_map_objects() {
1741        let input = FlatMapInput {
1742            value: vec![
1743                json!({"records": [{"action": "created"}, {"action": "updated"}]}),
1744                json!({"records": [{"action": "created"}]}),
1745            ],
1746            property_path: "records".to_string(),
1747        };
1748
1749        let result = flat_map(input).unwrap();
1750        assert_eq!(result.count, 3);
1751        assert_eq!(result.items.len(), 3);
1752    }
1753
1754    #[test]
1755    fn test_flat_map_missing_property() {
1756        let input = FlatMapInput {
1757            value: vec![
1758                json!({"items": [1, 2]}),
1759                json!({"other": [3, 4]}), // missing "items"
1760                json!({"items": [5]}),
1761            ],
1762            property_path: "items".to_string(),
1763        };
1764
1765        let result = flat_map(input).unwrap();
1766        assert_eq!(result.count, 3);
1767        assert_eq!(result.items, vec![json!(1), json!(2), json!(5)]);
1768    }
1769
1770    #[test]
1771    fn test_flat_map_empty() {
1772        let input = FlatMapInput {
1773            value: vec![],
1774            property_path: "items".to_string(),
1775        };
1776
1777        let result = flat_map(input).unwrap();
1778        assert_eq!(result.count, 0);
1779        assert!(result.items.is_empty());
1780    }
1781
1782    #[test]
1783    fn test_flat_map_nested_path() {
1784        let input = FlatMapInput {
1785            value: vec![
1786                json!({"data": {"items": [1, 2]}}),
1787                json!({"data": {"items": [3]}}),
1788            ],
1789            property_path: "data.items".to_string(),
1790        };
1791
1792        let result = flat_map(input).unwrap();
1793        assert_eq!(result.count, 3);
1794        assert_eq!(result.items, vec![json!(1), json!(2), json!(3)]);
1795    }
1796}