Skip to main content

fakecloud_stepfunctions/
intrinsics.rs

1//! Amazon States Language intrinsic functions.
2//!
3//! These appear in Parameters / ResultSelector / Arguments / Output
4//! values when the JSON key uses the `.$` suffix and the value is a
5//! string starting with `States.`. This module parses the call,
6//! resolves arguments (JSONPath references vs JSON literals), and
7//! returns the computed value.
8//!
9//! Reference: https://docs.aws.amazon.com/step-functions/latest/dg/intrinsic-functions.html
10
11use 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, "States.IntrinsicFailure: {}", self.0)
20    }
21}
22
23/// Returns true if `value` is a string that should be evaluated as an
24/// intrinsic (`States.Foo(...)`) rather than a JSONPath reference.
25pub fn is_intrinsic_call(value: &str) -> bool {
26    value.starts_with("States.") && value.contains('(')
27}
28
29/// Maximum nesting depth of intrinsic calls within one field. AWS: "You can
30/// nest up to 10 intrinsic functions within a field."
31const MAX_NESTING: usize = 10;
32
33/// Failure evaluating an intrinsic expression.
34#[derive(Debug, Clone)]
35pub enum CallError {
36    /// The function itself failed (bad arguments, unknown function, ...):
37    /// surfaced as `States.IntrinsicFailure`.
38    Intrinsic(IntrinsicError),
39    /// A JSONPath argument matched nothing: surfaced as `States.Runtime`, as
40    /// for every other path that matches nothing.
41    Path(String),
42}
43
44impl CallError {
45    /// The `(error, cause)` pair the state fails with.
46    pub fn into_states_error(self) -> (String, String) {
47        match self {
48            CallError::Intrinsic(e) => ("States.IntrinsicFailure".to_string(), e.0),
49            CallError::Path(cause) => ("States.Runtime".to_string(), cause),
50        }
51    }
52}
53
54impl From<IntrinsicError> for CallError {
55    fn from(e: IntrinsicError) -> Self {
56        CallError::Intrinsic(e)
57    }
58}
59
60/// Evaluate an ASL intrinsic call against `input`. Returns the
61/// computed value or an error string suitable for surfacing as
62/// `States.IntrinsicFailure`.
63pub fn evaluate(call: &str, input: &Value) -> Result<Value, IntrinsicError> {
64    evaluate_with_context(call, input, None).map_err(|e| match e {
65        CallError::Intrinsic(e) => e,
66        CallError::Path(cause) => IntrinsicError(cause),
67    })
68}
69
70/// Evaluate an intrinsic call whose `$$` arguments read `context` (the
71/// Step Functions context object) when one is supplied.
72pub fn evaluate_with_context(
73    call: &str,
74    input: &Value,
75    context: Option<&Value>,
76) -> Result<Value, CallError> {
77    let mut parser = CallParser {
78        chars: call.trim().chars().collect(),
79        pos: 0,
80    };
81    let expr = parser.call(1)?;
82    parser.skip_ws();
83    if parser.pos != parser.chars.len() {
84        return Err(IntrinsicError(format!("unexpected trailing input in '{call}'")).into());
85    }
86    eval_expr(&expr, input, context)
87}
88
89/// A parsed intrinsic argument.
90#[derive(Debug, Clone)]
91enum Expr {
92    Call {
93        name: String,
94        args: Vec<Expr>,
95    },
96    /// A single-quoted string literal: its unescaped value, and the raw text
97    /// between the quotes (States.Format needs the escapes to tell `\{\}`
98    /// apart from a `{}` placeholder).
99    Str {
100        value: String,
101        raw: String,
102    },
103    Path(String),
104    Literal(Value),
105}
106
107fn eval_expr(expr: &Expr, input: &Value, context: Option<&Value>) -> Result<Value, CallError> {
108    match expr {
109        Expr::Literal(v) => Ok(v.clone()),
110        Expr::Str { value, .. } => Ok(Value::String(value.clone())),
111        Expr::Path(path) => crate::io_processing::resolve_with_context(input, context, path)
112            .map_err(|e| CallError::Path(crate::io_processing::path_error_cause(path, &e))),
113        Expr::Call { name, args } => {
114            let values = args
115                .iter()
116                .map(|a| eval_expr(a, input, context))
117                .collect::<Result<Vec<_>, _>>()?;
118            if name == "States.Format" {
119                let raw = match args.first() {
120                    Some(Expr::Str { raw, .. }) => Some(raw.as_str()),
121                    _ => None,
122                };
123                return Ok(fn_format(raw, &values)?);
124            }
125            Ok(call_function(name, &values)?)
126        }
127    }
128}
129
130fn call_function(name: &str, args: &[Value]) -> Result<Value, IntrinsicError> {
131    match name {
132        "States.JsonToString" => fn_json_to_string(args),
133        "States.StringToJson" => fn_string_to_json(args),
134        "States.Array" => Ok(Value::Array(args.to_vec())),
135        "States.ArrayPartition" => fn_array_partition(args),
136        "States.ArrayContains" => fn_array_contains(args),
137        "States.ArrayRange" => fn_array_range(args),
138        "States.ArrayGetItem" => fn_array_get_item(args),
139        "States.ArrayLength" => fn_array_length(args),
140        "States.ArrayUnique" => fn_array_unique(args),
141        "States.Base64Encode" => fn_base64_encode(args),
142        "States.Base64Decode" => fn_base64_decode(args),
143        "States.Hash" => fn_hash(args),
144        "States.JsonMerge" => fn_json_merge(args),
145        "States.MathRandom" => fn_math_random(args),
146        "States.MathAdd" => fn_math_add(args),
147        "States.UUID" => fn_uuid(args),
148        "States.StringSplit" => fn_string_split(args),
149        other => Err(IntrinsicError(format!("unknown intrinsic '{other}'"))),
150    }
151}
152
153/// Recursive-descent parser for `States.Fn(arg, ...)` expressions. Arguments
154/// are nested calls, single-quoted strings (escapes `\'`, `\{`, `\}`, `\\`),
155/// JSONPath references (`$...` / `$$...`, which may themselves contain
156/// brackets, parentheses, commas and quotes inside filters) or JSON literals
157/// (numbers, `true`, `false`, `null`).
158struct CallParser {
159    chars: Vec<char>,
160    pos: usize,
161}
162
163impl CallParser {
164    fn peek(&self) -> Option<char> {
165        self.chars.get(self.pos).copied()
166    }
167
168    fn skip_ws(&mut self) {
169        while self.peek().is_some_and(char::is_whitespace) {
170            self.pos += 1;
171        }
172    }
173
174    fn rest_starts_with(&self, s: &str) -> bool {
175        let n = s.chars().count();
176        self.chars
177            .get(self.pos..self.pos + n)
178            .is_some_and(|w| w.iter().copied().eq(s.chars()))
179    }
180
181    fn text(&self) -> String {
182        self.chars.iter().collect()
183    }
184
185    fn err<T>(&self, msg: &str) -> Result<T, IntrinsicError> {
186        Err(IntrinsicError(format!(
187            "{msg} at position {} in '{}'",
188            self.pos,
189            self.text()
190        )))
191    }
192
193    fn call(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
194        if depth > MAX_NESTING {
195            return Err(IntrinsicError(format!(
196                "intrinsic functions can be nested at most {MAX_NESTING} levels deep in '{}'",
197                self.text()
198            )));
199        }
200        self.skip_ws();
201        if !self.rest_starts_with("States.") {
202            return self.err("expected an intrinsic function");
203        }
204        let start = self.pos;
205        while self
206            .peek()
207            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '.')
208        {
209            self.pos += 1;
210        }
211        let name: String = self.chars[start..self.pos].iter().collect();
212        self.skip_ws();
213        if self.peek() != Some('(') {
214            return Err(IntrinsicError(format!("missing '(' in '{}'", self.text())));
215        }
216        self.pos += 1;
217        let mut args = Vec::new();
218        self.skip_ws();
219        if self.peek() == Some(')') {
220            self.pos += 1;
221            return Ok(Expr::Call { name, args });
222        }
223        loop {
224            args.push(self.arg(depth)?);
225            self.skip_ws();
226            match self.peek() {
227                Some(',') => self.pos += 1,
228                Some(')') => {
229                    self.pos += 1;
230                    return Ok(Expr::Call { name, args });
231                }
232                None => return Err(IntrinsicError(format!("missing ')' in '{}'", self.text()))),
233                Some(_) => return self.err("expected ',' or ')'"),
234            }
235        }
236    }
237
238    fn arg(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
239        self.skip_ws();
240        match self.peek() {
241            Some('S') if self.rest_starts_with("States.") => self.call(depth + 1),
242            Some('\'') => self.string(),
243            Some('$') => self.path(),
244            Some(_) => self.literal(),
245            None => self.err("expected an argument"),
246        }
247    }
248
249    fn string(&mut self) -> Result<Expr, IntrinsicError> {
250        self.pos += 1; // opening quote
251        let mut value = String::new();
252        let mut raw = String::new();
253        loop {
254            match self.peek() {
255                None => return self.err("unterminated string literal"),
256                Some('\\') => {
257                    let Some(next) = self.chars.get(self.pos + 1).copied() else {
258                        return self.err("unterminated escape sequence");
259                    };
260                    raw.push('\\');
261                    raw.push(next);
262                    value.push_str(&unescape_char(next));
263                    self.pos += 2;
264                }
265                Some('\'') => {
266                    self.pos += 1;
267                    return Ok(Expr::Str { value, raw });
268                }
269                Some(c) => {
270                    value.push(c);
271                    raw.push(c);
272                    self.pos += 1;
273                }
274            }
275        }
276    }
277
278    /// A JSONPath runs to the next top-level `,` or `)`; brackets,
279    /// parentheses and quotes inside it (filters, quoted names) are skipped.
280    fn path(&mut self) -> Result<Expr, IntrinsicError> {
281        let start = self.pos;
282        let mut depth = 0usize;
283        let mut quote: Option<char> = None;
284        while let Some(c) = self.peek() {
285            if let Some(q) = quote {
286                if c == '\\' {
287                    self.pos += 1;
288                } else if c == q {
289                    quote = None;
290                }
291            } else {
292                match c {
293                    '\'' | '"' if depth > 0 => quote = Some(c),
294                    '[' | '(' => depth += 1,
295                    ']' | ')' if depth > 0 => depth -= 1,
296                    ',' | ')' if depth == 0 => break,
297                    _ => {}
298                }
299            }
300            self.pos += 1;
301        }
302        let path: String = self.chars[start..self.pos.min(self.chars.len())]
303            .iter()
304            .collect();
305        Ok(Expr::Path(path.trim_end().to_string()))
306    }
307
308    fn literal(&mut self) -> Result<Expr, IntrinsicError> {
309        let start = self.pos;
310        while self.peek().is_some_and(|c| c != ',' && c != ')') {
311            self.pos += 1;
312        }
313        let raw: String = self.chars[start..self.pos].iter().collect();
314        let raw = raw.trim();
315        serde_json::from_str::<Value>(raw)
316            .map(Expr::Literal)
317            .map_err(|e| IntrinsicError(format!("invalid argument '{raw}': {e}")))
318    }
319}
320
321/// Value of the character following a backslash in a string literal.
322fn unescape_char(c: char) -> String {
323    match c {
324        '\\' | '\'' | '{' | '}' => c.to_string(),
325        'n' => "\n".to_string(),
326        't' => "\t".to_string(),
327        other => format!("\\{other}"),
328    }
329}
330
331fn arg_as_str(v: &Value) -> Result<String, IntrinsicError> {
332    match v {
333        Value::String(s) => Ok(s.clone()),
334        other => Ok(serde_json::to_string(other).unwrap_or_default()),
335    }
336}
337
338fn arg_as_array(v: &Value) -> Result<&Vec<Value>, IntrinsicError> {
339    v.as_array()
340        .ok_or_else(|| IntrinsicError(format!("expected array, got {v}")))
341}
342
343fn arg_as_i64(v: &Value) -> Result<i64, IntrinsicError> {
344    v.as_i64()
345        .or_else(|| v.as_f64().map(|f| f as i64))
346        .ok_or_else(|| IntrinsicError(format!("expected integer, got {v}")))
347}
348
349fn arg_as_f64(v: &Value) -> Result<f64, IntrinsicError> {
350    v.as_f64()
351        .ok_or_else(|| IntrinsicError(format!("expected number, got {v}")))
352}
353
354fn need_args(args: &[Value], expected: usize, name: &str) -> Result<(), IntrinsicError> {
355    if args.len() != expected {
356        Err(IntrinsicError(format!(
357            "{name} expected {expected} args, got {}",
358            args.len()
359        )))
360    } else {
361        Ok(())
362    }
363}
364
365/// `States.Format(template, args...)`. Each `{}` in the template is
366/// replaced by the next argument. In a string-literal template, `\{` and `\}`
367/// are literal braces (so `'\{\}'` is not a placeholder); `raw` carries the
368/// literal's escaped text for that. A template from a path or nested call is
369/// used as-is.
370fn fn_format(raw: Option<&str>, args: &[Value]) -> Result<Value, IntrinsicError> {
371    if args.is_empty() {
372        return Err(IntrinsicError(
373            "States.Format requires at least one argument".into(),
374        ));
375    }
376    let template = match raw {
377        Some(raw) => raw.to_string(),
378        None => args[0]
379            .as_str()
380            .ok_or_else(|| IntrinsicError("States.Format template must be a string".into()))?
381            .to_string(),
382    };
383    let honour_escapes = raw.is_some();
384    let mut out = String::with_capacity(template.len());
385    let mut chars = template.chars().peekable();
386    let mut idx = 1;
387    while let Some(c) = chars.next() {
388        match c {
389            '\\' if honour_escapes => {
390                if let Some(n) = chars.next() {
391                    out.push_str(&unescape_char(n));
392                }
393            }
394            '{' if matches!(chars.peek(), Some('}')) => {
395                chars.next();
396                let v = args.get(idx).ok_or_else(|| {
397                    IntrinsicError("States.Format placeholder count exceeds args".into())
398                })?;
399                idx += 1;
400                match v {
401                    Value::String(s) => out.push_str(s),
402                    Value::Null => out.push_str("null"),
403                    other => out.push_str(&serde_json::to_string(other).unwrap_or_default()),
404                }
405            }
406            _ => out.push(c),
407        }
408    }
409    Ok(Value::String(out))
410}
411
412fn fn_json_to_string(args: &[Value]) -> Result<Value, IntrinsicError> {
413    need_args(args, 1, "States.JsonToString")?;
414    Ok(Value::String(
415        serde_json::to_string(&args[0]).unwrap_or_default(),
416    ))
417}
418
419fn fn_string_to_json(args: &[Value]) -> Result<Value, IntrinsicError> {
420    need_args(args, 1, "States.StringToJson")?;
421    let s = args[0]
422        .as_str()
423        .ok_or_else(|| IntrinsicError("States.StringToJson arg must be a string".into()))?;
424    serde_json::from_str(s)
425        .map_err(|e| IntrinsicError(format!("States.StringToJson parse failed: {e}")))
426}
427
428fn fn_array_partition(args: &[Value]) -> Result<Value, IntrinsicError> {
429    need_args(args, 2, "States.ArrayPartition")?;
430    let arr = arg_as_array(&args[0])?;
431    let chunk = arg_as_i64(&args[1])?;
432    if chunk <= 0 {
433        return Err(IntrinsicError(
434            "ArrayPartition chunk size must be > 0".into(),
435        ));
436    }
437    let chunk = chunk as usize;
438    let mut out: Vec<Value> = Vec::new();
439    for slice in arr.chunks(chunk) {
440        out.push(Value::Array(slice.to_vec()));
441    }
442    Ok(Value::Array(out))
443}
444
445fn fn_array_contains(args: &[Value]) -> Result<Value, IntrinsicError> {
446    need_args(args, 2, "States.ArrayContains")?;
447    let arr = arg_as_array(&args[0])?;
448    Ok(Value::Bool(arr.iter().any(|v| v == &args[1])))
449}
450
451/// AWS: "The array returned by States.ArrayRange can contain up to 1000 items."
452const ARRAY_RANGE_MAX_ITEMS: i128 = 1000;
453
454fn fn_array_range(args: &[Value]) -> Result<Value, IntrinsicError> {
455    need_args(args, 3, "States.ArrayRange")?;
456    let start = arg_as_i64(&args[0])?;
457    let end = arg_as_i64(&args[1])?;
458    let step = arg_as_i64(&args[2])?;
459    if step == 0 {
460        return Err(IntrinsicError("ArrayRange step must be != 0".into()));
461    }
462    // Compute the element count in i128 so extreme operands cannot overflow,
463    // and enforce the AWS cap before allocating anything.
464    let span = i128::from(end) - i128::from(start);
465    let count: i128 = if (step > 0 && span < 0) || (step < 0 && span > 0) {
466        0
467    } else {
468        span / i128::from(step) + 1
469    };
470    if count > ARRAY_RANGE_MAX_ITEMS {
471        return Err(IntrinsicError(format!(
472            "ArrayRange result exceeds the maximum of {ARRAY_RANGE_MAX_ITEMS} items"
473        )));
474    }
475    let out: Vec<Value> = (0..count)
476        .map(|k| json!((i128::from(start) + k * i128::from(step)) as i64))
477        .collect();
478    Ok(Value::Array(out))
479}
480
481fn fn_array_get_item(args: &[Value]) -> Result<Value, IntrinsicError> {
482    need_args(args, 2, "States.ArrayGetItem")?;
483    let arr = arg_as_array(&args[0])?;
484    let idx = arg_as_i64(&args[1])?;
485    if idx < 0 {
486        return Err(IntrinsicError("ArrayGetItem index must be >= 0".into()));
487    }
488    Ok(arr.get(idx as usize).cloned().unwrap_or(Value::Null))
489}
490
491fn fn_array_length(args: &[Value]) -> Result<Value, IntrinsicError> {
492    need_args(args, 1, "States.ArrayLength")?;
493    let arr = arg_as_array(&args[0])?;
494    Ok(json!(arr.len()))
495}
496
497fn fn_array_unique(args: &[Value]) -> Result<Value, IntrinsicError> {
498    need_args(args, 1, "States.ArrayUnique")?;
499    let arr = arg_as_array(&args[0])?;
500    let mut seen: Vec<Value> = Vec::new();
501    for v in arr {
502        if !seen.contains(v) {
503            seen.push(v.clone());
504        }
505    }
506    Ok(Value::Array(seen))
507}
508
509fn fn_base64_encode(args: &[Value]) -> Result<Value, IntrinsicError> {
510    need_args(args, 1, "States.Base64Encode")?;
511    let s = arg_as_str(&args[0])?;
512    Ok(Value::String(
513        base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
514    ))
515}
516
517fn fn_base64_decode(args: &[Value]) -> Result<Value, IntrinsicError> {
518    need_args(args, 1, "States.Base64Decode")?;
519    let s = arg_as_str(&args[0])?;
520    let bytes = base64::engine::general_purpose::STANDARD
521        .decode(s.as_bytes())
522        .map_err(|e| IntrinsicError(format!("Base64Decode failed: {e}")))?;
523    let decoded = String::from_utf8(bytes)
524        .map_err(|e| IntrinsicError(format!("Base64Decode utf8 failed: {e}")))?;
525    Ok(Value::String(decoded))
526}
527
528fn fn_hash(args: &[Value]) -> Result<Value, IntrinsicError> {
529    use md5::Digest;
530    need_args(args, 2, "States.Hash")?;
531    let input = arg_as_str(&args[0])?;
532    let algo = arg_as_str(&args[1])?;
533    let digest_hex = match algo.as_str() {
534        "MD5" => {
535            let mut h = md5::Md5::new();
536            h.update(input.as_bytes());
537            hex::encode(h.finalize())
538        }
539        "SHA-1" => {
540            let mut h = sha1::Sha1::new();
541            h.update(input.as_bytes());
542            hex::encode(h.finalize())
543        }
544        "SHA-256" => {
545            let mut h = sha2::Sha256::new();
546            h.update(input.as_bytes());
547            hex::encode(h.finalize())
548        }
549        "SHA-384" => {
550            let mut h = sha2::Sha384::new();
551            h.update(input.as_bytes());
552            hex::encode(h.finalize())
553        }
554        "SHA-512" => {
555            let mut h = sha2::Sha512::new();
556            h.update(input.as_bytes());
557            hex::encode(h.finalize())
558        }
559        other => {
560            return Err(IntrinsicError(format!(
561                "unsupported hash algorithm '{other}'"
562            )))
563        }
564    };
565    Ok(Value::String(digest_hex))
566}
567
568fn fn_json_merge(args: &[Value]) -> Result<Value, IntrinsicError> {
569    need_args(args, 3, "States.JsonMerge")?;
570    let a = args[0]
571        .as_object()
572        .ok_or_else(|| IntrinsicError("JsonMerge arg 1 must be object".into()))?;
573    let b = args[1]
574        .as_object()
575        .ok_or_else(|| IntrinsicError("JsonMerge arg 2 must be object".into()))?;
576    let deep = args[2]
577        .as_bool()
578        .ok_or_else(|| IntrinsicError("JsonMerge arg 3 must be bool".into()))?;
579    let mut merged = a.clone();
580    if deep {
581        deep_merge(&mut merged, b);
582    } else {
583        for (k, v) in b {
584            merged.insert(k.clone(), v.clone());
585        }
586    }
587    Ok(Value::Object(merged))
588}
589
590fn deep_merge(a: &mut serde_json::Map<String, Value>, b: &serde_json::Map<String, Value>) {
591    for (k, v) in b {
592        match (a.get_mut(k), v) {
593            (Some(Value::Object(am)), Value::Object(bm)) => deep_merge(am, bm),
594            _ => {
595                a.insert(k.clone(), v.clone());
596            }
597        }
598    }
599}
600
601fn fn_math_random(args: &[Value]) -> Result<Value, IntrinsicError> {
602    use rand::Rng;
603    if args.len() < 2 || args.len() > 3 {
604        return Err(IntrinsicError(
605            "States.MathRandom expected 2 or 3 args".into(),
606        ));
607    }
608    let start = arg_as_i64(&args[0])?;
609    let end = arg_as_i64(&args[1])?;
610    if end <= start {
611        return Err(IntrinsicError("MathRandom end must be > start".into()));
612    }
613    // 3rd arg is an optional seed; we honour it for deterministic tests.
614    let v: i64 = if let Some(seed_v) = args.get(2) {
615        use rand::SeedableRng;
616        let seed = arg_as_i64(seed_v)? as u64;
617        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
618        rng.gen_range(start..end)
619    } else {
620        rand::thread_rng().gen_range(start..end)
621    };
622    Ok(json!(v))
623}
624
625fn fn_math_add(args: &[Value]) -> Result<Value, IntrinsicError> {
626    need_args(args, 2, "States.MathAdd")?;
627    // Integer operands add with 64-bit integer semantics, but a genuine
628    // overflow errors instead of panicking (debug) or silently wrapping
629    // (release).
630    if let (Some(a), Some(b)) = (args[0].as_i64(), args[1].as_i64()) {
631        return match a.checked_add(b) {
632            Some(sum) => Ok(json!(sum)),
633            None => Err(IntrinsicError(
634                "States.MathAdd result overflows a 64-bit integer".into(),
635            )),
636        };
637    }
638    // Otherwise fall back to floating-point addition. This covers fractional
639    // operands (which the old i64 coercion truncated) and integers too large
640    // for i64. AWS treats numbers as given, so no truncation is applied.
641    let a = arg_as_f64(&args[0])?;
642    let b = arg_as_f64(&args[1])?;
643    Ok(json!(a + b))
644}
645
646fn fn_uuid(args: &[Value]) -> Result<Value, IntrinsicError> {
647    need_args(args, 0, "States.UUID")?;
648    Ok(Value::String(uuid::Uuid::new_v4().to_string()))
649}
650
651fn fn_string_split(args: &[Value]) -> Result<Value, IntrinsicError> {
652    need_args(args, 2, "States.StringSplit")?;
653    let s = arg_as_str(&args[0])?;
654    let splitter = arg_as_str(&args[1])?;
655    if splitter.is_empty() {
656        return Err(IntrinsicError(
657            "StringSplit delimiter must be non-empty".into(),
658        ));
659    }
660    // ASL StringSplit treats every char in the delimiter as a possible
661    // separator (eg. delimiter "., " splits on either dot, comma, or
662    // space) and drops empty tokens.
663    let chars: Vec<char> = splitter.chars().collect();
664    let parts: Vec<Value> = s
665        .split(|c: char| chars.contains(&c))
666        .filter(|p| !p.is_empty())
667        .map(|p| Value::String(p.to_string()))
668        .collect();
669    Ok(Value::Array(parts))
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675    use serde_json::json;
676
677    #[test]
678    fn format_substitutes_placeholders() {
679        let out = evaluate("States.Format('Hello, {}!', 'Alice')", &Value::Null).unwrap();
680        assert_eq!(out, json!("Hello, Alice!"));
681    }
682
683    #[test]
684    fn format_resolves_jsonpath_args() {
685        let input = json!({"name": "Bob", "n": 3});
686        let out = evaluate("States.Format('{}={}', $.name, $.n)", &input).unwrap();
687        assert_eq!(out, json!("Bob=3"));
688    }
689
690    #[test]
691    fn array_intrinsics() {
692        assert_eq!(
693            evaluate("States.Array(1, 2, 3)", &Value::Null).unwrap(),
694            json!([1, 2, 3])
695        );
696        assert_eq!(
697            evaluate("States.ArrayLength($)", &json!([10, 20, 30])).unwrap(),
698            json!(3)
699        );
700        assert_eq!(
701            evaluate("States.ArrayContains($, 2)", &json!([1, 2, 3])).unwrap(),
702            json!(true)
703        );
704        assert_eq!(
705            evaluate("States.ArrayContains($, 9)", &json!([1, 2, 3])).unwrap(),
706            json!(false)
707        );
708        assert_eq!(
709            evaluate("States.ArrayRange(1, 9, 2)", &Value::Null).unwrap(),
710            json!([1, 3, 5, 7, 9])
711        );
712        assert_eq!(
713            evaluate("States.ArrayPartition($, 2)", &json!([1, 2, 3, 4, 5])).unwrap(),
714            json!([[1, 2], [3, 4], [5]])
715        );
716        assert_eq!(
717            evaluate("States.ArrayGetItem($, 1)", &json!(["a", "b", "c"])).unwrap(),
718            json!("b")
719        );
720        assert_eq!(
721            evaluate("States.ArrayUnique($)", &json!([1, 2, 1, 3, 2])).unwrap(),
722            json!([1, 2, 3])
723        );
724    }
725
726    #[test]
727    fn json_intrinsics() {
728        assert_eq!(
729            evaluate("States.JsonToString($)", &json!({"x": 1})).unwrap(),
730            json!(r#"{"x":1}"#)
731        );
732        assert_eq!(
733            evaluate("States.StringToJson($)", &json!(r#"{"x":1}"#)).unwrap(),
734            json!({"x": 1})
735        );
736        assert_eq!(
737            evaluate(
738                "States.JsonMerge($.a, $.b, false)",
739                &json!({"a": {"x": 1, "y": 2}, "b": {"y": 9, "z": 3}})
740            )
741            .unwrap(),
742            json!({"x": 1, "y": 9, "z": 3})
743        );
744    }
745
746    #[test]
747    fn base64_intrinsics() {
748        let enc = evaluate("States.Base64Encode('hello')", &Value::Null).unwrap();
749        assert_eq!(enc, json!("aGVsbG8="));
750        let dec = evaluate("States.Base64Decode('aGVsbG8=')", &Value::Null).unwrap();
751        assert_eq!(dec, json!("hello"));
752    }
753
754    #[test]
755    fn hash_intrinsic() {
756        let out = evaluate("States.Hash('hello', 'SHA-256')", &Value::Null).unwrap();
757        assert_eq!(
758            out,
759            json!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
760        );
761    }
762
763    #[test]
764    fn math_intrinsics() {
765        assert_eq!(
766            evaluate("States.MathAdd(2, 3)", &Value::Null).unwrap(),
767            json!(5)
768        );
769        let r = evaluate("States.MathRandom(0, 10)", &Value::Null).unwrap();
770        let n = r.as_i64().unwrap();
771        assert!((0..10).contains(&n));
772    }
773
774    // L6: MathAdd must not panic (debug) or wrap (release) on i64 overflow, and
775    // must not truncate fractional operands.
776    #[test]
777    fn math_add_overflow_and_floats() {
778        // i64::MAX + 1 overflows → error rather than panic/wrap.
779        let expr = format!("States.MathAdd({}, 1)", i64::MAX);
780        assert!(evaluate(&expr, &Value::Null).is_err());
781
782        // Fractional operands are preserved, not truncated to int.
783        assert_eq!(
784            fn_math_add(&[json!(1.5), json!(2.25)]).unwrap(),
785            json!(3.75)
786        );
787        // Mixed int + float stays a float.
788        assert_eq!(fn_math_add(&[json!(2), json!(0.5)]).unwrap(), json!(2.5));
789        // Negative integers still work.
790        assert_eq!(fn_math_add(&[json!(-4), json!(1)]).unwrap(), json!(-3));
791    }
792
793    #[test]
794    fn uuid_intrinsic_is_v4() {
795        let out = evaluate("States.UUID()", &Value::Null).unwrap();
796        let s = out.as_str().unwrap();
797        // 8-4-4-4-12 = 36 chars total
798        assert_eq!(s.len(), 36);
799        assert_eq!(s.chars().nth(14).unwrap(), '4');
800    }
801
802    #[test]
803    fn string_split_intrinsic() {
804        assert_eq!(
805            evaluate("States.StringSplit('a,b,c', ',')", &Value::Null).unwrap(),
806            json!(["a", "b", "c"])
807        );
808        // Multi-char delimiter splits on any contained char and drops
809        // empties.
810        assert_eq!(
811            evaluate("States.StringSplit('a,b c', ', ')", &Value::Null).unwrap(),
812            json!(["a", "b", "c"])
813        );
814    }
815
816    #[test]
817    fn detects_intrinsic_call() {
818        assert!(is_intrinsic_call("States.UUID()"));
819        assert!(is_intrinsic_call("States.Format('{}', $.x)"));
820        assert!(!is_intrinsic_call("$.foo.bar"));
821        assert!(!is_intrinsic_call("States.IntrinsicFailure"));
822    }
823
824    #[test]
825    fn unknown_intrinsic_errors() {
826        let err = evaluate("States.NoSuchFunction()", &Value::Null).unwrap_err();
827        assert!(format!("{err}").contains("unknown"));
828    }
829
830    #[test]
831    fn array_range_extreme_operands_do_not_panic() {
832        let input = json!({"min": i64::MIN, "max": i64::MAX, "near": i64::MAX - 1});
833        // Unbounded span is rejected by the 1000-item cap, not by OOM.
834        let err = evaluate("States.ArrayRange($.min, $.max, 1)", &input).unwrap_err();
835        assert!(format!("{err}").contains("1000"), "{err}");
836        // A step that would overflow `i += step` yields the in-range items only.
837        assert_eq!(
838            evaluate("States.ArrayRange($.near, $.max, $.max)", &input).unwrap(),
839            json!([i64::MAX - 1])
840        );
841        assert_eq!(
842            evaluate("States.ArrayRange($.min, $.max, $.max)", &input).unwrap(),
843            json!([i64::MIN, -1, i64::MAX - 1])
844        );
845        // Negative step down to i64::MIN.
846        assert_eq!(
847            evaluate("States.ArrayRange($.max, $.min, $.min)", &input).unwrap(),
848            json!([i64::MAX, -1])
849        );
850        // Exactly 1000 items is allowed; 1001 is not.
851        let ok = evaluate("States.ArrayRange(1, 1000, 1)", &Value::Null).unwrap();
852        assert_eq!(ok.as_array().unwrap().len(), 1000);
853        assert!(evaluate("States.ArrayRange(1, 1001, 1)", &Value::Null).is_err());
854        // Wrong direction is an empty array.
855        assert_eq!(
856            evaluate("States.ArrayRange(5, 1, 1)", &Value::Null).unwrap(),
857            json!([])
858        );
859        assert!(evaluate("States.ArrayRange(1, 5, 0)", &Value::Null).is_err());
860    }
861
862    #[test]
863    fn nested_intrinsics() {
864        let input = json!({"s": "a,b,c", "x": 7});
865        assert_eq!(
866            evaluate(
867                "States.ArrayGetItem(States.StringSplit($.s, ','), 1)",
868                &input
869            )
870            .unwrap(),
871            json!("b")
872        );
873        assert_eq!(
874            evaluate(
875                r#"States.StringToJson(States.Format('\{"a":\{\}, "n": {}\}', $.x))"#,
876                &input
877            )
878            .unwrap(),
879            json!({"a": {}, "n": 7})
880        );
881        assert_eq!(
882            evaluate(
883                "States.ArrayLength(States.Array(States.MathAdd($.x, 1), 'p,q', States.Array()))",
884                &input
885            )
886            .unwrap(),
887            json!(3)
888        );
889    }
890
891    #[test]
892    fn nesting_limit_is_ten() {
893        let nest = |n: usize| {
894            let mut e = "States.Array()".to_string();
895            for _ in 1..n {
896                e = format!("States.Array({e})");
897            }
898            e
899        };
900        assert!(evaluate(&nest(10), &Value::Null).is_ok());
901        let err = evaluate(&nest(11), &Value::Null).unwrap_err();
902        assert!(format!("{err}").contains("nested"), "{err}");
903    }
904
905    #[test]
906    fn string_literal_escapes() {
907        assert_eq!(
908            evaluate(r"States.Format('it\'s \\ a \{\} {}', 'x')", &Value::Null).unwrap(),
909            json!(r"it's \ a {} x")
910        );
911        // Commas and parens inside strings never split arguments.
912        assert_eq!(
913            evaluate("States.Array('a,b', '(c)')", &Value::Null).unwrap(),
914            json!(["a,b", "(c)"])
915        );
916        // Escaped braces in a non-Format literal are plain braces.
917        assert_eq!(
918            evaluate(r"States.StringToJson('\{\}')", &Value::Null).unwrap(),
919            json!({})
920        );
921    }
922
923    #[test]
924    fn path_arguments_with_brackets_and_filters() {
925        let input = json!({"items": [{"id": 1, "n": "a"}, {"id": 2, "n": "b"}], "k": {"x,y": 3}});
926        assert_eq!(
927            evaluate("States.ArrayLength($.items[?(@.id > 1)])", &input).unwrap(),
928            json!(1)
929        );
930        assert_eq!(
931            evaluate("States.Array($['k']['x,y'], $.items[-1].n)", &input).unwrap(),
932            json!([3, "b"])
933        );
934    }
935
936    #[test]
937    fn missing_path_argument_is_runtime_error() {
938        let err = evaluate_with_context("States.Format('{}', $.nope)", &json!({}), None)
939            .unwrap_err()
940            .into_states_error();
941        assert_eq!(err.0, "States.Runtime");
942        let err = evaluate_with_context("States.ArrayGetItem($.a, -1)", &json!({"a": [1]}), None)
943            .unwrap_err()
944            .into_states_error();
945        assert_eq!(err.0, "States.IntrinsicFailure");
946    }
947
948    #[test]
949    fn context_path_arguments() {
950        let ctx = json!({"Execution": {"Id": "arn:x"}});
951        assert_eq!(
952            evaluate_with_context(
953                "States.Format('id={}', $$.Execution.Id)",
954                &json!({}),
955                Some(&ctx)
956            )
957            .unwrap(),
958            json!("id=arn:x")
959        );
960    }
961
962    #[test]
963    fn malformed_calls_error() {
964        for bad in [
965            "States.Format('{}'",
966            "States.Array(1,",
967            "States.Array(1) trailing",
968            "States.Array('unterminated)",
969            "States.Array(bogus)",
970        ] {
971            assert!(evaluate(bad, &Value::Null).is_err(), "{bad}");
972        }
973    }
974}