1use std::collections::HashMap;
7
8use chrono::{NaiveDateTime, Timelike, Datelike};
9use regex::Regex;
10use serde_json::Value;
11
12use crate::catalog::function_api::{FunctionImplementation, ReturnType};
13use crate::error::A2uiError;
14use crate::model::data_context::DataContext;
15
16fn require_str<'a>(
22 args: &'a HashMap<String, Value>,
23 key: &str,
24 func_name: &str,
25) -> std::result::Result<&'a str, A2uiError> {
26 args.get(key)
27 .and_then(|v| v.as_str())
28 .ok_or_else(|| {
29 A2uiError::InvalidFunctionCall(format!(
30 "{func_name}: missing or non-string argument '{key}'"
31 ))
32 })
33}
34
35fn opt_f64(args: &HashMap<String, Value>, key: &str) -> Option<f64> {
37 args.get(key).and_then(|v| v.as_f64())
38}
39
40fn opt_bool(args: &HashMap<String, Value>, key: &str) -> Option<bool> {
42 args.get(key).and_then(|v| v.as_bool())
43}
44
45pub struct RequiredFunction;
52
53impl FunctionImplementation for RequiredFunction {
54 fn name(&self) -> &'static str {
55 "required"
56 }
57
58 fn return_type(&self) -> ReturnType {
59 ReturnType::Boolean
60 }
61
62 fn execute(
63 &self,
64 args: &HashMap<String, Value>,
65 _context: &DataContext,
66 ) -> Result<Value, A2uiError> {
67 let val = args.get("value").cloned().unwrap_or(Value::Null);
68 let present = match &val {
69 Value::Null => false,
70 Value::String(s) => !s.is_empty(),
71 Value::Array(arr) => !arr.is_empty(),
72 _ => true,
73 };
74 Ok(Value::Bool(present))
75 }
76}
77
78pub struct RegexFunction;
84
85impl FunctionImplementation for RegexFunction {
86 fn name(&self) -> &'static str {
87 "regex"
88 }
89
90 fn return_type(&self) -> ReturnType {
91 ReturnType::Boolean
92 }
93
94 fn execute(
95 &self,
96 args: &HashMap<String, Value>,
97 _context: &DataContext,
98 ) -> Result<Value, A2uiError> {
99 let value = require_str(args, "value", "regex")?;
100 let pattern = require_str(args, "pattern", "regex")?;
101
102 let re = Regex::new(pattern).map_err(|e| {
103 A2uiError::InvalidFunctionCall(format!("regex: invalid pattern '{pattern}': {e}"))
104 })?;
105
106 Ok(Value::Bool(re.is_match(value)))
107 }
108}
109
110pub struct LengthFunction;
116
117impl FunctionImplementation for LengthFunction {
118 fn name(&self) -> &'static str {
119 "length"
120 }
121
122 fn return_type(&self) -> ReturnType {
123 ReturnType::Boolean
124 }
125
126 fn execute(
127 &self,
128 args: &HashMap<String, Value>,
129 _context: &DataContext,
130 ) -> Result<Value, A2uiError> {
131 let value = require_str(args, "value", "length")?;
132 let len = value.chars().count() as f64;
133
134 if let Some(min) = opt_f64(args, "min") {
135 if len < min {
136 return Ok(Value::Bool(false));
137 }
138 }
139 if let Some(max) = opt_f64(args, "max") {
140 if len > max {
141 return Ok(Value::Bool(false));
142 }
143 }
144 Ok(Value::Bool(true))
145 }
146}
147
148pub struct NumericFunction;
155
156impl FunctionImplementation for NumericFunction {
157 fn name(&self) -> &'static str {
158 "numeric"
159 }
160
161 fn return_type(&self) -> ReturnType {
162 ReturnType::Boolean
163 }
164
165 fn execute(
166 &self,
167 args: &HashMap<String, Value>,
168 _context: &DataContext,
169 ) -> Result<Value, A2uiError> {
170 let val = args.get("value").cloned().unwrap_or(Value::Null);
171
172 let num = match &val {
174 Value::Number(n) => n.as_f64(),
175 Value::String(s) => s.parse::<f64>().ok(),
176 _ => None,
177 };
178
179 let Some(n) = num else {
180 return Ok(Value::Bool(false));
181 };
182
183 if let Some(min) = opt_f64(args, "min") {
184 if n < min {
185 return Ok(Value::Bool(false));
186 }
187 }
188 if let Some(max) = opt_f64(args, "max") {
189 if n > max {
190 return Ok(Value::Bool(false));
191 }
192 }
193 Ok(Value::Bool(true))
194 }
195}
196
197pub struct EmailFunction;
203
204impl FunctionImplementation for EmailFunction {
205 fn name(&self) -> &'static str {
206 "email"
207 }
208
209 fn return_type(&self) -> ReturnType {
210 ReturnType::Boolean
211 }
212
213 fn execute(
214 &self,
215 args: &HashMap<String, Value>,
216 _context: &DataContext,
217 ) -> Result<Value, A2uiError> {
218 let value = require_str(args, "value", "email")?;
219
220 let re = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap();
222 Ok(Value::Bool(re.is_match(value)))
223 }
224}
225
226pub struct AndFunction;
232
233impl FunctionImplementation for AndFunction {
234 fn name(&self) -> &'static str {
235 "and"
236 }
237
238 fn return_type(&self) -> ReturnType {
239 ReturnType::Boolean
240 }
241
242 fn execute(
243 &self,
244 args: &HashMap<String, Value>,
245 _context: &DataContext,
246 ) -> Result<Value, A2uiError> {
247 let arr = args
248 .get("values")
249 .and_then(|v| v.as_array())
250 .ok_or_else(|| {
251 A2uiError::InvalidFunctionCall("and: missing or non-array argument 'values'".into())
252 })?;
253
254 let all_true = arr.iter().all(|v| v.as_bool().unwrap_or(false));
255 Ok(Value::Bool(all_true))
256 }
257}
258
259pub struct OrFunction;
265
266impl FunctionImplementation for OrFunction {
267 fn name(&self) -> &'static str {
268 "or"
269 }
270
271 fn return_type(&self) -> ReturnType {
272 ReturnType::Boolean
273 }
274
275 fn execute(
276 &self,
277 args: &HashMap<String, Value>,
278 _context: &DataContext,
279 ) -> Result<Value, A2uiError> {
280 let arr = args
281 .get("values")
282 .and_then(|v| v.as_array())
283 .ok_or_else(|| {
284 A2uiError::InvalidFunctionCall("or: missing or non-array argument 'values'".into())
285 })?;
286
287 let any_true = arr.iter().any(|v| v.as_bool().unwrap_or(false));
288 Ok(Value::Bool(any_true))
289 }
290}
291
292pub struct NotFunction;
298
299impl FunctionImplementation for NotFunction {
300 fn name(&self) -> &'static str {
301 "not"
302 }
303
304 fn return_type(&self) -> ReturnType {
305 ReturnType::Boolean
306 }
307
308 fn execute(
309 &self,
310 args: &HashMap<String, Value>,
311 _context: &DataContext,
312 ) -> Result<Value, A2uiError> {
313 let val = args
314 .get("value")
315 .and_then(|v| v.as_bool())
316 .ok_or_else(|| {
317 A2uiError::InvalidFunctionCall(
318 "not: missing or non-boolean argument 'value'".into(),
319 )
320 })?;
321
322 Ok(Value::Bool(!val))
323 }
324}
325
326pub struct FormatNumberFunction;
332
333impl FunctionImplementation for FormatNumberFunction {
334 fn name(&self) -> &'static str {
335 "formatNumber"
336 }
337
338 fn return_type(&self) -> ReturnType {
339 ReturnType::String
340 }
341
342 fn execute(
343 &self,
344 args: &HashMap<String, Value>,
345 _context: &DataContext,
346 ) -> Result<Value, A2uiError> {
347 let val = args
348 .get("value")
349 .and_then(|v| v.as_f64())
350 .ok_or_else(|| {
351 A2uiError::InvalidFunctionCall(
352 "formatNumber: missing or non-numeric argument 'value'".into(),
353 )
354 })?;
355
356 let grouping = opt_bool(args, "grouping").unwrap_or(true);
357 let decimals = opt_f64(args, "decimals").map(|d| d as usize);
358
359 let formatted = format_number_impl(val, grouping, decimals);
360 Ok(Value::String(formatted))
361 }
362}
363
364fn format_number_impl(val: f64, grouping: bool, decimals: Option<usize>) -> String {
366 let abs = val.abs();
367 let sign = if val < 0.0 { "-" } else { "" };
368
369 let int_part = abs.trunc() as u64;
371
372 let int_str = if grouping {
373 format_with_grouping(int_part)
374 } else {
375 int_part.to_string()
376 };
377
378 let frac_str = match decimals {
379 Some(d) => {
380 let rounded = format!("{abs:.d$}");
382 if d == 0 {
384 String::new()
385 } else {
386 rounded
387 .find('.')
388 .map(|pos| rounded[pos + 1..].to_string())
389 .unwrap_or_default()
390 }
391 }
392 None => {
393 let s = format!("{abs}");
396 if let Some(dot) = s.find('.') {
397 let frac = &s[dot + 1..];
398 if frac == "0" {
400 String::new()
401 } else {
402 frac.to_string()
403 }
404 } else {
405 String::new()
406 }
407 }
408 };
409
410 if frac_str.is_empty() {
411 format!("{sign}{int_str}")
412 } else {
413 format!("{sign}{int_str}.{frac_str}")
414 }
415}
416
417fn format_with_grouping(n: u64) -> String {
419 let s = n.to_string();
420 let mut result = String::with_capacity(s.len() + s.len() / 3);
421 let mut count = 0;
422 for ch in s.chars().rev() {
423 if count == 3 {
424 result.push(',');
425 count = 0;
426 }
427 result.push(ch);
428 count += 1;
429 }
430 result.chars().rev().collect()
431}
432
433pub struct FormatCurrencyFunction;
439
440impl FunctionImplementation for FormatCurrencyFunction {
441 fn name(&self) -> &'static str {
442 "formatCurrency"
443 }
444
445 fn return_type(&self) -> ReturnType {
446 ReturnType::String
447 }
448
449 fn execute(
450 &self,
451 args: &HashMap<String, Value>,
452 _context: &DataContext,
453 ) -> Result<Value, A2uiError> {
454 let val = args
455 .get("value")
456 .and_then(|v| v.as_f64())
457 .ok_or_else(|| {
458 A2uiError::InvalidFunctionCall(
459 "formatCurrency: missing or non-numeric argument 'value'".into(),
460 )
461 })?;
462
463 let currency = require_str(args, "currency", "formatCurrency")?;
464
465 let grouping = opt_bool(args, "grouping").unwrap_or(true);
466 let decimals = opt_f64(args, "decimals").map(|d| d as usize);
467
468 let formatted = format_number_impl(val, grouping, decimals);
469 Ok(Value::String(format!("{currency} {formatted}")))
470 }
471}
472
473pub struct FormatDateFunction;
479
480impl FunctionImplementation for FormatDateFunction {
481 fn name(&self) -> &'static str {
482 "formatDate"
483 }
484
485 fn return_type(&self) -> ReturnType {
486 ReturnType::String
487 }
488
489 fn execute(
490 &self,
491 args: &HashMap<String, Value>,
492 _context: &DataContext,
493 ) -> Result<Value, A2uiError> {
494 let value = require_str(args, "value", "formatDate")?;
495 let fmt = require_str(args, "format", "formatDate")?;
496
497 let dt = NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S")
499 .or_else(|_| NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f"))
500 .or_else(|_| NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S"))
501 .or_else(|_| {
502 chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
504 .map(|d| d.and_hms_opt(0, 0, 0).unwrap())
505 })
506 .map_err(|_| {
507 A2uiError::InvalidFunctionCall(format!(
508 "formatDate: could not parse datetime '{value}'"
509 ))
510 })?;
511
512 let formatted = apply_date_format(&dt, fmt);
513 Ok(Value::String(formatted))
514 }
515}
516
517fn apply_date_format(dt: &NaiveDateTime, fmt: &str) -> String {
519 let mut result = String::with_capacity(fmt.len() * 2);
520 let chars: Vec<char> = fmt.chars().collect();
521 let mut i = 0;
522
523 let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
524 let months = [
525 "Jan",
526 "Feb",
527 "Mar",
528 "Apr",
529 "May",
530 "Jun",
531 "Jul",
532 "Aug",
533 "Sep",
534 "Oct",
535 "Nov",
536 "Dec",
537 ];
538
539 while i < chars.len() {
540 let c = chars[i];
541
542 let start = i;
544 while i < chars.len() && chars[i] == c {
545 i += 1;
546 }
547 let count = i - start;
548
549 match c {
550 'y' => match count {
551 4 => result.push_str(&format!("{:04}", dt.year())),
552 2 => result.push_str(&format!("{:02}", dt.year() % 100)),
553 _ => result.push_str(&dt.year().to_string()),
554 },
555 'M' => match count {
556 3 => result.push_str(months[(dt.month() - 1) as usize]),
557 2 => result.push_str(&format!("{:02}", dt.month())),
558 1 => result.push_str(&dt.month().to_string()),
559 _ => result.push_str(&format!("{:02}", dt.month())),
560 },
561 'd' => match count {
562 2 => result.push_str(&format!("{:02}", dt.day())),
563 1 => result.push_str(&dt.day().to_string()),
564 _ => result.push_str(&format!("{:02}", dt.day())),
565 },
566 'H' => match count {
567 2 => result.push_str(&format!("{:02}", dt.hour())),
568 1 => result.push_str(&dt.hour().to_string()),
569 _ => result.push_str(&format!("{:02}", dt.hour())),
570 },
571 'm' => match count {
572 2 => result.push_str(&format!("{:02}", dt.minute())),
573 1 => result.push_str(&dt.minute().to_string()),
574 _ => result.push_str(&format!("{:02}", dt.minute())),
575 },
576 's' => match count {
577 2 => result.push_str(&format!("{:02}", dt.second())),
578 1 => result.push_str(&dt.second().to_string()),
579 _ => result.push_str(&format!("{:02}", dt.second())),
580 },
581 'E' => {
582 result.push_str(weekdays[dt.weekday().num_days_from_monday() as usize]);
584 }
585 'h' => {
586 let hour_12 = dt.hour() % 12;
588 let hour_12 = if hour_12 == 0 { 12 } else { hour_12 };
589 match count {
590 2 => result.push_str(&format!("{hour_12:02}")),
591 _ => result.push_str(&hour_12.to_string()),
592 }
593 }
594 'a' => {
595 let ampm = if dt.hour() < 12 { "AM" } else { "PM" };
597 result.push_str(ampm);
598 }
599 '\'' => {
600 let mut j = start + 1;
603 while j < chars.len() && chars[j] != '\'' {
604 result.push(chars[j]);
605 j += 1;
606 }
607 i = j + 1;
608 }
609 _ => {
610 for _ in 0..count {
612 result.push(c);
613 }
614 }
615 }
616 }
617
618 result
619}
620
621pub struct FormatStringFunction;
627
628impl FunctionImplementation for FormatStringFunction {
629 fn name(&self) -> &'static str {
630 "formatString"
631 }
632
633 fn return_type(&self) -> ReturnType {
634 ReturnType::String
635 }
636
637 fn execute(
638 &self,
639 args: &HashMap<String, Value>,
640 context: &DataContext,
641 ) -> Result<Value, A2uiError> {
642 let value = require_str(args, "value", "formatString")?;
643 let result = interpolate_string(value, context);
644 Ok(Value::String(result))
645 }
646}
647
648fn interpolate_string(template: &str, context: &DataContext) -> String {
656 let mut result = String::with_capacity(template.len());
657 let bytes = template.as_bytes();
658 let mut i = 0;
659
660 while i < bytes.len() {
661 if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'$' {
662 if i + 2 < bytes.len() && bytes[i + 2] == b'{' {
664 result.push_str("${");
665 i += 3;
666 continue;
667 }
668 result.push('\\');
670 i += 1;
671 continue;
672 }
673
674 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
675 let start = i + 2;
677 let mut depth = 1u32;
678 let mut end = start;
679 while end < bytes.len() && depth > 0 {
680 if bytes[end] == b'{' {
681 depth += 1;
682 } else if bytes[end] == b'}' {
683 depth -= 1;
684 }
685 if depth > 0 {
686 end += 1;
687 }
688 }
689
690 if depth == 0 {
691 let expr = &template[start..end];
692 let resolved = resolve_expression(expr, context);
693 result.push_str(&resolved);
694 i = end + 1; } else {
696 result.push_str("${");
698 i += 2;
699 }
700 } else {
701 result.push(bytes[i] as char);
702 i += 1;
703 }
704 }
705
706 result
707}
708
709fn resolve_expression(expr: &str, context: &DataContext) -> String {
720 let trimmed = expr.trim();
721
722 if let Some(paren_pos) = trimmed.find('(') {
724 let func_name = &trimmed[..paren_pos];
725 if is_identifier(func_name)
727 && trimmed.ends_with(')')
728 && func_name.chars().next().map_or(false, |c| c.is_alphabetic() || c == '_')
729 {
730 let args_str = &trimmed[paren_pos + 1..trimmed.len() - 1];
731 let args = match parse_function_args(args_str, context) {
732 Ok(a) => a,
733 Err(_) => return String::new(),
734 };
735 return context
736 .call_function_by_name(func_name, &args)
737 .map(|v| crate::model::data_context::value_to_string(&v))
738 .unwrap_or_default();
739 }
740 }
741
742 if trimmed.starts_with('/') {
744 return context
745 .get(trimmed)
746 .map(|v| crate::model::data_context::value_to_string(&v))
747 .unwrap_or_default();
748 }
749
750 context
752 .get(trimmed)
753 .map(|v| crate::model::data_context::value_to_string(&v))
754 .unwrap_or_default()
755}
756
757fn is_identifier(s: &str) -> bool {
759 !s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_')
760}
761
762fn parse_function_args(
770 args_str: &str,
771 context: &DataContext,
772) -> Result<HashMap<String, Value>, A2uiError> {
773 let mut args = HashMap::new();
774 if args_str.trim().is_empty() {
775 return Ok(args);
776 }
777
778 let mut i = 0;
779 let chars: Vec<char> = args_str.chars().collect();
780
781 while i < chars.len() {
782 while i < chars.len() && (chars[i].is_whitespace() || chars[i] == ',') {
784 i += 1;
785 }
786 if i >= chars.len() {
787 break;
788 }
789
790 let key_start = i;
792 while i < chars.len() && chars[i] != ':' && chars[i] != '=' {
793 i += 1;
794 }
795 if i >= chars.len() || (chars[i] != ':' && chars[i] != '=') {
796 return Err(A2uiError::InvalidFunctionCall(format!(
797 "formatString: expected ':' or '=' in function args at position {i}"
798 )));
799 }
800 let key: String = chars[key_start..i].iter().collect();
801 let key = key.trim().to_string();
802 i += 1; while i < chars.len() && chars[i].is_whitespace() {
806 i += 1;
807 }
808
809 let val;
811 if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
812 i += 2; let mut depth = 1u32;
815 let inner_start = i;
816 while i < chars.len() && depth > 0 {
817 if chars[i] == '{' {
818 depth += 1;
819 } else if chars[i] == '}' {
820 depth -= 1;
821 }
822 if depth > 0 {
823 i += 1;
824 }
825 }
826 let inner: String = chars[inner_start..i].iter().collect();
827 if i < chars.len() {
828 i += 1; }
830 val = context
832 .get(inner.trim())
833 .unwrap_or(Value::String(String::new()));
834 } else if i < chars.len() && chars[i] == '\'' {
835 i += 1; let val_start = i;
838 while i < chars.len() && chars[i] != '\'' {
839 i += 1;
840 }
841 let s: String = chars[val_start..i].iter().collect();
842 if i < chars.len() {
843 i += 1; }
845 val = Value::String(s);
846 } else if i < chars.len() && (chars[i] == '-' || chars[i].is_ascii_digit()) {
847 let val_start = i;
849 if chars[i] == '-' {
850 i += 1;
851 }
852 while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
853 i += 1;
854 }
855 let num_str: String = chars[val_start..i].iter().collect();
856 val = num_str
857 .parse::<f64>()
858 .map(|n| serde_json::json!(n))
859 .unwrap_or(Value::String(num_str));
860 } else {
861 let val_start = i;
863 while i < chars.len() && chars[i] != ',' && chars[i] != ')' && !chars[i].is_whitespace()
864 {
865 i += 1;
866 }
867 let token: String = chars[val_start..i].iter().collect();
868 let token = token.trim();
869
870 if token == "true" {
872 val = Value::Bool(true);
873 } else if token == "false" {
874 val = Value::Bool(false);
875 } else {
876 val = context
878 .get(token)
879 .unwrap_or_else(|| Value::String(token.to_string()));
880 }
881 };
882
883 args.insert(key, val);
884 }
885
886 Ok(args)
887}
888
889pub struct PluralizeFunction;
895
896impl FunctionImplementation for PluralizeFunction {
897 fn name(&self) -> &'static str {
898 "pluralize"
899 }
900
901 fn return_type(&self) -> ReturnType {
902 ReturnType::String
903 }
904
905 fn execute(
906 &self,
907 args: &HashMap<String, Value>,
908 _context: &DataContext,
909 ) -> Result<Value, A2uiError> {
910 let val = args
911 .get("value")
912 .and_then(|v| v.as_f64())
913 .ok_or_else(|| {
914 A2uiError::InvalidFunctionCall(
915 "pluralize: missing or non-numeric argument 'value'".into(),
916 )
917 })?;
918
919 let category = if val == 0.0 {
921 "zero"
922 } else if val == 1.0 {
923 "one"
924 } else {
925 "other"
926 };
927
928 let result = args
930 .get(category)
931 .and_then(|v| v.as_str())
932 .or_else(|| args.get("other").and_then(|v| v.as_str()))
933 .unwrap_or("")
934 .to_string();
935
936 Ok(Value::String(result))
937 }
938}
939
940pub struct OpenUrlFunction;
946
947impl FunctionImplementation for OpenUrlFunction {
948 fn name(&self) -> &'static str {
949 "openUrl"
950 }
951
952 fn return_type(&self) -> ReturnType {
953 ReturnType::Void
954 }
955
956 fn execute(
957 &self,
958 _args: &HashMap<String, Value>,
959 _context: &DataContext,
960 ) -> Result<Value, A2uiError> {
961 Ok(Value::Null)
963 }
964}
965
966pub fn build_basic_functions() -> Vec<Box<dyn FunctionImplementation>> {
972 vec![
973 Box::new(RequiredFunction),
974 Box::new(RegexFunction),
975 Box::new(LengthFunction),
976 Box::new(NumericFunction),
977 Box::new(EmailFunction),
978 Box::new(AndFunction),
979 Box::new(OrFunction),
980 Box::new(NotFunction),
981 Box::new(FormatNumberFunction),
982 Box::new(FormatCurrencyFunction),
983 Box::new(FormatDateFunction),
984 Box::new(FormatStringFunction),
985 Box::new(PluralizeFunction),
986 Box::new(OpenUrlFunction),
987 ]
988}
989
990#[cfg(test)]
995mod tests {
996 use super::*;
997 use crate::model::data_model::DataModel;
998 use serde_json::json;
999
1000 fn empty_context() -> DataContext<'static> {
1002 let dm = Box::leak(Box::new(DataModel::new()));
1005 let fns = Box::leak(Box::new(HashMap::new()));
1006 DataContext::new(dm, fns)
1007 }
1008
1009 fn context_with_data(data: Value) -> DataContext<'static> {
1011 let dm = Box::leak(Box::new(DataModel::from_value(data)));
1012 let fns = Box::leak(Box::new(HashMap::new()));
1013 DataContext::new(dm, fns)
1014 }
1015
1016 fn context_with_functions(data: Value) -> DataContext<'static> {
1018 use crate::catalog::function_api::FunctionImplementation;
1019 let dm = Box::leak(Box::new(DataModel::from_value(data)));
1020 let fns_map: HashMap<String, Box<dyn FunctionImplementation>> = build_basic_functions()
1021 .into_iter()
1022 .map(|f| (f.name().to_string(), f))
1023 .collect();
1024 let fns = Box::leak(Box::new(fns_map));
1025 DataContext::new(dm, fns)
1026 }
1027
1028 #[test]
1031 fn test_required_string() {
1032 let ctx = empty_context();
1033 let f = RequiredFunction;
1034
1035 let mut args = HashMap::new();
1036 args.insert("value".into(), json!("hello"));
1037 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1038
1039 args.insert("value".into(), json!(""));
1040 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1041
1042 args.insert("value".into(), Value::Null);
1043 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1044 }
1045
1046 #[test]
1047 fn test_required_array() {
1048 let ctx = empty_context();
1049 let f = RequiredFunction;
1050
1051 let mut args = HashMap::new();
1052 args.insert("value".into(), json!([1, 2, 3]));
1053 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1054
1055 args.insert("value".into(), json!([]));
1056 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1057 }
1058
1059 #[test]
1062 fn test_regex_match() {
1063 let ctx = empty_context();
1064 let f = RegexFunction;
1065
1066 let mut args = HashMap::new();
1067 args.insert("value".into(), json!("hello123"));
1068 args.insert("pattern".into(), json!("^[a-z]+[0-9]+$"));
1069 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1070
1071 args.insert("value".into(), json!("HELLO"));
1072 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1073 }
1074
1075 #[test]
1078 fn test_length_bounds() {
1079 let ctx = empty_context();
1080 let f = LengthFunction;
1081
1082 let mut args = HashMap::new();
1083 args.insert("value".into(), json!("abc"));
1084 args.insert("min".into(), json!(2));
1085 args.insert("max".into(), json!(5));
1086 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1087
1088 args.insert("value".into(), json!("a"));
1089 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1090
1091 args.insert("value".into(), json!("abcdef"));
1092 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1093 }
1094
1095 #[test]
1096 fn test_length_no_bounds() {
1097 let ctx = empty_context();
1098 let f = LengthFunction;
1099
1100 let mut args = HashMap::new();
1101 args.insert("value".into(), json!("anything"));
1102 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1103 }
1104
1105 #[test]
1108 fn test_numeric_valid() {
1109 let ctx = empty_context();
1110 let f = NumericFunction;
1111
1112 let mut args = HashMap::new();
1113 args.insert("value".into(), json!(42));
1114 args.insert("min".into(), json!(0));
1115 args.insert("max".into(), json!(100));
1116 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1117 }
1118
1119 #[test]
1120 fn test_numeric_string_value() {
1121 let ctx = empty_context();
1122 let f = NumericFunction;
1123
1124 let mut args = HashMap::new();
1125 args.insert("value".into(), json!("3.14"));
1126 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1127 }
1128
1129 #[test]
1130 fn test_numeric_invalid_string() {
1131 let ctx = empty_context();
1132 let f = NumericFunction;
1133
1134 let mut args = HashMap::new();
1135 args.insert("value".into(), json!("not a number"));
1136 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1137 }
1138
1139 #[test]
1140 fn test_numeric_out_of_range() {
1141 let ctx = empty_context();
1142 let f = NumericFunction;
1143
1144 let mut args = HashMap::new();
1145 args.insert("value".into(), json!(200));
1146 args.insert("max".into(), json!(100));
1147 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1148 }
1149
1150 #[test]
1153 fn test_email_valid() {
1154 let ctx = empty_context();
1155 let f = EmailFunction;
1156
1157 let mut args = HashMap::new();
1158 args.insert("value".into(), json!("user@example.com"));
1159 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1160 }
1161
1162 #[test]
1163 fn test_email_invalid() {
1164 let ctx = empty_context();
1165 let f = EmailFunction;
1166
1167 let mut args = HashMap::new();
1168 args.insert("value".into(), json!("not-an-email"));
1169 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1170
1171 args.insert("value".into(), json!("@missing-local.com"));
1172 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1173 }
1174
1175 #[test]
1178 fn test_and_all_true() {
1179 let ctx = empty_context();
1180 let f = AndFunction;
1181
1182 let mut args = HashMap::new();
1183 args.insert("values".into(), json!([true, true, true]));
1184 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1185 }
1186
1187 #[test]
1188 fn test_and_with_false() {
1189 let ctx = empty_context();
1190 let f = AndFunction;
1191
1192 let mut args = HashMap::new();
1193 args.insert("values".into(), json!([true, false, true]));
1194 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1195 }
1196
1197 #[test]
1200 fn test_or_any_true() {
1201 let ctx = empty_context();
1202 let f = OrFunction;
1203
1204 let mut args = HashMap::new();
1205 args.insert("values".into(), json!([false, true, false]));
1206 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1207 }
1208
1209 #[test]
1210 fn test_or_all_false() {
1211 let ctx = empty_context();
1212 let f = OrFunction;
1213
1214 let mut args = HashMap::new();
1215 args.insert("values".into(), json!([false, false, false]));
1216 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1217 }
1218
1219 #[test]
1222 fn test_not() {
1223 let ctx = empty_context();
1224 let f = NotFunction;
1225
1226 let mut args = HashMap::new();
1227 args.insert("value".into(), json!(true));
1228 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(false));
1229
1230 args.insert("value".into(), json!(false));
1231 assert_eq!(f.execute(&args, &ctx).unwrap(), json!(true));
1232 }
1233
1234 #[test]
1237 fn test_format_number_basic() {
1238 let ctx = empty_context();
1239 let f = FormatNumberFunction;
1240
1241 let mut args = HashMap::new();
1242 args.insert("value".into(), json!(1234567.89));
1243 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("1,234,567.89"));
1244 }
1245
1246 #[test]
1247 fn test_format_number_no_grouping() {
1248 let ctx = empty_context();
1249 let f = FormatNumberFunction;
1250
1251 let mut args = HashMap::new();
1252 args.insert("value".into(), json!(1234567));
1253 args.insert("grouping".into(), json!(false));
1254 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("1234567"));
1255 }
1256
1257 #[test]
1258 fn test_format_number_decimals() {
1259 let ctx = empty_context();
1260 let f = FormatNumberFunction;
1261
1262 let mut args = HashMap::new();
1263 args.insert("value".into(), json!(std::f64::consts::PI));
1264 args.insert("decimals".into(), json!(2));
1265 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("3.14"));
1266 }
1267
1268 #[test]
1269 fn test_format_number_negative() {
1270 let ctx = empty_context();
1271 let f = FormatNumberFunction;
1272
1273 let mut args = HashMap::new();
1274 args.insert("value".into(), json!(-1234.5));
1275 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("-1,234.5"));
1276 }
1277
1278 #[test]
1281 fn test_format_currency() {
1282 let ctx = empty_context();
1283 let f = FormatCurrencyFunction;
1284
1285 let mut args = HashMap::new();
1286 args.insert("value".into(), json!(1234.56));
1287 args.insert("currency".into(), json!("USD"));
1288 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("USD 1,234.56"));
1289 }
1290
1291 #[test]
1294 fn test_format_date_full() {
1295 let ctx = empty_context();
1296 let f = FormatDateFunction;
1297
1298 let mut args = HashMap::new();
1299 args.insert("value".into(), json!("2024-03-15T14:30:00"));
1300 args.insert("format".into(), json!("yyyy-MM-dd HH:mm:ss"));
1301 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("2024-03-15 14:30:00"));
1302 }
1303
1304 #[test]
1305 fn test_format_date_weekday() {
1306 let ctx = empty_context();
1307 let f = FormatDateFunction;
1308
1309 let mut args = HashMap::new();
1311 args.insert("value".into(), json!("2024-03-15T00:00:00"));
1312 args.insert("format".into(), json!("E yyyy-MM-dd"));
1313 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("Fri 2024-03-15"));
1314 }
1315
1316 #[test]
1317 fn test_format_date_month_name() {
1318 let ctx = empty_context();
1319 let f = FormatDateFunction;
1320
1321 let mut args = HashMap::new();
1322 args.insert("value".into(), json!("2024-12-25T10:00:00"));
1323 args.insert("format".into(), json!("MMM dd, yyyy"));
1324 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("Dec 25, 2024"));
1325 }
1326
1327 #[test]
1328 fn test_format_date_time_only() {
1329 let ctx = empty_context();
1330 let f = FormatDateFunction;
1331
1332 let mut args = HashMap::new();
1333 args.insert("value".into(), json!("2024-01-01T09:05:03"));
1334 args.insert("format".into(), json!("HH:mm:ss"));
1335 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("09:05:03"));
1336 }
1337
1338 #[test]
1341 fn test_format_date_12h_midnight() {
1342 let ctx = empty_context();
1344 let f = FormatDateFunction;
1345
1346 let mut args = HashMap::new();
1347 args.insert("value".into(), json!("2024-01-01T00:00:00"));
1348 args.insert("format".into(), json!("h:mm a"));
1349 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("12:00 AM"));
1350 }
1351
1352 #[test]
1353 fn test_format_date_12h_noon() {
1354 let ctx = empty_context();
1356 let f = FormatDateFunction;
1357
1358 let mut args = HashMap::new();
1359 args.insert("value".into(), json!("2024-06-15T12:00:00"));
1360 args.insert("format".into(), json!("h:mm a"));
1361 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("12:00 PM"));
1362 }
1363
1364 #[test]
1365 fn test_format_date_12h_afternoon() {
1366 let ctx = empty_context();
1368 let f = FormatDateFunction;
1369
1370 let mut args = HashMap::new();
1371 args.insert("value".into(), json!("2024-03-15T15:30:00"));
1372 args.insert("format".into(), json!("h:mm a"));
1373 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("3:30 PM"));
1374 }
1375
1376 #[test]
1377 fn test_format_date_12h_morning() {
1378 let ctx = empty_context();
1380 let f = FormatDateFunction;
1381
1382 let mut args = HashMap::new();
1383 args.insert("value".into(), json!("2024-03-15T09:05:00"));
1384 args.insert("format".into(), json!("h:mm a"));
1385 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("9:05 AM"));
1386 }
1387
1388 #[test]
1389 fn test_format_date_hh_leading_zero() {
1390 let ctx = empty_context();
1392 let f = FormatDateFunction;
1393
1394 let mut args = HashMap::new();
1395 args.insert("value".into(), json!("2024-03-15T09:00:00"));
1396 args.insert("format".into(), json!("hh:mm a"));
1397 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("09:00 AM"));
1398 }
1399
1400 #[test]
1401 fn test_format_date_hh_midnight_leading_zero() {
1402 let ctx = empty_context();
1404 let f = FormatDateFunction;
1405
1406 let mut args = HashMap::new();
1407 args.insert("value".into(), json!("2024-01-01T00:30:00"));
1408 args.insert("format".into(), json!("hh:mm a"));
1409 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("12:30 AM"));
1410 }
1411
1412 #[test]
1413 fn test_format_date_12h_full_format() {
1414 let ctx = empty_context();
1416 let f = FormatDateFunction;
1417
1418 let mut args = HashMap::new();
1419 args.insert("value".into(), json!("2024-12-25T14:30:00"));
1420 args.insert("format".into(), json!("MMM dd, yyyy hh:mm a"));
1421 assert_eq!(
1422 f.execute(&args, &ctx).unwrap(),
1423 json!("Dec 25, 2024 02:30 PM")
1424 );
1425 }
1426
1427 #[test]
1430 fn test_format_string_data_path() {
1431 let ctx = context_with_data(json!({"user": {"name": "Alice"}}));
1432 let f = FormatStringFunction;
1433
1434 let mut args = HashMap::new();
1435 args.insert("value".into(), json!("Hello, ${/user/name}!"));
1436 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("Hello, Alice!"));
1437 }
1438
1439 #[test]
1440 fn test_format_string_escape() {
1441 let ctx = empty_context();
1442 let f = FormatStringFunction;
1443
1444 let mut args = HashMap::new();
1445 args.insert("value".into(), json!("escaped: \\${literal}"));
1446 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("escaped: ${literal}"));
1447 }
1448
1449 #[test]
1450 fn test_format_string_mixed() {
1451 let ctx = context_with_data(json!({"greeting": "Hello", "target": "World"}));
1452 let f = FormatStringFunction;
1453
1454 let mut args = HashMap::new();
1455 args.insert("value".into(), json!("${/greeting}, ${/target}!"));
1456 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("Hello, World!"));
1457 }
1458
1459 #[test]
1462 fn test_format_string_function_call_format_date() {
1463 let ctx = context_with_functions(json!({"event": {"date": "2024-03-15T15:30:00"}}));
1465 let f = FormatStringFunction;
1466
1467 let mut args = HashMap::new();
1468 args.insert(
1469 "value".into(),
1470 json!("The event is at ${formatDate(value:${/event/date}, format:'h:mm a')}"),
1471 );
1472 assert_eq!(
1473 f.execute(&args, &ctx).unwrap(),
1474 json!("The event is at 3:30 PM")
1475 );
1476 }
1477
1478 #[test]
1479 fn test_format_string_function_call_format_number() {
1480 let ctx = context_with_functions(json!({"price": 1234.5}));
1482 let f = FormatStringFunction;
1483
1484 let mut args = HashMap::new();
1485 args.insert(
1486 "value".into(),
1487 json!("Price: ${formatNumber(value:${/price}, grouping:false)}"),
1488 );
1489 assert_eq!(
1490 f.execute(&args, &ctx).unwrap(),
1491 json!("Price: 1234.5")
1492 );
1493 }
1494
1495 #[test]
1496 fn test_format_string_function_call_pluralize() {
1497 let ctx = context_with_functions(json!({"count": 5}));
1499 let f = FormatStringFunction;
1500
1501 let mut args = HashMap::new();
1502 args.insert(
1503 "value".into(),
1504 json!("${pluralize(value:${/count}, one:'item', other:'items')}"),
1505 );
1506 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("items"));
1507 }
1508
1509 #[test]
1510 fn test_format_string_unknown_function() {
1511 let ctx = context_with_functions(json!({}));
1513 let f = FormatStringFunction;
1514
1515 let mut args = HashMap::new();
1516 args.insert("value".into(), json!("result: ${unknownFunc()}"));
1517 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("result: "));
1518 }
1519
1520 #[test]
1523 fn test_pluralize_one() {
1524 let ctx = empty_context();
1525 let f = PluralizeFunction;
1526
1527 let mut args = HashMap::new();
1528 args.insert("value".into(), json!(1));
1529 args.insert("one".into(), json!("item"));
1530 args.insert("other".into(), json!("items"));
1531 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("item"));
1532 }
1533
1534 #[test]
1535 fn test_pluralize_other() {
1536 let ctx = empty_context();
1537 let f = PluralizeFunction;
1538
1539 let mut args = HashMap::new();
1540 args.insert("value".into(), json!(5));
1541 args.insert("one".into(), json!("item"));
1542 args.insert("other".into(), json!("items"));
1543 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("items"));
1544 }
1545
1546 #[test]
1547 fn test_pluralize_zero() {
1548 let ctx = empty_context();
1549 let f = PluralizeFunction;
1550
1551 let mut args = HashMap::new();
1552 args.insert("value".into(), json!(0));
1553 args.insert("zero".into(), json!("no items"));
1554 args.insert("one".into(), json!("item"));
1555 args.insert("other".into(), json!("items"));
1556 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("no items"));
1557 }
1558
1559 #[test]
1560 fn test_pluralize_zero_fallback() {
1561 let ctx = empty_context();
1562 let f = PluralizeFunction;
1563
1564 let mut args = HashMap::new();
1565 args.insert("value".into(), json!(0));
1566 args.insert("one".into(), json!("item"));
1567 args.insert("other".into(), json!("items"));
1568 assert_eq!(f.execute(&args, &ctx).unwrap(), json!("items"));
1569 }
1570
1571 #[test]
1574 fn test_open_url_noop() {
1575 let ctx = empty_context();
1576 let f = OpenUrlFunction;
1577
1578 let args = HashMap::new();
1579 assert_eq!(f.execute(&args, &ctx).unwrap(), Value::Null);
1580 }
1581
1582 #[test]
1585 fn test_build_basic_functions_count() {
1586 let fns = build_basic_functions();
1587 assert_eq!(fns.len(), 14);
1588
1589 let names: Vec<&str> = fns.iter().map(|f| f.name()).collect();
1590 assert!(names.contains(&"required"));
1591 assert!(names.contains(&"regex"));
1592 assert!(names.contains(&"length"));
1593 assert!(names.contains(&"numeric"));
1594 assert!(names.contains(&"email"));
1595 assert!(names.contains(&"and"));
1596 assert!(names.contains(&"or"));
1597 assert!(names.contains(&"not"));
1598 assert!(names.contains(&"formatNumber"));
1599 assert!(names.contains(&"formatCurrency"));
1600 assert!(names.contains(&"formatDate"));
1601 assert!(names.contains(&"formatString"));
1602 assert!(names.contains(&"pluralize"));
1603 assert!(names.contains(&"openUrl"));
1604 }
1605}