1use base64::Engine;
12use serde_json::{json, Value};
13
14#[derive(Debug, Clone)]
15pub struct IntrinsicError(pub String);
16
17impl std::fmt::Display for IntrinsicError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "{}", self.0)
20 }
21}
22
23pub fn is_intrinsic_call(value: &str) -> bool {
26 value.starts_with("States.") && value.contains('(')
27}
28
29const MAX_NESTING: usize = 10;
32
33#[derive(Debug, Clone)]
35pub enum CallError {
36 Intrinsic(IntrinsicError),
39 Path(String),
42}
43
44impl CallError {
45 pub fn into_states_error(self, call: &str) -> (String, String) {
51 match self {
52 CallError::Intrinsic(e) => (
53 "States.Runtime".to_string(),
54 format!(
55 "There was an error while evaluating the intrinsic function: {call}. {}",
56 e.0
57 ),
58 ),
59 CallError::Path(cause) => ("States.Runtime".to_string(), cause),
60 }
61 }
62}
63
64impl From<IntrinsicError> for CallError {
65 fn from(e: IntrinsicError) -> Self {
66 CallError::Intrinsic(e)
67 }
68}
69
70pub fn evaluate(call: &str, input: &Value) -> Result<Value, IntrinsicError> {
74 evaluate_with_context(call, input, None).map_err(|e| match e {
75 CallError::Intrinsic(e) => e,
76 CallError::Path(cause) => IntrinsicError(cause),
77 })
78}
79
80pub fn evaluate_with_context(
83 call: &str,
84 input: &Value,
85 context: Option<&Value>,
86) -> Result<Value, CallError> {
87 let mut parser = CallParser {
88 chars: call.trim().chars().collect(),
89 pos: 0,
90 };
91 let expr = parser.call(1)?;
92 parser.skip_ws();
93 if parser.pos != parser.chars.len() {
94 return Err(IntrinsicError(format!("unexpected trailing input in '{call}'")).into());
95 }
96 eval_expr(&expr, input, context)
97}
98
99#[derive(Debug, Clone)]
101enum Expr {
102 Call {
103 name: String,
104 args: Vec<Expr>,
105 },
106 Str {
110 value: String,
111 raw: String,
112 },
113 Path(String),
114 Literal(Value),
115}
116
117fn eval_expr(expr: &Expr, input: &Value, context: Option<&Value>) -> Result<Value, CallError> {
118 match expr {
119 Expr::Literal(v) => Ok(v.clone()),
120 Expr::Str { value, .. } => Ok(Value::String(value.clone())),
121 Expr::Path(path) => crate::io_processing::resolve_with_context(input, context, path)
122 .map_err(|e| CallError::Path(crate::io_processing::path_error_cause(path, &e))),
123 Expr::Call { name, args } => {
124 let values = args
125 .iter()
126 .map(|a| eval_expr(a, input, context))
127 .collect::<Result<Vec<_>, _>>()?;
128 if name == "States.Format" {
129 let raw = match args.first() {
130 Some(Expr::Str { raw, .. }) => Some(raw.as_str()),
131 _ => None,
132 };
133 return Ok(fn_format(raw, &values)?);
134 }
135 Ok(call_function(name, &values)?)
136 }
137 }
138}
139
140fn call_function(name: &str, args: &[Value]) -> Result<Value, IntrinsicError> {
141 match name {
142 "States.JsonToString" => fn_json_to_string(args),
143 "States.StringToJson" => fn_string_to_json(args),
144 "States.Array" => Ok(Value::Array(args.to_vec())),
145 "States.ArrayPartition" => fn_array_partition(args),
146 "States.ArrayContains" => fn_array_contains(args),
147 "States.ArrayRange" => fn_array_range(args),
148 "States.ArrayGetItem" => fn_array_get_item(args),
149 "States.ArrayLength" => fn_array_length(args),
150 "States.ArrayUnique" => fn_array_unique(args),
151 "States.Base64Encode" => fn_base64_encode(args),
152 "States.Base64Decode" => fn_base64_decode(args),
153 "States.Hash" => fn_hash(args),
154 "States.JsonMerge" => fn_json_merge(args),
155 "States.MathRandom" => fn_math_random(args),
156 "States.MathAdd" => fn_math_add(args),
157 "States.UUID" => fn_uuid(args),
158 "States.StringSplit" => fn_string_split(args),
159 other => Err(IntrinsicError(format!("unknown intrinsic '{other}'"))),
160 }
161}
162
163struct CallParser {
169 chars: Vec<char>,
170 pos: usize,
171}
172
173impl CallParser {
174 fn peek(&self) -> Option<char> {
175 self.chars.get(self.pos).copied()
176 }
177
178 fn skip_ws(&mut self) {
179 while self.peek().is_some_and(char::is_whitespace) {
180 self.pos += 1;
181 }
182 }
183
184 fn rest_starts_with(&self, s: &str) -> bool {
185 let n = s.chars().count();
186 self.chars
187 .get(self.pos..self.pos + n)
188 .is_some_and(|w| w.iter().copied().eq(s.chars()))
189 }
190
191 fn text(&self) -> String {
192 self.chars.iter().collect()
193 }
194
195 fn err<T>(&self, msg: &str) -> Result<T, IntrinsicError> {
196 Err(IntrinsicError(format!(
197 "{msg} at position {} in '{}'",
198 self.pos,
199 self.text()
200 )))
201 }
202
203 fn call(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
204 if depth > MAX_NESTING {
205 return Err(IntrinsicError(format!(
206 "intrinsic functions can be nested at most {MAX_NESTING} levels deep in '{}'",
207 self.text()
208 )));
209 }
210 self.skip_ws();
211 if !self.rest_starts_with("States.") {
212 return self.err("expected an intrinsic function");
213 }
214 let start = self.pos;
215 while self
216 .peek()
217 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '.')
218 {
219 self.pos += 1;
220 }
221 let name: String = self.chars[start..self.pos].iter().collect();
222 self.skip_ws();
223 if self.peek() != Some('(') {
224 return Err(IntrinsicError(format!("missing '(' in '{}'", self.text())));
225 }
226 self.pos += 1;
227 let mut args = Vec::new();
228 self.skip_ws();
229 if self.peek() == Some(')') {
230 self.pos += 1;
231 return Ok(Expr::Call { name, args });
232 }
233 loop {
234 args.push(self.arg(depth)?);
235 self.skip_ws();
236 match self.peek() {
237 Some(',') => self.pos += 1,
238 Some(')') => {
239 self.pos += 1;
240 return Ok(Expr::Call { name, args });
241 }
242 None => return Err(IntrinsicError(format!("missing ')' in '{}'", self.text()))),
243 Some(_) => return self.err("expected ',' or ')'"),
244 }
245 }
246 }
247
248 fn arg(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
249 self.skip_ws();
250 match self.peek() {
251 Some('S') if self.rest_starts_with("States.") => self.call(depth + 1),
252 Some('\'') => self.string(),
253 Some('$') => self.path(),
254 Some(_) => self.literal(),
255 None => self.err("expected an argument"),
256 }
257 }
258
259 fn string(&mut self) -> Result<Expr, IntrinsicError> {
260 self.pos += 1; let mut value = String::new();
262 let mut raw = String::new();
263 loop {
264 match self.peek() {
265 None => return self.err("unterminated string literal"),
266 Some('\\') => {
267 let Some(next) = self.chars.get(self.pos + 1).copied() else {
268 return self.err("unterminated escape sequence");
269 };
270 raw.push('\\');
271 raw.push(next);
272 value.push_str(&unescape_char(next));
273 self.pos += 2;
274 }
275 Some('\'') => {
276 self.pos += 1;
277 return Ok(Expr::Str { value, raw });
278 }
279 Some(c) => {
280 value.push(c);
281 raw.push(c);
282 self.pos += 1;
283 }
284 }
285 }
286 }
287
288 fn path(&mut self) -> Result<Expr, IntrinsicError> {
291 let start = self.pos;
292 let mut depth = 0usize;
293 let mut quote: Option<char> = None;
294 while let Some(c) = self.peek() {
295 if let Some(q) = quote {
296 if c == '\\' {
297 self.pos += 1;
298 } else if c == q {
299 quote = None;
300 }
301 } else {
302 match c {
303 '\'' | '"' if depth > 0 => quote = Some(c),
304 '[' | '(' => depth += 1,
305 ']' | ')' if depth > 0 => depth -= 1,
306 ',' | ')' if depth == 0 => break,
307 _ => {}
308 }
309 }
310 self.pos += 1;
311 }
312 let path: String = self.chars[start..self.pos.min(self.chars.len())]
313 .iter()
314 .collect();
315 Ok(Expr::Path(path.trim_end().to_string()))
316 }
317
318 fn literal(&mut self) -> Result<Expr, IntrinsicError> {
319 let start = self.pos;
320 while self.peek().is_some_and(|c| c != ',' && c != ')') {
321 self.pos += 1;
322 }
323 let raw: String = self.chars[start..self.pos].iter().collect();
324 let raw = raw.trim();
325 serde_json::from_str::<Value>(raw)
326 .map(Expr::Literal)
327 .map_err(|e| IntrinsicError(format!("invalid argument '{raw}': {e}")))
328 }
329}
330
331fn unescape_char(c: char) -> String {
333 match c {
334 '\\' | '\'' | '{' | '}' => c.to_string(),
335 'n' => "\n".to_string(),
336 't' => "\t".to_string(),
337 other => format!("\\{other}"),
338 }
339}
340
341fn arg_as_str(v: &Value) -> Result<String, IntrinsicError> {
342 match v {
343 Value::String(s) => Ok(s.clone()),
344 other => Ok(serde_json::to_string(other).unwrap_or_default()),
345 }
346}
347
348fn arg_as_array(v: &Value) -> Result<&Vec<Value>, IntrinsicError> {
349 v.as_array()
350 .ok_or_else(|| IntrinsicError(format!("expected array, got {v}")))
351}
352
353fn arg_as_i64(v: &Value) -> Result<i64, IntrinsicError> {
354 v.as_i64()
355 .or_else(|| v.as_f64().map(|f| f as i64))
356 .ok_or_else(|| IntrinsicError(format!("expected integer, got {v}")))
357}
358
359fn arg_as_f64(v: &Value) -> Result<f64, IntrinsicError> {
360 v.as_f64()
361 .ok_or_else(|| IntrinsicError(format!("expected number, got {v}")))
362}
363
364fn need_args(args: &[Value], expected: usize, name: &str) -> Result<(), IntrinsicError> {
365 if args.len() != expected {
366 Err(IntrinsicError(format!(
367 "{name} expected {expected} args, got {}",
368 args.len()
369 )))
370 } else {
371 Ok(())
372 }
373}
374
375fn fn_format(raw: Option<&str>, args: &[Value]) -> Result<Value, IntrinsicError> {
381 if args.is_empty() {
382 return Err(IntrinsicError(
383 "States.Format requires at least one argument".into(),
384 ));
385 }
386 let template = match raw {
387 Some(raw) => raw.to_string(),
388 None => args[0]
389 .as_str()
390 .ok_or_else(|| IntrinsicError("States.Format template must be a string".into()))?
391 .to_string(),
392 };
393 let honour_escapes = raw.is_some();
394 let mut out = String::with_capacity(template.len());
395 let mut chars = template.chars().peekable();
396 let mut idx = 1;
397 while let Some(c) = chars.next() {
398 match c {
399 '\\' if honour_escapes => {
400 if let Some(n) = chars.next() {
401 out.push_str(&unescape_char(n));
402 }
403 }
404 '{' if matches!(chars.peek(), Some('}')) => {
405 chars.next();
406 let v = args.get(idx).ok_or_else(format_arity_error)?;
407 idx += 1;
408 match v {
409 Value::String(s) => out.push_str(s),
410 Value::Null => out.push_str("null"),
411 other => out.push_str(&serde_json::to_string(other).unwrap_or_default()),
412 }
413 }
414 _ => out.push(c),
415 }
416 }
417 if idx != args.len() {
418 return Err(format_arity_error());
419 }
420 Ok(Value::String(out))
421}
422
423fn format_arity_error() -> IntrinsicError {
425 IntrinsicError(
426 "Invalid arguments in States.Format: number of arguments do not match the occurrences \
427 of {}."
428 .into(),
429 )
430}
431
432fn fn_json_to_string(args: &[Value]) -> Result<Value, IntrinsicError> {
433 need_args(args, 1, "States.JsonToString")?;
434 Ok(Value::String(
435 serde_json::to_string(&args[0]).unwrap_or_default(),
436 ))
437}
438
439fn fn_string_to_json(args: &[Value]) -> Result<Value, IntrinsicError> {
440 need_args(args, 1, "States.StringToJson")?;
441 let s = args[0]
442 .as_str()
443 .ok_or_else(|| IntrinsicError("States.StringToJson arg must be a string".into()))?;
444 serde_json::from_str(s).map_err(|e| {
445 IntrinsicError(format!(
446 "Cannot convert String to Json in States.StringToJson, caused by: {e}"
447 ))
448 })
449}
450
451fn fn_array_partition(args: &[Value]) -> Result<Value, IntrinsicError> {
452 need_args(args, 2, "States.ArrayPartition")?;
453 let arr = arg_as_array(&args[0])?;
454 let chunk = arg_as_i64(&args[1])?;
455 if chunk <= 0 {
456 return Err(IntrinsicError(
457 "ArrayPartition chunk size must be > 0".into(),
458 ));
459 }
460 let chunk = chunk as usize;
461 let mut out: Vec<Value> = Vec::new();
462 for slice in arr.chunks(chunk) {
463 out.push(Value::Array(slice.to_vec()));
464 }
465 Ok(Value::Array(out))
466}
467
468fn fn_array_contains(args: &[Value]) -> Result<Value, IntrinsicError> {
469 need_args(args, 2, "States.ArrayContains")?;
470 let arr = arg_as_array(&args[0])?;
471 Ok(Value::Bool(arr.iter().any(|v| v == &args[1])))
472}
473
474const ARRAY_RANGE_MAX_ITEMS: i128 = 1000;
476
477fn fn_array_range(args: &[Value]) -> Result<Value, IntrinsicError> {
478 need_args(args, 3, "States.ArrayRange")?;
479 let start = arg_as_i64(&args[0])?;
480 let end = arg_as_i64(&args[1])?;
481 let step = arg_as_i64(&args[2])?;
482 if step == 0 {
483 return Err(IntrinsicError(
485 "Invalid arguments in States.ArrayRange".into(),
486 ));
487 }
488 let span = i128::from(end) - i128::from(start);
491 let count: i128 = if (step > 0 && span < 0) || (step < 0 && span > 0) {
492 0
493 } else {
494 span / i128::from(step) + 1
495 };
496 if count > ARRAY_RANGE_MAX_ITEMS {
497 return Err(IntrinsicError(format!(
498 "ArrayRange result exceeds the maximum of {ARRAY_RANGE_MAX_ITEMS} items"
499 )));
500 }
501 let out: Vec<Value> = (0..count)
502 .map(|k| json!((i128::from(start) + k * i128::from(step)) as i64))
503 .collect();
504 Ok(Value::Array(out))
505}
506
507fn fn_array_get_item(args: &[Value]) -> Result<Value, IntrinsicError> {
508 need_args(args, 2, "States.ArrayGetItem")?;
509 let arr = arg_as_array(&args[0])?;
510 let idx = arg_as_i64(&args[1])?;
511 if idx < 0 {
512 return Err(IntrinsicError("ArrayGetItem index must be >= 0".into()));
513 }
514 Ok(arr.get(idx as usize).cloned().unwrap_or(Value::Null))
515}
516
517fn fn_array_length(args: &[Value]) -> Result<Value, IntrinsicError> {
518 need_args(args, 1, "States.ArrayLength")?;
519 let arr = arg_as_array(&args[0])?;
520 Ok(json!(arr.len()))
521}
522
523fn fn_array_unique(args: &[Value]) -> Result<Value, IntrinsicError> {
524 need_args(args, 1, "States.ArrayUnique")?;
525 let arr = arg_as_array(&args[0])?;
526 let mut seen: Vec<Value> = Vec::new();
527 for v in arr {
528 if !seen.contains(v) {
529 seen.push(v.clone());
530 }
531 }
532 Ok(Value::Array(seen))
533}
534
535fn fn_base64_encode(args: &[Value]) -> Result<Value, IntrinsicError> {
536 need_args(args, 1, "States.Base64Encode")?;
537 let s = arg_as_str(&args[0])?;
538 Ok(Value::String(
539 base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
540 ))
541}
542
543fn fn_base64_decode(args: &[Value]) -> Result<Value, IntrinsicError> {
544 need_args(args, 1, "States.Base64Decode")?;
545 let s = arg_as_str(&args[0])?;
546 let alphabet: Vec<u8> = s
549 .bytes()
550 .filter(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
551 .collect();
552 let bytes = base64::engine::general_purpose::STANDARD
553 .decode(&alphabet)
554 .map_err(|e| IntrinsicError(format!("Base64Decode failed: {e}")))?;
555 let decoded = String::from_utf8(bytes)
556 .map_err(|e| IntrinsicError(format!("Base64Decode utf8 failed: {e}")))?;
557 Ok(Value::String(decoded))
558}
559
560fn fn_hash(args: &[Value]) -> Result<Value, IntrinsicError> {
561 use md5::Digest;
562 need_args(args, 2, "States.Hash")?;
563 let input = arg_as_str(&args[0])?;
564 let algo = arg_as_str(&args[1])?;
565 let digest_hex = match algo.as_str() {
566 "MD5" => {
567 let mut h = md5::Md5::new();
568 h.update(input.as_bytes());
569 hex::encode(h.finalize())
570 }
571 "SHA-1" => {
572 let mut h = sha1::Sha1::new();
573 h.update(input.as_bytes());
574 hex::encode(h.finalize())
575 }
576 "SHA-256" => {
577 let mut h = sha2::Sha256::new();
578 h.update(input.as_bytes());
579 hex::encode(h.finalize())
580 }
581 "SHA-384" => {
582 let mut h = sha2::Sha384::new();
583 h.update(input.as_bytes());
584 hex::encode(h.finalize())
585 }
586 "SHA-512" => {
587 let mut h = sha2::Sha512::new();
588 h.update(input.as_bytes());
589 hex::encode(h.finalize())
590 }
591 other => {
592 return Err(IntrinsicError(format!(
593 "unsupported hash algorithm '{other}'"
594 )))
595 }
596 };
597 Ok(Value::String(digest_hex))
598}
599
600fn fn_json_merge(args: &[Value]) -> Result<Value, IntrinsicError> {
601 need_args(args, 3, "States.JsonMerge")?;
602 let a = args[0]
603 .as_object()
604 .ok_or_else(|| IntrinsicError("JsonMerge arg 1 must be object".into()))?;
605 let b = args[1]
606 .as_object()
607 .ok_or_else(|| IntrinsicError("JsonMerge arg 2 must be object".into()))?;
608 let deep = args[2]
609 .as_bool()
610 .ok_or_else(|| IntrinsicError("JsonMerge arg 3 must be bool".into()))?;
611 let mut merged = a.clone();
612 if deep {
613 deep_merge(&mut merged, b);
614 } else {
615 for (k, v) in b {
616 merged.insert(k.clone(), v.clone());
617 }
618 }
619 Ok(Value::Object(merged))
620}
621
622fn deep_merge(a: &mut serde_json::Map<String, Value>, b: &serde_json::Map<String, Value>) {
623 for (k, v) in b {
624 match (a.get_mut(k), v) {
625 (Some(Value::Object(am)), Value::Object(bm)) => deep_merge(am, bm),
626 _ => {
627 a.insert(k.clone(), v.clone());
628 }
629 }
630 }
631}
632
633fn fn_math_random(args: &[Value]) -> Result<Value, IntrinsicError> {
634 use rand::Rng;
635 if args.len() < 2 || args.len() > 3 {
636 return Err(IntrinsicError(
637 "States.MathRandom expected 2 or 3 args".into(),
638 ));
639 }
640 let start = arg_as_i64(&args[0])?;
641 let end = arg_as_i64(&args[1])?;
642 if end <= start {
643 return Err(IntrinsicError("MathRandom end must be > start".into()));
644 }
645 let v: i64 = if let Some(seed_v) = args.get(2) {
647 use rand::SeedableRng;
648 let seed = arg_as_i64(seed_v)? as u64;
649 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
650 rng.gen_range(start..end)
651 } else {
652 rand::thread_rng().gen_range(start..end)
653 };
654 Ok(json!(v))
655}
656
657fn fn_math_add(args: &[Value]) -> Result<Value, IntrinsicError> {
658 need_args(args, 2, "States.MathAdd")?;
659 if let (Some(a), Some(b)) = (args[0].as_i64(), args[1].as_i64()) {
663 return match a.checked_add(b) {
664 Some(sum) => Ok(json!(sum)),
665 None => Err(IntrinsicError(
666 "States.MathAdd result overflows a 64-bit integer".into(),
667 )),
668 };
669 }
670 let invalid = |_| IntrinsicError("Invalid arguments in States.MathAdd".into());
675 let a = arg_as_f64(&args[0]).map_err(invalid)?;
676 let b = arg_as_f64(&args[1]).map_err(invalid)?;
677 Ok(json!(a + b))
678}
679
680fn fn_uuid(args: &[Value]) -> Result<Value, IntrinsicError> {
681 need_args(args, 0, "States.UUID")?;
682 Ok(Value::String(uuid::Uuid::new_v4().to_string()))
683}
684
685fn fn_string_split(args: &[Value]) -> Result<Value, IntrinsicError> {
686 need_args(args, 2, "States.StringSplit")?;
687 let s = arg_as_str(&args[0])?;
688 let splitter = arg_as_str(&args[1])?;
689 if splitter.is_empty() {
690 return Err(IntrinsicError(
691 "StringSplit delimiter must be non-empty".into(),
692 ));
693 }
694 let chars: Vec<char> = splitter.chars().collect();
698 let parts: Vec<Value> = s
699 .split(|c: char| chars.contains(&c))
700 .filter(|p| !p.is_empty())
701 .map(|p| Value::String(p.to_string()))
702 .collect();
703 Ok(Value::Array(parts))
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use serde_json::json;
710
711 #[test]
712 fn format_substitutes_placeholders() {
713 let out = evaluate("States.Format('Hello, {}!', 'Alice')", &Value::Null).unwrap();
714 assert_eq!(out, json!("Hello, Alice!"));
715 }
716
717 #[test]
718 fn format_resolves_jsonpath_args() {
719 let input = json!({"name": "Bob", "n": 3});
720 let out = evaluate("States.Format('{}={}', $.name, $.n)", &input).unwrap();
721 assert_eq!(out, json!("Bob=3"));
722 }
723
724 #[test]
725 fn array_intrinsics() {
726 assert_eq!(
727 evaluate("States.Array(1, 2, 3)", &Value::Null).unwrap(),
728 json!([1, 2, 3])
729 );
730 assert_eq!(
731 evaluate("States.ArrayLength($)", &json!([10, 20, 30])).unwrap(),
732 json!(3)
733 );
734 assert_eq!(
735 evaluate("States.ArrayContains($, 2)", &json!([1, 2, 3])).unwrap(),
736 json!(true)
737 );
738 assert_eq!(
739 evaluate("States.ArrayContains($, 9)", &json!([1, 2, 3])).unwrap(),
740 json!(false)
741 );
742 assert_eq!(
743 evaluate("States.ArrayRange(1, 9, 2)", &Value::Null).unwrap(),
744 json!([1, 3, 5, 7, 9])
745 );
746 assert_eq!(
747 evaluate("States.ArrayPartition($, 2)", &json!([1, 2, 3, 4, 5])).unwrap(),
748 json!([[1, 2], [3, 4], [5]])
749 );
750 assert_eq!(
751 evaluate("States.ArrayGetItem($, 1)", &json!(["a", "b", "c"])).unwrap(),
752 json!("b")
753 );
754 assert_eq!(
755 evaluate("States.ArrayUnique($)", &json!([1, 2, 1, 3, 2])).unwrap(),
756 json!([1, 2, 3])
757 );
758 }
759
760 #[test]
761 fn json_intrinsics() {
762 assert_eq!(
763 evaluate("States.JsonToString($)", &json!({"x": 1})).unwrap(),
764 json!(r#"{"x":1}"#)
765 );
766 assert_eq!(
767 evaluate("States.StringToJson($)", &json!(r#"{"x":1}"#)).unwrap(),
768 json!({"x": 1})
769 );
770 assert_eq!(
771 evaluate(
772 "States.JsonMerge($.a, $.b, false)",
773 &json!({"a": {"x": 1, "y": 2}, "b": {"y": 9, "z": 3}})
774 )
775 .unwrap(),
776 json!({"x": 1, "y": 9, "z": 3})
777 );
778 }
779
780 #[test]
781 fn base64_intrinsics() {
782 let enc = evaluate("States.Base64Encode('hello')", &Value::Null).unwrap();
783 assert_eq!(enc, json!("aGVsbG8="));
784 let dec = evaluate("States.Base64Decode('aGVsbG8=')", &Value::Null).unwrap();
785 assert_eq!(dec, json!("hello"));
786 }
787
788 #[test]
789 fn hash_intrinsic() {
790 let out = evaluate("States.Hash('hello', 'SHA-256')", &Value::Null).unwrap();
791 assert_eq!(
792 out,
793 json!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
794 );
795 }
796
797 #[test]
798 fn math_intrinsics() {
799 assert_eq!(
800 evaluate("States.MathAdd(2, 3)", &Value::Null).unwrap(),
801 json!(5)
802 );
803 let r = evaluate("States.MathRandom(0, 10)", &Value::Null).unwrap();
804 let n = r.as_i64().unwrap();
805 assert!((0..10).contains(&n));
806 }
807
808 #[test]
811 fn math_add_overflow_and_floats() {
812 let expr = format!("States.MathAdd({}, 1)", i64::MAX);
814 assert!(evaluate(&expr, &Value::Null).is_err());
815
816 assert_eq!(
818 fn_math_add(&[json!(1.5), json!(2.25)]).unwrap(),
819 json!(3.75)
820 );
821 assert_eq!(fn_math_add(&[json!(2), json!(0.5)]).unwrap(), json!(2.5));
823 assert_eq!(fn_math_add(&[json!(-4), json!(1)]).unwrap(), json!(-3));
825 }
826
827 #[test]
828 fn uuid_intrinsic_is_v4() {
829 let out = evaluate("States.UUID()", &Value::Null).unwrap();
830 let s = out.as_str().unwrap();
831 assert_eq!(s.len(), 36);
833 assert_eq!(s.chars().nth(14).unwrap(), '4');
834 }
835
836 #[test]
837 fn string_split_intrinsic() {
838 assert_eq!(
839 evaluate("States.StringSplit('a,b,c', ',')", &Value::Null).unwrap(),
840 json!(["a", "b", "c"])
841 );
842 assert_eq!(
845 evaluate("States.StringSplit('a,b c', ', ')", &Value::Null).unwrap(),
846 json!(["a", "b", "c"])
847 );
848 }
849
850 #[test]
851 fn detects_intrinsic_call() {
852 assert!(is_intrinsic_call("States.UUID()"));
853 assert!(is_intrinsic_call("States.Format('{}', $.x)"));
854 assert!(!is_intrinsic_call("$.foo.bar"));
855 assert!(!is_intrinsic_call("States.IntrinsicFailure"));
856 }
857
858 #[test]
859 fn unknown_intrinsic_errors() {
860 let err = evaluate("States.NoSuchFunction()", &Value::Null).unwrap_err();
861 assert!(format!("{err}").contains("unknown"));
862 }
863
864 #[test]
865 fn array_range_extreme_operands_do_not_panic() {
866 let input = json!({"min": i64::MIN, "max": i64::MAX, "near": i64::MAX - 1});
867 let err = evaluate("States.ArrayRange($.min, $.max, 1)", &input).unwrap_err();
869 assert!(format!("{err}").contains("1000"), "{err}");
870 assert_eq!(
872 evaluate("States.ArrayRange($.near, $.max, $.max)", &input).unwrap(),
873 json!([i64::MAX - 1])
874 );
875 assert_eq!(
876 evaluate("States.ArrayRange($.min, $.max, $.max)", &input).unwrap(),
877 json!([i64::MIN, -1, i64::MAX - 1])
878 );
879 assert_eq!(
881 evaluate("States.ArrayRange($.max, $.min, $.min)", &input).unwrap(),
882 json!([i64::MAX, -1])
883 );
884 let ok = evaluate("States.ArrayRange(1, 1000, 1)", &Value::Null).unwrap();
886 assert_eq!(ok.as_array().unwrap().len(), 1000);
887 assert!(evaluate("States.ArrayRange(1, 1001, 1)", &Value::Null).is_err());
888 assert_eq!(
890 evaluate("States.ArrayRange(5, 1, 1)", &Value::Null).unwrap(),
891 json!([])
892 );
893 assert!(evaluate("States.ArrayRange(1, 5, 0)", &Value::Null).is_err());
894 }
895
896 #[test]
897 fn nested_intrinsics() {
898 let input = json!({"s": "a,b,c", "x": 7});
899 assert_eq!(
900 evaluate(
901 "States.ArrayGetItem(States.StringSplit($.s, ','), 1)",
902 &input
903 )
904 .unwrap(),
905 json!("b")
906 );
907 assert_eq!(
908 evaluate(
909 r#"States.StringToJson(States.Format('\{"a":\{\}, "n": {}\}', $.x))"#,
910 &input
911 )
912 .unwrap(),
913 json!({"a": {}, "n": 7})
914 );
915 assert_eq!(
916 evaluate(
917 "States.ArrayLength(States.Array(States.MathAdd($.x, 1), 'p,q', States.Array()))",
918 &input
919 )
920 .unwrap(),
921 json!(3)
922 );
923 }
924
925 #[test]
926 fn nesting_limit_is_ten() {
927 let nest = |n: usize| {
928 let mut e = "States.Array()".to_string();
929 for _ in 1..n {
930 e = format!("States.Array({e})");
931 }
932 e
933 };
934 assert!(evaluate(&nest(10), &Value::Null).is_ok());
935 let err = evaluate(&nest(11), &Value::Null).unwrap_err();
936 assert!(format!("{err}").contains("nested"), "{err}");
937 }
938
939 #[test]
940 fn string_literal_escapes() {
941 assert_eq!(
942 evaluate(r"States.Format('it\'s \\ a \{\} {}', 'x')", &Value::Null).unwrap(),
943 json!(r"it's \ a {} x")
944 );
945 assert_eq!(
947 evaluate("States.Array('a,b', '(c)')", &Value::Null).unwrap(),
948 json!(["a,b", "(c)"])
949 );
950 assert_eq!(
952 evaluate(r"States.StringToJson('\{\}')", &Value::Null).unwrap(),
953 json!({})
954 );
955 }
956
957 #[test]
958 fn path_arguments_with_brackets_and_filters() {
959 let input = json!({"items": [{"id": 1, "n": "a"}, {"id": 2, "n": "b"}], "k": {"x,y": 3}});
960 assert_eq!(
961 evaluate("States.ArrayLength($.items[?(@.id > 1)])", &input).unwrap(),
962 json!(1)
963 );
964 assert_eq!(
965 evaluate("States.Array($['k']['x,y'], $.items[-1].n)", &input).unwrap(),
966 json!([3, "b"])
967 );
968 }
969
970 #[test]
971 fn missing_path_argument_is_runtime_error() {
972 let call = "States.Format('{}', $.nope)";
973 let err = evaluate_with_context(call, &json!({}), None)
974 .unwrap_err()
975 .into_states_error(call);
976 assert_eq!(err.0, "States.Runtime");
977 let call = "States.ArrayGetItem($.a, -1)";
978 let err = evaluate_with_context(call, &json!({"a": [1]}), None)
979 .unwrap_err()
980 .into_states_error(call);
981 assert_eq!(err.0, "States.Runtime");
982 assert!(err.1.starts_with(
983 "There was an error while evaluating the intrinsic function: \
984 States.ArrayGetItem($.a, -1). "
985 ));
986 }
987
988 #[test]
990 fn base64_decode_skips_characters_outside_the_alphabet() {
991 assert_eq!(
992 evaluate("States.Base64Decode($.s)", &json!({"s": "%%%"})).unwrap(),
993 json!("")
994 );
995 assert_eq!(
996 evaluate("States.Base64Decode($.s)", &json!({"s": "aGk="})).unwrap(),
997 json!("hi")
998 );
999 }
1000
1001 #[test]
1003 fn failing_functions_read_as_recorded() {
1004 let cause = |call: &str, input: Value| {
1005 evaluate_with_context(call, &input, None)
1006 .unwrap_err()
1007 .into_states_error(call)
1008 .1
1009 };
1010 assert_eq!(
1011 cause("States.MathAdd($.s, 1)", json!({"s": "x"})),
1012 "There was an error while evaluating the intrinsic function: States.MathAdd($.s, 1). \
1013 Invalid arguments in States.MathAdd"
1014 );
1015 assert_eq!(
1016 cause("States.Format('{} {}', $.a)", json!({"a": "1"})),
1017 "There was an error while evaluating the intrinsic function: States.Format('{} {}', \
1018 $.a). Invalid arguments in States.Format: number of arguments do not match the \
1019 occurrences of {}."
1020 );
1021 assert_eq!(
1022 cause("States.ArrayRange(1, 2, 0)", json!({})),
1023 "There was an error while evaluating the intrinsic function: States.ArrayRange(1, 2, \
1024 0). Invalid arguments in States.ArrayRange"
1025 );
1026 assert!(cause("States.StringToJson($.x)", json!({"x": "not json"})).starts_with(
1027 "There was an error while evaluating the intrinsic function: States.StringToJson($.x). \
1028 Cannot convert String to Json in States.StringToJson, caused by: "
1029 ));
1030 }
1031
1032 #[test]
1033 fn context_path_arguments() {
1034 let ctx = json!({"Execution": {"Id": "arn:x"}});
1035 assert_eq!(
1036 evaluate_with_context(
1037 "States.Format('id={}', $$.Execution.Id)",
1038 &json!({}),
1039 Some(&ctx)
1040 )
1041 .unwrap(),
1042 json!("id=arn:x")
1043 );
1044 }
1045
1046 #[test]
1047 fn malformed_calls_error() {
1048 for bad in [
1049 "States.Format('{}'",
1050 "States.Array(1,",
1051 "States.Array(1) trailing",
1052 "States.Array('unterminated)",
1053 "States.Array(bogus)",
1054 ] {
1055 assert!(evaluate(bad, &Value::Null).is_err(), "{bad}");
1056 }
1057 }
1058}