Skip to content

Commit 9165a2a

Browse files
committed
small doc and local runner fixes
1 parent 4e45bfb commit 9165a2a

14 files changed

Lines changed: 123 additions & 69 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ npx simplesteps compile workflow.ts -o output/ --query-language jsonpath
229229
| [CLI Reference](docs/cli.md) | `simplesteps compile` flags |
230230
| [Services](docs/services.md) | AWS service bindings + `Steps.awsSdk()` |
231231
| [Error Handling](docs/error-handling.md) | try/catch, retry, custom errors |
232-
| [Intrinsic Functions](docs/intrinsic-functions.md) | All 18 ASL intrinsics |
232+
| [Intrinsic Functions](docs/intrinsic-functions.md) | All 19 ASL intrinsics |
233233
| [Language Reference](docs/language-reference.md) | Every TypeScript construct mapped |
234234
| [Constants](docs/constants.md) | Compile-time constant folding |
235235
| [Limitations](docs/limitations.md) | Unsupported patterns and workarounds |

docs/intrinsic-functions.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Intrinsic Functions
22

3-
SimpleSteps maps all 18 ASL intrinsic functions to TypeScript. **Write natural JavaScript** — the compiler handles the mapping. `Steps.*` methods are also available for direct access (useful in JSONPath mode or when no JS equivalent exists).
3+
SimpleSteps maps all 18 standard ASL intrinsic functions (plus the `States.ArraySlice` AWS extension) to TypeScript. **Write natural JavaScript** — the compiler handles the mapping. `Steps.*` methods are also available for direct access (useful in JSONPath mode or when no JS equivalent exists).
44

55
## Summary
66

@@ -22,6 +22,7 @@ SimpleSteps maps all 18 ASL intrinsic functions to TypeScript. **Write natural J
2222
| -- | `States.ArrayPartition` | `Steps.arrayPartition(arr, n)` |
2323
| -- | `States.ArrayRange` | `Steps.arrayRange(start, end, step)` |
2424
| -- | `States.ArrayUnique` | `Steps.arrayUnique(arr)` |
25+
| -- | `States.ArraySlice` | `Steps.arraySlice(arr, start, end)` |
2526
| -- | `States.Hash` | `Steps.hash(data, algorithm)` |
2627
| -- | `States.MathRandom` | `Steps.random(start, end)` |
2728

@@ -75,9 +76,10 @@ const count = items.length; // States.ArrayLength($.items)
7576
const parts = csvLine.split(','); // States.StringSplit($.csvLine, ',')
7677

7778
// Steps.* only (no JS equivalent)
78-
const chunks = Steps.partition(items, 4); // States.ArrayPartition($.items, 4)
79-
const indices = Steps.range(0, 100, 10); // States.ArrayRange(0, 100, 10)
80-
const deduped = Steps.unique(items); // States.ArrayUnique($.items)
79+
const chunks = Steps.arrayPartition(items, 4); // States.ArrayPartition($.items, 4)
80+
const indices = Steps.arrayRange(0, 100, 10); // States.ArrayRange(0, 100, 10)
81+
const deduped = Steps.arrayUnique(items); // States.ArrayUnique($.items)
82+
const page = Steps.arraySlice(items, 0, 10); // States.ArraySlice($.items, 0, 10)
8183
```
8284

8385
## Encoding

docs/language-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ The context object is optional — if your workflow doesn't need execution metad
480480

481481
## Intrinsic Functions
482482

483-
See [Intrinsic Functions](./intrinsic-functions.md) for all 18 ASL intrinsics and their JS mappings.
483+
See [Intrinsic Functions](./intrinsic-functions.md) for all 19 ASL intrinsics and their JS mappings.
484484

485485
## Error Handling
486486

docs/limitations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,11 @@ For AWS services not covered by the built-in bindings:
418418

419419
```typescript
420420
const obj = await Steps.awsSdk<{ Bucket: string; Key: string }, { Body: string }>(
421-
'S3', 'GetObject', { Bucket: 'my-bucket', Key: input.key }
421+
's3', 'getObject', { Bucket: 'my-bucket', Key: input.key }
422422
);
423423
```
424424

425-
Compiles to `Resource: "arn:aws:states:::aws-sdk:s3:GetObject"`.
425+
Compiles to `Resource: "arn:aws:states:::aws-sdk:s3:getObject"`.
426426

427427
## Query Language Modes
428428

docs/services.md

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ await Steps.awsSdk('ses', 'sendEmail', {
368368
});
369369

370370
// Bedrock
371-
const response = await Steps.awsSdk<BedrockResponse>('bedrock', 'invokeModel', {
371+
const response = await Steps.awsSdk<BedrockParams, BedrockResponse>('bedrock', 'invokeModel', {
372372
ModelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
373373
Body: JSON.stringify({ prompt: input.prompt }),
374374
});
@@ -502,26 +502,4 @@ export const workflow = Steps.createFunction(
502502

503503
The activity ARN is used directly as the Task state `Resource`. Retry, timeout, and heartbeat options are supported.
504504

505-
---
506-
507-
## Steps.awsSdk() — Direct SDK Integration
508-
509-
For any AWS service not covered by typed bindings, use `Steps.awsSdk()`:
510-
511-
```typescript
512-
const result = await Steps.awsSdk('s3', 'listObjectsV2', {
513-
Bucket: input.bucket,
514-
Prefix: 'data/',
515-
});
516-
517-
// SNS example
518-
await Steps.awsSdk('sns', 'publish', {
519-
TopicArn: 'arn:aws:sns:us-east-1:123:MyTopic',
520-
Message: input.message,
521-
});
522-
```
523-
524-
This compiles to `Resource: "arn:aws:states:::aws-sdk:s3:listObjectsV2"` with the parameters as Task `Arguments` (JSONata) or `Parameters` (JSONPath).
525-
526-
The service and action arguments must be string literals (not variables) so the compiler can construct the ARN at compile time.
527505

packages/cdk/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class SimpleStepsStateMachine extends sfn.StateMachine {
183183
throw new Error(
184184
'Inline workflows require the SimpleSteps TypeScript transformer. ' +
185185
'Add the transformer to your build pipeline. ' +
186-
'See: https://github.com/DevNamedZed/simplesteps#transformer-setup',
186+
'See: https://github.com/DevNamedZed/simplesteps/blob/main/docs/cdk-integration.md',
187187
);
188188
}
189189

packages/core/src/compiler/analysis/variableResolver.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,9 @@ const STEPS_TO_INTRINSIC: Record<string, string> = {
661661
arrayLength: 'States.ArrayLength',
662662
arrayUnique: 'States.ArrayUnique',
663663
arraySlice: 'States.ArraySlice',
664+
partition: 'States.ArrayPartition',
665+
unique: 'States.ArrayUnique',
666+
range: 'States.ArrayRange',
664667
jsonParse: 'States.StringToJson',
665668
jsonStringify: 'States.JsonToString',
666669
merge: 'States.JsonMerge',
@@ -1079,6 +1082,9 @@ function resolveCallExpression(
10791082
// JS replace() with string pattern replaces first occurrence only; $replace limit=1
10801083
return { kind: 'intrinsic', path: `$replace(${baseStr}, ${patStr}, ${repStr}, 1)` };
10811084
}
1085+
// Pattern or replacement couldn't be resolved (e.g. regex literal)
1086+
context.addError(expr, 'str.replace() requires string arguments. RegExp patterns are not supported.', ErrorCodes.Expr.UncompilableExpression.code);
1087+
return { kind: 'unknown' };
10821088
}
10831089
return jsonataOnlyError(context, expr, 'str.replace()');
10841090
}
@@ -1257,6 +1263,12 @@ function resolveCallExpression(
12571263
}
12581264
}
12591265

1266+
// arr.reduce(fn) without initial value — specific error
1267+
if (methodName === 'reduce' && expr.arguments.length === 1) {
1268+
context.addError(expr, 'Array.reduce() requires an initial value in SimpleSteps. Use arr.reduce(fn, initialValue).', ErrorCodes.Expr.UncompilableExpression.code);
1269+
return { kind: 'unknown' };
1270+
}
1271+
12601272
// arr.reduce(fn, init) → $reduce(arr, function($prev, $v) { expr }, init)
12611273
if (methodName === 'reduce' && expr.arguments.length === 2) {
12621274
const cbArg = expr.arguments[0];
@@ -1529,7 +1541,7 @@ function resolveJsonataStepsCall(
15291541
if (serialized.length === 2) return { kind: 'intrinsic', path: `${serialized[0]} + ${serialized[1]}` };
15301542
break;
15311543
case 'random':
1532-
if (serialized.length === 2) return { kind: 'intrinsic', path: `$floor($random() * (${serialized[1]} - ${serialized[0]})) + ${serialized[0]}` };
1544+
if (serialized.length === 2) return { kind: 'intrinsic', path: `$floor($random() * (${serialized[1]} - ${serialized[0]} + 1)) + ${serialized[0]}` };
15331545
break;
15341546
case 'base64Encode':
15351547
if (serialized.length === 1) return { kind: 'intrinsic', path: `$base64encode(${serialized[0]})` };
@@ -1549,6 +1561,7 @@ function resolveJsonataStepsCall(
15491561
if (serialized.length === 1) return { kind: 'intrinsic', path: `$count(${serialized[0]})` };
15501562
break;
15511563
case 'arrayUnique':
1564+
case 'unique':
15521565
if (serialized.length === 1) return { kind: 'intrinsic', path: `$distinct(${serialized[0]})` };
15531566
break;
15541567
case 'jsonParse':
@@ -1568,7 +1581,9 @@ function resolveJsonataStepsCall(
15681581
// Keep as States.* intrinsics (no clean JSONata equivalent)
15691582
case 'hash':
15701583
case 'arrayPartition':
1584+
case 'partition':
15711585
case 'arrayRange':
1586+
case 'range':
15721587
case 'arraySlice': {
15731588
const intrinsicName = STEPS_TO_INTRINSIC[methodName];
15741589
if (intrinsicName) {
@@ -1652,6 +1667,7 @@ function resolveObjectLiteral(
16521667
if (ts.isSpreadAssignment(prop)) {
16531668
spreadArgs.push(resolveExpression(context, prop.expression, variables, dialect));
16541669
} else {
1670+
context.addError(prop, 'Cannot mix spread and non-spread properties. Use either {...a, ...b} or {x: 1, y: 2}.', ErrorCodes.Expr.UncompilableExpression.code);
16551671
return { kind: 'unknown' };
16561672
}
16571673
}

packages/local/src/choiceEvaluator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@ function evaluateComparisonRule(
111111
* '*' matches zero or more characters. NOT a regex.
112112
*/
113113
function stringMatches(value: string, pattern: string): boolean {
114-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
115-
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
114+
// Handle \* escape sequences (literal asterisk) before general regex escaping
115+
const PLACEHOLDER = '\x00STAR\x00';
116+
const withPlaceholder = pattern.replace(/\\\*/g, PLACEHOLDER);
117+
const escaped = withPlaceholder.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
118+
const regexStr = escaped.replace(/\*/g, '.*').split(PLACEHOLDER).join('\\*');
119+
const regex = new RegExp('^' + regexStr + '$');
116120
return regex.test(value);
117121
}
118122

packages/local/src/interpreter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export async function interpret(
9999
// Update per-state context
100100
context.State.Name = currentStateName;
101101
context.State.EnteredTime = new Date().toISOString();
102+
context.State.RetryCount = 0;
102103

103104
const start = Date.now();
104105
let result: StepResult;

packages/local/src/intrinsics.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type IntrinsicArg =
8383
| { type: 'string'; value: string }
8484
| { type: 'number'; value: number }
8585
| { type: 'boolean'; value: boolean }
86+
| { type: 'null'; value: null }
8687
| { type: 'call'; value: IntrinsicCall };
8788

8889
function parseIntrinsicCall(expr: string): IntrinsicCall {
@@ -185,11 +186,14 @@ function parseArgList(str: string, start: number): { args: IntrinsicArg[]; end:
185186
} else if (ch === 'f' && str.startsWith('false', i)) {
186187
args.push({ type: 'boolean', value: false });
187188
i += 5;
189+
} else if (ch === 'n' && str.startsWith('null', i)) {
190+
args.push({ type: 'null', value: null });
191+
i += 4;
188192
} else if (ch === '-' || ch === '+' || (ch >= '0' && ch <= '9')) {
189193
// Number literal
190194
let j = i;
191195
if (ch === '-' || ch === '+') j++;
192-
while (j < str.length && ((str[j] >= '0' && str[j] <= '9') || str[j] === '.')) j++;
196+
while (j < str.length && ((str[j] >= '0' && str[j] <= '9') || str[j] === '.' || str[j] === 'e' || str[j] === 'E' || ((str[j] === '+' || str[j] === '-') && (str[j-1] === 'e' || str[j-1] === 'E')))) j++;
193197
args.push({ type: 'number', value: parseFloat(str.slice(i, j)) });
194198
i = j;
195199
} else if (ch === ',') {
@@ -217,6 +221,7 @@ function resolveArg(arg: IntrinsicArg, stateData: any, context: ContextObject):
217221
case 'string': return arg.value;
218222
case 'number': return arg.value;
219223
case 'boolean': return arg.value;
224+
case 'null': return null;
220225
case 'path': return resolveReference(arg.value, stateData, context);
221226
case 'call': return evaluateCall(arg.value, stateData, context);
222227
}
@@ -236,11 +241,12 @@ function evaluateCall(call: IntrinsicCall, stateData: any, context: ContextObjec
236241
case 'States.ArrayGetItem': return args[0][args[1]];
237242
case 'States.ArrayLength': return args[0].length;
238243
case 'States.ArrayUnique': return statesArrayUnique(args[0]);
244+
case 'States.ArraySlice': return (args[0] as any[]).slice(args[1] as number, args[2] as number);
239245
case 'States.Base64Encode': return base64Encode(args[0]);
240246
case 'States.Base64Decode': return base64Decode(args[0]);
241247
case 'States.Hash': return computeHash(args[1], args[0]);
242248
case 'States.JsonMerge': return statesJsonMerge(args[0], args[1], args[2]);
243-
case 'States.MathRandom': return Math.floor(Math.random() * (args[1] - args[0])) + args[0];
249+
case 'States.MathRandom': return Math.floor(Math.random() * (args[1] - args[0] + 1)) + args[0];
244250
case 'States.MathAdd': return args[0] + args[1];
245251
case 'States.StringSplit': return args[1] === '' ? [...args[0]] : args[0].split(args[1]);
246252
case 'States.UUID': return getRandomUUID();
@@ -256,10 +262,19 @@ function evaluateCall(call: IntrinsicCall, stateData: any, context: ContextObjec
256262
function statesFormat(args: any[]): string {
257263
const template = args[0] as string;
258264
let argIndex = 1;
259-
return template.replace(/\{}/g, () => {
260-
const val = args[argIndex++];
261-
return val === undefined ? '' : String(val);
262-
});
265+
let result = '';
266+
for (let i = 0; i < template.length; i++) {
267+
if (template[i] === '\\' && i + 1 < template.length && (template[i + 1] === '{' || template[i + 1] === '}')) {
268+
result += template[++i];
269+
} else if (template[i] === '{' && i + 1 < template.length && template[i + 1] === '}') {
270+
const val = args[argIndex++];
271+
result += val === undefined ? '' : String(val);
272+
i++; // skip }
273+
} else {
274+
result += template[i];
275+
}
276+
}
277+
return result;
263278
}
264279

265280
function statesJsonMerge(a: any, b: any, deep?: boolean): any {
@@ -298,6 +313,7 @@ function statesArrayContains(arr: any[], value: any): boolean {
298313
}
299314

300315
function statesArrayRange(start: number, end: number, step: number): number[] {
316+
if (step === 0) throw new Error('States.ArrayRange: step must not be zero');
301317
const result: number[] = [];
302318
if (step > 0) {
303319
for (let i = start; i <= end; i += step) result.push(i);

0 commit comments

Comments
 (0)