1use 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
21fn 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#[derive(Debug, Deserialize, CapabilityInput)]
36#[capability_input(display_name = "Extract Property Input")]
37pub struct ExtractInput {
38 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
57#[capability_input(display_name = "Get Value Input")]
58pub struct GetValueByPathInput {
59 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
78#[capability_input(display_name = "Set Value Input")]
79pub struct SetValueByPathInput {
80 #[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 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
107#[capability_input(display_name = "Filter Non-Values Input")]
108pub struct FilterNoValueInput {
109 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
169#[capability_input(display_name = "Select First Input")]
170pub struct SelectFirstInput {
171 #[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#[derive(Debug, Deserialize, CapabilityInput)]
182#[capability_input(display_name = "Parse JSON Input")]
183pub struct FromJsonStringInput {
184 #[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#[derive(Debug, Deserialize, CapabilityInput)]
195#[capability_input(display_name = "Stringify JSON Input")]
196pub struct ToJsonStringInput {
197 #[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#[derive(Debug, Deserialize, Clone, PartialEq, VariantNames)]
208#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
209#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
210pub enum MatchCondition {
211 Includes,
213 Excludes,
215 StartsWith,
217 EndsWith,
219 Contains,
221}
222
223impl EnumVariants for MatchCondition {
224 fn variant_names() -> &'static [&'static str] {
225 Self::VARIANTS
226 }
227}
228
229#[derive(Debug, Deserialize, CapabilityInput)]
231#[capability_input(display_name = "Filter Array Input")]
232pub struct FilterInput {
233 #[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 #[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 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
270#[capability_input(display_name = "Sort Array Input")]
271pub struct SortInput {
272 #[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 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
305#[capability_input(display_name = "Map Fields Input")]
306pub struct MapFieldsInput {
307 #[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(
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#[derive(Debug, Deserialize, CapabilityInput)]
326#[capability_input(display_name = "Group By Input")]
327pub struct GroupByInput {
328 #[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 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
357#[capability_input(display_name = "Append Input")]
358pub struct AppendInput {
359 #[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 #[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#[derive(Debug, Deserialize, CapabilityInput)]
378#[capability_input(display_name = "Flat Map Input")]
379pub struct FlatMapInput {
380 #[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 #[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#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
403#[capability_output(display_name = "Extract Output")]
404pub struct ExtractOutput {
405 #[field(
407 display_name = "Values",
408 description = "Array of extracted property values"
409 )]
410 pub values: Vec<Value>,
411
412 #[field(display_name = "Count", description = "Number of values extracted")]
414 pub count: usize,
415}
416
417#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
419#[capability_output(display_name = "Filter Output")]
420pub struct FilterOutput {
421 #[field(
423 display_name = "Items",
424 description = "Array of items that passed the filter"
425 )]
426 pub items: Vec<Value>,
427
428 #[field(
430 display_name = "Count",
431 description = "Number of items that passed the filter"
432 )]
433 pub count: usize,
434
435 #[field(
437 display_name = "Removed Count",
438 description = "Number of items removed by the filter"
439 )]
440 pub removed_count: usize,
441}
442
443#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
445#[capability_output(display_name = "Sort Output")]
446pub struct SortOutput {
447 #[field(display_name = "Items", description = "Array of sorted items")]
449 pub items: Vec<Value>,
450
451 #[field(
453 display_name = "Count",
454 description = "Number of items in the sorted array"
455 )]
456 pub count: usize,
457}
458
459#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
461#[capability_output(display_name = "Group By Output")]
462pub struct GroupByOutput {
463 #[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 #[field(
472 display_name = "Group Count",
473 description = "Number of unique groups created"
474 )]
475 pub group_count: usize,
476}
477
478#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
480#[capability_output(display_name = "Map Fields Output")]
481pub struct MapFieldsOutput {
482 #[field(
484 display_name = "Result",
485 description = "Object with mapped field values"
486 )]
487 pub result: HashMap<String, Value>,
488
489 #[field(
491 display_name = "Field Count",
492 description = "Number of fields successfully mapped"
493 )]
494 pub field_count: usize,
495}
496
497#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
499#[capability_output(display_name = "Append Output")]
500pub struct AppendOutput {
501 #[field(
503 display_name = "Array",
504 description = "Array with the new item appended"
505 )]
506 pub array: Vec<Value>,
507
508 #[field(
510 display_name = "Length",
511 description = "New length of the array after appending"
512 )]
513 pub length: usize,
514}
515
516#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
518#[capability_output(display_name = "Flat Map Output")]
519pub struct FlatMapOutput {
520 #[field(
522 display_name = "Items",
523 description = "Flattened array of all nested items"
524 )]
525 pub items: Vec<Value>,
526
527 #[field(
529 display_name = "Count",
530 description = "Total number of items in the flattened array"
531 )]
532 pub count: usize,
533}
534
535#[derive(Debug, Deserialize, CapabilityInput)]
537#[capability_input(display_name = "Array Length Input")]
538pub struct ArrayLengthInput {
539 #[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#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
550#[capability_output(display_name = "Array Length Output")]
551pub struct ArrayLengthOutput {
552 #[field(
554 display_name = "Length",
555 description = "Length of array/string, or number of keys in object"
556 )]
557 pub length: usize,
558
559 #[field(
561 display_name = "Is Array",
562 description = "True if the value was an array"
563 )]
564 pub is_array: bool,
565}
566
567#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
569#[capability_output(display_name = "JSON String Output")]
570pub struct ToJsonStringOutput {
571 #[field(display_name = "JSON", description = "The serialized JSON string")]
573 pub json: String,
574
575 #[field(
577 display_name = "Length",
578 description = "Length of the JSON string in characters"
579 )]
580 pub length: usize,
581}
582
583#[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#[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#[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#[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 if input.filter_null_values && property_value.is_null() {
670 return false;
671 }
672
673 if input.filter_empty_strings
675 && let Some(s) = property_value.as_str()
676 && s.is_empty()
677 {
678 return false;
679 }
680
681 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 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#[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 if item.is_null() {
732 continue;
733 }
734
735 if let Some(s) = item.as_str()
737 && (s.is_empty() || s.trim().is_empty())
738 {
739 continue;
740 }
741
742 if let Some(n) = item.as_f64()
744 && n == 0.0
745 {
746 continue;
747 }
748
749 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#[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#[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#[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 let match_values_list: Vec<Value> = match input.match_values {
808 Value::Array(arr) => arr,
809 other => vec![other],
810 };
811
812 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#[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#[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#[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 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 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 let json_path = if input.key.starts_with("$.") {
954 input.key.clone()
955 } else {
956 format!("$.{}", input.key)
957 };
958
959 let mut grouped: HashMap<String, Vec<Value>> = HashMap::new();
962
963 for item in collection {
964 let key_value = get_property_value(item, &json_path);
966
967 if key_value.is_null() {
969 continue;
970 }
971
972 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 let group_count = grouped.len();
988 if input.as_map {
989 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 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#[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#[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#[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
1067fn 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 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
1103fn 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 map.insert(parts[0].to_string(), value);
1116 return Value::Object(map);
1117 } else {
1118 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
1150fn matches_filter_values(property_value: &Value, filter_values: &[Value]) -> bool {
1152 if property_value.is_null() {
1153 return false;
1154 }
1155
1156 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 filter_values.contains(property_value)
1168}
1169
1170fn matches_string_filter<F>(property_value: &Value, filter_values: &[Value], compare: F) -> bool
1172where
1173 F: Fn(&str, &str) -> bool,
1174{
1175 let property_str = match property_value {
1177 Value::String(s) => s.as_str(),
1178 _ => return false,
1179 };
1180
1181 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
1194fn 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, (_, Value::Null) => Ordering::Less,
1202
1203 (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 (Value::String(s1), Value::String(s2)) => s1.cmp(s2),
1212
1213 (Value::Bool(b1), Value::Bool(b2)) => b1.cmp(b2),
1215
1216 _ => a.to_string().cmp(&b.to_string()),
1218 }
1219}
1220
1221#[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); 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"}, {"name": "Charlie", "status": null}, {"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 assert_eq!(obj.get("active").unwrap().as_array().unwrap().len(), 2);
1579 assert_eq!(obj.len(), 1); }
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 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]}), 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}