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 need_args(args: &[Value], expected: usize, name: &str) -> Result<(), IntrinsicError> {
185 if args.len() != expected {
186 Err(IntrinsicError(format!(
187 "{name} expected {expected} args, got {}",
188 args.len()
189 )))
190 } else {
191 Ok(())
192 }
193}
194
195fn fn_format(args: &[Value]) -> Result<Value, IntrinsicError> {
196 if args.is_empty() {
197 return Err(IntrinsicError(
198 "States.Format requires at least one argument".into(),
199 ));
200 }
201 let template = args[0]
202 .as_str()
203 .ok_or_else(|| IntrinsicError("States.Format template must be a string".into()))?;
204 let mut out = String::with_capacity(template.len());
205 let mut chars = template.chars().peekable();
206 let mut idx = 1;
207 while let Some(c) = chars.next() {
208 match c {
209 '\\' => {
210 if let Some(&n) = chars.peek() {
211 out.push(n);
212 chars.next();
213 }
214 }
215 '{' if matches!(chars.peek(), Some('}')) => {
216 chars.next();
217 let v = args.get(idx).ok_or_else(|| {
218 IntrinsicError("States.Format placeholder count exceeds args".into())
219 })?;
220 idx += 1;
221 match v {
222 Value::String(s) => out.push_str(s),
223 Value::Null => out.push_str("null"),
224 other => out.push_str(&serde_json::to_string(other).unwrap_or_default()),
225 }
226 }
227 _ => out.push(c),
228 }
229 }
230 Ok(Value::String(out))
231}
232
233fn fn_json_to_string(args: &[Value]) -> Result<Value, IntrinsicError> {
234 need_args(args, 1, "States.JsonToString")?;
235 Ok(Value::String(
236 serde_json::to_string(&args[0]).unwrap_or_default(),
237 ))
238}
239
240fn fn_string_to_json(args: &[Value]) -> Result<Value, IntrinsicError> {
241 need_args(args, 1, "States.StringToJson")?;
242 let s = args[0]
243 .as_str()
244 .ok_or_else(|| IntrinsicError("States.StringToJson arg must be a string".into()))?;
245 serde_json::from_str(s)
246 .map_err(|e| IntrinsicError(format!("States.StringToJson parse failed: {e}")))
247}
248
249fn fn_array_partition(args: &[Value]) -> Result<Value, IntrinsicError> {
250 need_args(args, 2, "States.ArrayPartition")?;
251 let arr = arg_as_array(&args[0])?;
252 let chunk = arg_as_i64(&args[1])?;
253 if chunk <= 0 {
254 return Err(IntrinsicError(
255 "ArrayPartition chunk size must be > 0".into(),
256 ));
257 }
258 let chunk = chunk as usize;
259 let mut out: Vec<Value> = Vec::new();
260 for slice in arr.chunks(chunk) {
261 out.push(Value::Array(slice.to_vec()));
262 }
263 Ok(Value::Array(out))
264}
265
266fn fn_array_contains(args: &[Value]) -> Result<Value, IntrinsicError> {
267 need_args(args, 2, "States.ArrayContains")?;
268 let arr = arg_as_array(&args[0])?;
269 Ok(Value::Bool(arr.iter().any(|v| v == &args[1])))
270}
271
272fn fn_array_range(args: &[Value]) -> Result<Value, IntrinsicError> {
273 need_args(args, 3, "States.ArrayRange")?;
274 let start = arg_as_i64(&args[0])?;
275 let end = arg_as_i64(&args[1])?;
276 let step = arg_as_i64(&args[2])?;
277 if step == 0 {
278 return Err(IntrinsicError("ArrayRange step must be != 0".into()));
279 }
280 let mut out = Vec::new();
281 let mut i = start;
282 if step > 0 {
283 while i <= end {
284 out.push(json!(i));
285 i += step;
286 }
287 } else {
288 while i >= end {
289 out.push(json!(i));
290 i += step;
291 }
292 }
293 Ok(Value::Array(out))
294}
295
296fn fn_array_get_item(args: &[Value]) -> Result<Value, IntrinsicError> {
297 need_args(args, 2, "States.ArrayGetItem")?;
298 let arr = arg_as_array(&args[0])?;
299 let idx = arg_as_i64(&args[1])?;
300 if idx < 0 {
301 return Err(IntrinsicError("ArrayGetItem index must be >= 0".into()));
302 }
303 Ok(arr.get(idx as usize).cloned().unwrap_or(Value::Null))
304}
305
306fn fn_array_length(args: &[Value]) -> Result<Value, IntrinsicError> {
307 need_args(args, 1, "States.ArrayLength")?;
308 let arr = arg_as_array(&args[0])?;
309 Ok(json!(arr.len()))
310}
311
312fn fn_array_unique(args: &[Value]) -> Result<Value, IntrinsicError> {
313 need_args(args, 1, "States.ArrayUnique")?;
314 let arr = arg_as_array(&args[0])?;
315 let mut seen: Vec<Value> = Vec::new();
316 for v in arr {
317 if !seen.contains(v) {
318 seen.push(v.clone());
319 }
320 }
321 Ok(Value::Array(seen))
322}
323
324fn fn_base64_encode(args: &[Value]) -> Result<Value, IntrinsicError> {
325 need_args(args, 1, "States.Base64Encode")?;
326 let s = arg_as_str(&args[0])?;
327 Ok(Value::String(
328 base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
329 ))
330}
331
332fn fn_base64_decode(args: &[Value]) -> Result<Value, IntrinsicError> {
333 need_args(args, 1, "States.Base64Decode")?;
334 let s = arg_as_str(&args[0])?;
335 let bytes = base64::engine::general_purpose::STANDARD
336 .decode(s.as_bytes())
337 .map_err(|e| IntrinsicError(format!("Base64Decode failed: {e}")))?;
338 let decoded = String::from_utf8(bytes)
339 .map_err(|e| IntrinsicError(format!("Base64Decode utf8 failed: {e}")))?;
340 Ok(Value::String(decoded))
341}
342
343fn fn_hash(args: &[Value]) -> Result<Value, IntrinsicError> {
344 use md5::Digest;
345 need_args(args, 2, "States.Hash")?;
346 let input = arg_as_str(&args[0])?;
347 let algo = arg_as_str(&args[1])?;
348 let digest_hex = match algo.as_str() {
349 "MD5" => {
350 let mut h = md5::Md5::new();
351 h.update(input.as_bytes());
352 hex::encode(h.finalize())
353 }
354 "SHA-1" => {
355 let mut h = sha1::Sha1::new();
356 h.update(input.as_bytes());
357 hex::encode(h.finalize())
358 }
359 "SHA-256" => {
360 let mut h = sha2::Sha256::new();
361 h.update(input.as_bytes());
362 hex::encode(h.finalize())
363 }
364 "SHA-384" => {
365 let mut h = sha2::Sha384::new();
366 h.update(input.as_bytes());
367 hex::encode(h.finalize())
368 }
369 "SHA-512" => {
370 let mut h = sha2::Sha512::new();
371 h.update(input.as_bytes());
372 hex::encode(h.finalize())
373 }
374 other => {
375 return Err(IntrinsicError(format!(
376 "unsupported hash algorithm '{other}'"
377 )))
378 }
379 };
380 Ok(Value::String(digest_hex))
381}
382
383fn fn_json_merge(args: &[Value]) -> Result<Value, IntrinsicError> {
384 need_args(args, 3, "States.JsonMerge")?;
385 let a = args[0]
386 .as_object()
387 .ok_or_else(|| IntrinsicError("JsonMerge arg 1 must be object".into()))?;
388 let b = args[1]
389 .as_object()
390 .ok_or_else(|| IntrinsicError("JsonMerge arg 2 must be object".into()))?;
391 let deep = args[2]
392 .as_bool()
393 .ok_or_else(|| IntrinsicError("JsonMerge arg 3 must be bool".into()))?;
394 let mut merged = a.clone();
395 if deep {
396 deep_merge(&mut merged, b);
397 } else {
398 for (k, v) in b {
399 merged.insert(k.clone(), v.clone());
400 }
401 }
402 Ok(Value::Object(merged))
403}
404
405fn deep_merge(a: &mut serde_json::Map<String, Value>, b: &serde_json::Map<String, Value>) {
406 for (k, v) in b {
407 match (a.get_mut(k), v) {
408 (Some(Value::Object(am)), Value::Object(bm)) => deep_merge(am, bm),
409 _ => {
410 a.insert(k.clone(), v.clone());
411 }
412 }
413 }
414}
415
416fn fn_math_random(args: &[Value]) -> Result<Value, IntrinsicError> {
417 use rand::Rng;
418 if args.len() < 2 || args.len() > 3 {
419 return Err(IntrinsicError(
420 "States.MathRandom expected 2 or 3 args".into(),
421 ));
422 }
423 let start = arg_as_i64(&args[0])?;
424 let end = arg_as_i64(&args[1])?;
425 if end <= start {
426 return Err(IntrinsicError("MathRandom end must be > start".into()));
427 }
428 let v: i64 = if let Some(seed_v) = args.get(2) {
430 use rand::SeedableRng;
431 let seed = arg_as_i64(seed_v)? as u64;
432 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
433 rng.gen_range(start..end)
434 } else {
435 rand::thread_rng().gen_range(start..end)
436 };
437 Ok(json!(v))
438}
439
440fn fn_math_add(args: &[Value]) -> Result<Value, IntrinsicError> {
441 need_args(args, 2, "States.MathAdd")?;
442 let a = arg_as_i64(&args[0])?;
443 let b = arg_as_i64(&args[1])?;
444 Ok(json!(a + b))
445}
446
447fn fn_uuid(args: &[Value]) -> Result<Value, IntrinsicError> {
448 need_args(args, 0, "States.UUID")?;
449 Ok(Value::String(uuid::Uuid::new_v4().to_string()))
450}
451
452fn fn_string_split(args: &[Value]) -> Result<Value, IntrinsicError> {
453 need_args(args, 2, "States.StringSplit")?;
454 let s = arg_as_str(&args[0])?;
455 let splitter = arg_as_str(&args[1])?;
456 if splitter.is_empty() {
457 return Err(IntrinsicError(
458 "StringSplit delimiter must be non-empty".into(),
459 ));
460 }
461 let chars: Vec<char> = splitter.chars().collect();
465 let parts: Vec<Value> = s
466 .split(|c: char| chars.contains(&c))
467 .filter(|p| !p.is_empty())
468 .map(|p| Value::String(p.to_string()))
469 .collect();
470 Ok(Value::Array(parts))
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use serde_json::json;
477
478 #[test]
479 fn format_substitutes_placeholders() {
480 let out = evaluate("States.Format('Hello, {}!', 'Alice')", &Value::Null).unwrap();
481 assert_eq!(out, json!("Hello, Alice!"));
482 }
483
484 #[test]
485 fn format_resolves_jsonpath_args() {
486 let input = json!({"name": "Bob", "n": 3});
487 let out = evaluate("States.Format('{}={}', $.name, $.n)", &input).unwrap();
488 assert_eq!(out, json!("Bob=3"));
489 }
490
491 #[test]
492 fn array_intrinsics() {
493 assert_eq!(
494 evaluate("States.Array(1, 2, 3)", &Value::Null).unwrap(),
495 json!([1, 2, 3])
496 );
497 assert_eq!(
498 evaluate("States.ArrayLength($)", &json!([10, 20, 30])).unwrap(),
499 json!(3)
500 );
501 assert_eq!(
502 evaluate("States.ArrayContains($, 2)", &json!([1, 2, 3])).unwrap(),
503 json!(true)
504 );
505 assert_eq!(
506 evaluate("States.ArrayContains($, 9)", &json!([1, 2, 3])).unwrap(),
507 json!(false)
508 );
509 assert_eq!(
510 evaluate("States.ArrayRange(1, 9, 2)", &Value::Null).unwrap(),
511 json!([1, 3, 5, 7, 9])
512 );
513 assert_eq!(
514 evaluate("States.ArrayPartition($, 2)", &json!([1, 2, 3, 4, 5])).unwrap(),
515 json!([[1, 2], [3, 4], [5]])
516 );
517 assert_eq!(
518 evaluate("States.ArrayGetItem($, 1)", &json!(["a", "b", "c"])).unwrap(),
519 json!("b")
520 );
521 assert_eq!(
522 evaluate("States.ArrayUnique($)", &json!([1, 2, 1, 3, 2])).unwrap(),
523 json!([1, 2, 3])
524 );
525 }
526
527 #[test]
528 fn json_intrinsics() {
529 assert_eq!(
530 evaluate("States.JsonToString($)", &json!({"x": 1})).unwrap(),
531 json!(r#"{"x":1}"#)
532 );
533 assert_eq!(
534 evaluate("States.StringToJson($)", &json!(r#"{"x":1}"#)).unwrap(),
535 json!({"x": 1})
536 );
537 assert_eq!(
538 evaluate(
539 "States.JsonMerge($.a, $.b, false)",
540 &json!({"a": {"x": 1, "y": 2}, "b": {"y": 9, "z": 3}})
541 )
542 .unwrap(),
543 json!({"x": 1, "y": 9, "z": 3})
544 );
545 }
546
547 #[test]
548 fn base64_intrinsics() {
549 let enc = evaluate("States.Base64Encode('hello')", &Value::Null).unwrap();
550 assert_eq!(enc, json!("aGVsbG8="));
551 let dec = evaluate("States.Base64Decode('aGVsbG8=')", &Value::Null).unwrap();
552 assert_eq!(dec, json!("hello"));
553 }
554
555 #[test]
556 fn hash_intrinsic() {
557 let out = evaluate("States.Hash('hello', 'SHA-256')", &Value::Null).unwrap();
558 assert_eq!(
559 out,
560 json!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
561 );
562 }
563
564 #[test]
565 fn math_intrinsics() {
566 assert_eq!(
567 evaluate("States.MathAdd(2, 3)", &Value::Null).unwrap(),
568 json!(5)
569 );
570 let r = evaluate("States.MathRandom(0, 10)", &Value::Null).unwrap();
571 let n = r.as_i64().unwrap();
572 assert!((0..10).contains(&n));
573 }
574
575 #[test]
576 fn uuid_intrinsic_is_v4() {
577 let out = evaluate("States.UUID()", &Value::Null).unwrap();
578 let s = out.as_str().unwrap();
579 assert_eq!(s.len(), 36);
581 assert_eq!(s.chars().nth(14).unwrap(), '4');
582 }
583
584 #[test]
585 fn string_split_intrinsic() {
586 assert_eq!(
587 evaluate("States.StringSplit('a,b,c', ',')", &Value::Null).unwrap(),
588 json!(["a", "b", "c"])
589 );
590 assert_eq!(
593 evaluate("States.StringSplit('a,b c', ', ')", &Value::Null).unwrap(),
594 json!(["a", "b", "c"])
595 );
596 }
597
598 #[test]
599 fn detects_intrinsic_call() {
600 assert!(is_intrinsic_call("States.UUID()"));
601 assert!(is_intrinsic_call("States.Format('{}', $.x)"));
602 assert!(!is_intrinsic_call("$.foo.bar"));
603 assert!(!is_intrinsic_call("States.IntrinsicFailure"));
604 }
605
606 #[test]
607 fn unknown_intrinsic_errors() {
608 let err = evaluate("States.NoSuchFunction()", &Value::Null).unwrap_err();
609 assert!(format!("{err}").contains("unknown"));
610 }
611}