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, "States.IntrinsicFailure: {}", 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) -> (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
60pub 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
70pub 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#[derive(Debug, Clone)]
91enum Expr {
92 Call {
93 name: String,
94 args: Vec<Expr>,
95 },
96 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
153struct 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; 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 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
321fn 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
365fn 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
451const 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 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 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 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 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 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 #[test]
777 fn math_add_overflow_and_floats() {
778 let expr = format!("States.MathAdd({}, 1)", i64::MAX);
780 assert!(evaluate(&expr, &Value::Null).is_err());
781
782 assert_eq!(
784 fn_math_add(&[json!(1.5), json!(2.25)]).unwrap(),
785 json!(3.75)
786 );
787 assert_eq!(fn_math_add(&[json!(2), json!(0.5)]).unwrap(), json!(2.5));
789 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 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 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 let err = evaluate("States.ArrayRange($.min, $.max, 1)", &input).unwrap_err();
835 assert!(format!("{err}").contains("1000"), "{err}");
836 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 assert_eq!(
847 evaluate("States.ArrayRange($.max, $.min, $.min)", &input).unwrap(),
848 json!([i64::MAX, -1])
849 );
850 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 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 assert_eq!(
913 evaluate("States.Array('a,b', '(c)')", &Value::Null).unwrap(),
914 json!(["a,b", "(c)"])
915 );
916 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}