1use base64::Engine;
12use serde_json::{json, Value};
13
14use crate::io_processing::resolve_path;
15
16#[derive(Debug, Clone)]
17pub struct IntrinsicError(pub String);
18
19impl std::fmt::Display for IntrinsicError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(f, "States.IntrinsicFailure: {}", self.0)
22 }
23}
24
25pub fn is_intrinsic_call(value: &str) -> bool {
28 value.starts_with("States.") && value.contains('(')
29}
30
31pub fn evaluate(call: &str, input: &Value) -> Result<Value, IntrinsicError> {
35 let (name, args_str) = split_call(call)?;
36 let args = parse_args(args_str, input)?;
37 match name {
38 "States.Format" => fn_format(&args),
39 "States.JsonToString" => fn_json_to_string(&args),
40 "States.StringToJson" => fn_string_to_json(&args),
41 "States.Array" => Ok(Value::Array(args)),
42 "States.ArrayPartition" => fn_array_partition(&args),
43 "States.ArrayContains" => fn_array_contains(&args),
44 "States.ArrayRange" => fn_array_range(&args),
45 "States.ArrayGetItem" => fn_array_get_item(&args),
46 "States.ArrayLength" => fn_array_length(&args),
47 "States.ArrayUnique" => fn_array_unique(&args),
48 "States.Base64Encode" => fn_base64_encode(&args),
49 "States.Base64Decode" => fn_base64_decode(&args),
50 "States.Hash" => fn_hash(&args),
51 "States.JsonMerge" => fn_json_merge(&args),
52 "States.MathRandom" => fn_math_random(&args),
53 "States.MathAdd" => fn_math_add(&args),
54 "States.UUID" => fn_uuid(&args),
55 "States.StringSplit" => fn_string_split(&args),
56 other => Err(IntrinsicError(format!("unknown intrinsic '{other}'"))),
57 }
58}
59
60fn split_call(call: &str) -> Result<(&str, &str), IntrinsicError> {
61 let open = call
62 .find('(')
63 .ok_or_else(|| IntrinsicError(format!("missing '(' in '{call}'")))?;
64 if !call.ends_with(')') {
65 return Err(IntrinsicError(format!("missing ')' in '{call}'")));
66 }
67 let name = &call[..open];
68 let args_str = &call[open + 1..call.len() - 1];
69 Ok((name, args_str))
70}
71
72fn parse_args(args_str: &str, input: &Value) -> Result<Vec<Value>, IntrinsicError> {
73 let mut out = Vec::new();
74 if args_str.trim().is_empty() {
75 return Ok(out);
76 }
77 for raw in split_top_level_commas(args_str) {
78 let arg = raw.trim();
79 if arg.is_empty() {
80 continue;
81 }
82 out.push(parse_arg(arg, input)?);
83 }
84 Ok(out)
85}
86
87fn split_top_level_commas(s: &str) -> Vec<String> {
91 let mut out = Vec::new();
92 let mut current = String::new();
93 let mut in_single = false;
94 let mut in_double = false;
95 let mut chars = s.chars().peekable();
96 while let Some(c) = chars.next() {
97 match c {
98 '\\' if in_single => {
99 if let Some(&next) = chars.peek() {
100 current.push('\\');
101 current.push(next);
102 chars.next();
103 }
104 }
105 '\'' if !in_double => {
106 in_single = !in_single;
107 current.push(c);
108 }
109 '"' if !in_single => {
110 in_double = !in_double;
111 current.push(c);
112 }
113 ',' if !in_single && !in_double => {
114 out.push(current.clone());
115 current.clear();
116 }
117 _ => current.push(c),
118 }
119 }
120 if !current.is_empty() || s.ends_with(',') {
121 out.push(current);
122 }
123 out
124}
125
126fn parse_arg(arg: &str, input: &Value) -> Result<Value, IntrinsicError> {
127 if arg.starts_with('$') {
128 Ok(resolve_path(input, arg))
129 } else if arg.starts_with('\'') && arg.ends_with('\'') && arg.len() >= 2 {
130 let inner = &arg[1..arg.len() - 1];
132 Ok(Value::String(unescape_single_quoted(inner)))
133 } else {
134 serde_json::from_str(arg)
137 .map_err(|e| IntrinsicError(format!("invalid argument '{arg}': {e}")))
138 }
139}
140
141fn unescape_single_quoted(s: &str) -> String {
142 let mut out = String::with_capacity(s.len());
143 let mut chars = s.chars().peekable();
144 while let Some(c) = chars.next() {
145 if c == '\\' {
146 match chars.next() {
147 Some('\\') => out.push('\\'),
148 Some('\'') => out.push('\''),
149 Some('n') => out.push('\n'),
150 Some('t') => out.push('\t'),
151 Some('{') => out.push('{'),
152 Some('}') => out.push('}'),
153 Some(other) => {
154 out.push('\\');
155 out.push(other);
156 }
157 None => out.push('\\'),
158 }
159 } else {
160 out.push(c);
161 }
162 }
163 out
164}
165
166fn arg_as_str(v: &Value) -> Result<String, IntrinsicError> {
167 match v {
168 Value::String(s) => Ok(s.clone()),
169 other => Ok(serde_json::to_string(other).unwrap_or_default()),
170 }
171}
172
173fn arg_as_array(v: &Value) -> Result<&Vec<Value>, IntrinsicError> {
174 v.as_array()
175 .ok_or_else(|| IntrinsicError(format!("expected array, got {v}")))
176}
177
178fn arg_as_i64(v: &Value) -> Result<i64, IntrinsicError> {
179 v.as_i64()
180 .or_else(|| v.as_f64().map(|f| f as i64))
181 .ok_or_else(|| IntrinsicError(format!("expected integer, got {v}")))
182}
183
184fn arg_as_f64(v: &Value) -> Result<f64, IntrinsicError> {
185 v.as_f64()
186 .ok_or_else(|| IntrinsicError(format!("expected number, got {v}")))
187}
188
189fn need_args(args: &[Value], expected: usize, name: &str) -> Result<(), IntrinsicError> {
190 if args.len() != expected {
191 Err(IntrinsicError(format!(
192 "{name} expected {expected} args, got {}",
193 args.len()
194 )))
195 } else {
196 Ok(())
197 }
198}
199
200fn fn_format(args: &[Value]) -> Result<Value, IntrinsicError> {
201 if args.is_empty() {
202 return Err(IntrinsicError(
203 "States.Format requires at least one argument".into(),
204 ));
205 }
206 let template = args[0]
207 .as_str()
208 .ok_or_else(|| IntrinsicError("States.Format template must be a string".into()))?;
209 let mut out = String::with_capacity(template.len());
210 let mut chars = template.chars().peekable();
211 let mut idx = 1;
212 while let Some(c) = chars.next() {
213 match c {
214 '\\' => {
215 if let Some(&n) = chars.peek() {
216 out.push(n);
217 chars.next();
218 }
219 }
220 '{' if matches!(chars.peek(), Some('}')) => {
221 chars.next();
222 let v = args.get(idx).ok_or_else(|| {
223 IntrinsicError("States.Format placeholder count exceeds args".into())
224 })?;
225 idx += 1;
226 match v {
227 Value::String(s) => out.push_str(s),
228 Value::Null => out.push_str("null"),
229 other => out.push_str(&serde_json::to_string(other).unwrap_or_default()),
230 }
231 }
232 _ => out.push(c),
233 }
234 }
235 Ok(Value::String(out))
236}
237
238fn fn_json_to_string(args: &[Value]) -> Result<Value, IntrinsicError> {
239 need_args(args, 1, "States.JsonToString")?;
240 Ok(Value::String(
241 serde_json::to_string(&args[0]).unwrap_or_default(),
242 ))
243}
244
245fn fn_string_to_json(args: &[Value]) -> Result<Value, IntrinsicError> {
246 need_args(args, 1, "States.StringToJson")?;
247 let s = args[0]
248 .as_str()
249 .ok_or_else(|| IntrinsicError("States.StringToJson arg must be a string".into()))?;
250 serde_json::from_str(s)
251 .map_err(|e| IntrinsicError(format!("States.StringToJson parse failed: {e}")))
252}
253
254fn fn_array_partition(args: &[Value]) -> Result<Value, IntrinsicError> {
255 need_args(args, 2, "States.ArrayPartition")?;
256 let arr = arg_as_array(&args[0])?;
257 let chunk = arg_as_i64(&args[1])?;
258 if chunk <= 0 {
259 return Err(IntrinsicError(
260 "ArrayPartition chunk size must be > 0".into(),
261 ));
262 }
263 let chunk = chunk as usize;
264 let mut out: Vec<Value> = Vec::new();
265 for slice in arr.chunks(chunk) {
266 out.push(Value::Array(slice.to_vec()));
267 }
268 Ok(Value::Array(out))
269}
270
271fn fn_array_contains(args: &[Value]) -> Result<Value, IntrinsicError> {
272 need_args(args, 2, "States.ArrayContains")?;
273 let arr = arg_as_array(&args[0])?;
274 Ok(Value::Bool(arr.iter().any(|v| v == &args[1])))
275}
276
277fn fn_array_range(args: &[Value]) -> Result<Value, IntrinsicError> {
278 need_args(args, 3, "States.ArrayRange")?;
279 let start = arg_as_i64(&args[0])?;
280 let end = arg_as_i64(&args[1])?;
281 let step = arg_as_i64(&args[2])?;
282 if step == 0 {
283 return Err(IntrinsicError("ArrayRange step must be != 0".into()));
284 }
285 let mut out = Vec::new();
286 let mut i = start;
287 if step > 0 {
288 while i <= end {
289 out.push(json!(i));
290 i += step;
291 }
292 } else {
293 while i >= end {
294 out.push(json!(i));
295 i += step;
296 }
297 }
298 Ok(Value::Array(out))
299}
300
301fn fn_array_get_item(args: &[Value]) -> Result<Value, IntrinsicError> {
302 need_args(args, 2, "States.ArrayGetItem")?;
303 let arr = arg_as_array(&args[0])?;
304 let idx = arg_as_i64(&args[1])?;
305 if idx < 0 {
306 return Err(IntrinsicError("ArrayGetItem index must be >= 0".into()));
307 }
308 Ok(arr.get(idx as usize).cloned().unwrap_or(Value::Null))
309}
310
311fn fn_array_length(args: &[Value]) -> Result<Value, IntrinsicError> {
312 need_args(args, 1, "States.ArrayLength")?;
313 let arr = arg_as_array(&args[0])?;
314 Ok(json!(arr.len()))
315}
316
317fn fn_array_unique(args: &[Value]) -> Result<Value, IntrinsicError> {
318 need_args(args, 1, "States.ArrayUnique")?;
319 let arr = arg_as_array(&args[0])?;
320 let mut seen: Vec<Value> = Vec::new();
321 for v in arr {
322 if !seen.contains(v) {
323 seen.push(v.clone());
324 }
325 }
326 Ok(Value::Array(seen))
327}
328
329fn fn_base64_encode(args: &[Value]) -> Result<Value, IntrinsicError> {
330 need_args(args, 1, "States.Base64Encode")?;
331 let s = arg_as_str(&args[0])?;
332 Ok(Value::String(
333 base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
334 ))
335}
336
337fn fn_base64_decode(args: &[Value]) -> Result<Value, IntrinsicError> {
338 need_args(args, 1, "States.Base64Decode")?;
339 let s = arg_as_str(&args[0])?;
340 let bytes = base64::engine::general_purpose::STANDARD
341 .decode(s.as_bytes())
342 .map_err(|e| IntrinsicError(format!("Base64Decode failed: {e}")))?;
343 let decoded = String::from_utf8(bytes)
344 .map_err(|e| IntrinsicError(format!("Base64Decode utf8 failed: {e}")))?;
345 Ok(Value::String(decoded))
346}
347
348fn fn_hash(args: &[Value]) -> Result<Value, IntrinsicError> {
349 use md5::Digest;
350 need_args(args, 2, "States.Hash")?;
351 let input = arg_as_str(&args[0])?;
352 let algo = arg_as_str(&args[1])?;
353 let digest_hex = match algo.as_str() {
354 "MD5" => {
355 let mut h = md5::Md5::new();
356 h.update(input.as_bytes());
357 hex::encode(h.finalize())
358 }
359 "SHA-1" => {
360 let mut h = sha1::Sha1::new();
361 h.update(input.as_bytes());
362 hex::encode(h.finalize())
363 }
364 "SHA-256" => {
365 let mut h = sha2::Sha256::new();
366 h.update(input.as_bytes());
367 hex::encode(h.finalize())
368 }
369 "SHA-384" => {
370 let mut h = sha2::Sha384::new();
371 h.update(input.as_bytes());
372 hex::encode(h.finalize())
373 }
374 "SHA-512" => {
375 let mut h = sha2::Sha512::new();
376 h.update(input.as_bytes());
377 hex::encode(h.finalize())
378 }
379 other => {
380 return Err(IntrinsicError(format!(
381 "unsupported hash algorithm '{other}'"
382 )))
383 }
384 };
385 Ok(Value::String(digest_hex))
386}
387
388fn fn_json_merge(args: &[Value]) -> Result<Value, IntrinsicError> {
389 need_args(args, 3, "States.JsonMerge")?;
390 let a = args[0]
391 .as_object()
392 .ok_or_else(|| IntrinsicError("JsonMerge arg 1 must be object".into()))?;
393 let b = args[1]
394 .as_object()
395 .ok_or_else(|| IntrinsicError("JsonMerge arg 2 must be object".into()))?;
396 let deep = args[2]
397 .as_bool()
398 .ok_or_else(|| IntrinsicError("JsonMerge arg 3 must be bool".into()))?;
399 let mut merged = a.clone();
400 if deep {
401 deep_merge(&mut merged, b);
402 } else {
403 for (k, v) in b {
404 merged.insert(k.clone(), v.clone());
405 }
406 }
407 Ok(Value::Object(merged))
408}
409
410fn deep_merge(a: &mut serde_json::Map<String, Value>, b: &serde_json::Map<String, Value>) {
411 for (k, v) in b {
412 match (a.get_mut(k), v) {
413 (Some(Value::Object(am)), Value::Object(bm)) => deep_merge(am, bm),
414 _ => {
415 a.insert(k.clone(), v.clone());
416 }
417 }
418 }
419}
420
421fn fn_math_random(args: &[Value]) -> Result<Value, IntrinsicError> {
422 use rand::Rng;
423 if args.len() < 2 || args.len() > 3 {
424 return Err(IntrinsicError(
425 "States.MathRandom expected 2 or 3 args".into(),
426 ));
427 }
428 let start = arg_as_i64(&args[0])?;
429 let end = arg_as_i64(&args[1])?;
430 if end <= start {
431 return Err(IntrinsicError("MathRandom end must be > start".into()));
432 }
433 let v: i64 = if let Some(seed_v) = args.get(2) {
435 use rand::SeedableRng;
436 let seed = arg_as_i64(seed_v)? as u64;
437 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
438 rng.gen_range(start..end)
439 } else {
440 rand::thread_rng().gen_range(start..end)
441 };
442 Ok(json!(v))
443}
444
445fn fn_math_add(args: &[Value]) -> Result<Value, IntrinsicError> {
446 need_args(args, 2, "States.MathAdd")?;
447 if let (Some(a), Some(b)) = (args[0].as_i64(), args[1].as_i64()) {
451 return match a.checked_add(b) {
452 Some(sum) => Ok(json!(sum)),
453 None => Err(IntrinsicError(
454 "States.MathAdd result overflows a 64-bit integer".into(),
455 )),
456 };
457 }
458 let a = arg_as_f64(&args[0])?;
462 let b = arg_as_f64(&args[1])?;
463 Ok(json!(a + b))
464}
465
466fn fn_uuid(args: &[Value]) -> Result<Value, IntrinsicError> {
467 need_args(args, 0, "States.UUID")?;
468 Ok(Value::String(uuid::Uuid::new_v4().to_string()))
469}
470
471fn fn_string_split(args: &[Value]) -> Result<Value, IntrinsicError> {
472 need_args(args, 2, "States.StringSplit")?;
473 let s = arg_as_str(&args[0])?;
474 let splitter = arg_as_str(&args[1])?;
475 if splitter.is_empty() {
476 return Err(IntrinsicError(
477 "StringSplit delimiter must be non-empty".into(),
478 ));
479 }
480 let chars: Vec<char> = splitter.chars().collect();
484 let parts: Vec<Value> = s
485 .split(|c: char| chars.contains(&c))
486 .filter(|p| !p.is_empty())
487 .map(|p| Value::String(p.to_string()))
488 .collect();
489 Ok(Value::Array(parts))
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use serde_json::json;
496
497 #[test]
498 fn format_substitutes_placeholders() {
499 let out = evaluate("States.Format('Hello, {}!', 'Alice')", &Value::Null).unwrap();
500 assert_eq!(out, json!("Hello, Alice!"));
501 }
502
503 #[test]
504 fn format_resolves_jsonpath_args() {
505 let input = json!({"name": "Bob", "n": 3});
506 let out = evaluate("States.Format('{}={}', $.name, $.n)", &input).unwrap();
507 assert_eq!(out, json!("Bob=3"));
508 }
509
510 #[test]
511 fn array_intrinsics() {
512 assert_eq!(
513 evaluate("States.Array(1, 2, 3)", &Value::Null).unwrap(),
514 json!([1, 2, 3])
515 );
516 assert_eq!(
517 evaluate("States.ArrayLength($)", &json!([10, 20, 30])).unwrap(),
518 json!(3)
519 );
520 assert_eq!(
521 evaluate("States.ArrayContains($, 2)", &json!([1, 2, 3])).unwrap(),
522 json!(true)
523 );
524 assert_eq!(
525 evaluate("States.ArrayContains($, 9)", &json!([1, 2, 3])).unwrap(),
526 json!(false)
527 );
528 assert_eq!(
529 evaluate("States.ArrayRange(1, 9, 2)", &Value::Null).unwrap(),
530 json!([1, 3, 5, 7, 9])
531 );
532 assert_eq!(
533 evaluate("States.ArrayPartition($, 2)", &json!([1, 2, 3, 4, 5])).unwrap(),
534 json!([[1, 2], [3, 4], [5]])
535 );
536 assert_eq!(
537 evaluate("States.ArrayGetItem($, 1)", &json!(["a", "b", "c"])).unwrap(),
538 json!("b")
539 );
540 assert_eq!(
541 evaluate("States.ArrayUnique($)", &json!([1, 2, 1, 3, 2])).unwrap(),
542 json!([1, 2, 3])
543 );
544 }
545
546 #[test]
547 fn json_intrinsics() {
548 assert_eq!(
549 evaluate("States.JsonToString($)", &json!({"x": 1})).unwrap(),
550 json!(r#"{"x":1}"#)
551 );
552 assert_eq!(
553 evaluate("States.StringToJson($)", &json!(r#"{"x":1}"#)).unwrap(),
554 json!({"x": 1})
555 );
556 assert_eq!(
557 evaluate(
558 "States.JsonMerge($.a, $.b, false)",
559 &json!({"a": {"x": 1, "y": 2}, "b": {"y": 9, "z": 3}})
560 )
561 .unwrap(),
562 json!({"x": 1, "y": 9, "z": 3})
563 );
564 }
565
566 #[test]
567 fn base64_intrinsics() {
568 let enc = evaluate("States.Base64Encode('hello')", &Value::Null).unwrap();
569 assert_eq!(enc, json!("aGVsbG8="));
570 let dec = evaluate("States.Base64Decode('aGVsbG8=')", &Value::Null).unwrap();
571 assert_eq!(dec, json!("hello"));
572 }
573
574 #[test]
575 fn hash_intrinsic() {
576 let out = evaluate("States.Hash('hello', 'SHA-256')", &Value::Null).unwrap();
577 assert_eq!(
578 out,
579 json!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
580 );
581 }
582
583 #[test]
584 fn math_intrinsics() {
585 assert_eq!(
586 evaluate("States.MathAdd(2, 3)", &Value::Null).unwrap(),
587 json!(5)
588 );
589 let r = evaluate("States.MathRandom(0, 10)", &Value::Null).unwrap();
590 let n = r.as_i64().unwrap();
591 assert!((0..10).contains(&n));
592 }
593
594 #[test]
597 fn math_add_overflow_and_floats() {
598 let expr = format!("States.MathAdd({}, 1)", i64::MAX);
600 assert!(evaluate(&expr, &Value::Null).is_err());
601
602 assert_eq!(
604 fn_math_add(&[json!(1.5), json!(2.25)]).unwrap(),
605 json!(3.75)
606 );
607 assert_eq!(fn_math_add(&[json!(2), json!(0.5)]).unwrap(), json!(2.5));
609 assert_eq!(fn_math_add(&[json!(-4), json!(1)]).unwrap(), json!(-3));
611 }
612
613 #[test]
614 fn uuid_intrinsic_is_v4() {
615 let out = evaluate("States.UUID()", &Value::Null).unwrap();
616 let s = out.as_str().unwrap();
617 assert_eq!(s.len(), 36);
619 assert_eq!(s.chars().nth(14).unwrap(), '4');
620 }
621
622 #[test]
623 fn string_split_intrinsic() {
624 assert_eq!(
625 evaluate("States.StringSplit('a,b,c', ',')", &Value::Null).unwrap(),
626 json!(["a", "b", "c"])
627 );
628 assert_eq!(
631 evaluate("States.StringSplit('a,b c', ', ')", &Value::Null).unwrap(),
632 json!(["a", "b", "c"])
633 );
634 }
635
636 #[test]
637 fn detects_intrinsic_call() {
638 assert!(is_intrinsic_call("States.UUID()"));
639 assert!(is_intrinsic_call("States.Format('{}', $.x)"));
640 assert!(!is_intrinsic_call("$.foo.bar"));
641 assert!(!is_intrinsic_call("States.IntrinsicFailure"));
642 }
643
644 #[test]
645 fn unknown_intrinsic_errors() {
646 let err = evaluate("States.NoSuchFunction()", &Value::Null).unwrap_err();
647 assert!(format!("{err}").contains("unknown"));
648 }
649}