Skip to content

Commit

Permalink
feat(step-functions): task utility (#518)
Browse files Browse the repository at this point in the history
  • Loading branch information
thantos committed Sep 21, 2022
1 parent b76bca0 commit d117cde
Show file tree
Hide file tree
Showing 16 changed files with 1,496 additions and 341 deletions.
10 changes: 9 additions & 1 deletion .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions .projenrc.ts
Expand Up @@ -12,7 +12,7 @@ import { Job, JobPermission } from "projen/lib/github/workflows-model";
class GitHooksPreCommitComponent extends TextFile {
constructor(project: Project) {
super(project, ".git/hooks/pre-commit", {
lines: ["#!/bin/sh", "npx -y lint-staged"],
lines: ["#!/bin/sh", "yarn run typecheck", "npx -y lint-staged"],
});
}

Expand Down Expand Up @@ -266,7 +266,9 @@ project.testTask.reset();
project.testTask.env("TEST_DEPLOY_TARGET", "AWS");
project.testTask.env("NODE_OPTIONS", "--max-old-space-size=6144");

project.testTask.exec("tsc -p ./tsconfig.dev.json --noEmit");
const typeCheck = project.addTask("typecheck", {
exec: "tsc -p ./tsconfig.dev.json --noEmit",
});

const testFast = project.addTask("test:fast", {
exec: "jest --passWithNoTests --all --updateSnapshot --testPathIgnorePatterns '(localstack|runtime)'",
Expand All @@ -280,6 +282,7 @@ const testApp = project.addTask("test:app", {
exec: "cd ./test-app && yarn && yarn build && yarn synth --quiet",
});

project.testTask.spawn(typeCheck);
project.testTask.spawn(testFast);
project.testTask.spawn(testRuntime);
project.testTask.spawn(testApp);
Expand Down
1 change: 1 addition & 0 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 117 additions & 4 deletions src/asl/asl-graph.ts
Expand Up @@ -1798,7 +1798,10 @@ export namespace ASLGraph {
value:
| ASLGraph.IntrinsicFunction
| ASLGraph.JsonPath
| ASLGraph.LiteralValue
| ASLGraph.LiteralValue<number>
| ASLGraph.LiteralValue<string>
| ASLGraph.LiteralValue<boolean>
| ASLGraph.LiteralValue<null>
) {
return intrinsicFunction("States.JsonToString", value);
}
Expand Down Expand Up @@ -2028,6 +2031,12 @@ export namespace ASLGraph {
);
}

export function escapeFormatLiteralString(
literal: ASLGraph.LiteralValue<string>
) {
return literal.value.replace(/[\}\{\'}]/g, "\\$&");
}

/**
* Escape special characters in Step Functions intrinsics.
*
Expand All @@ -2037,13 +2046,117 @@ export namespace ASLGraph {
*
* https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html#amazon-states-language-intrinsic-functions-escapes
*/
export function escapeFormatString(literal: ASLGraph.LiteralValue) {
if (typeof literal.value === "string") {
return literal.value.replace(/[\}\{\'}]/g, "\\$&");
export function escapeFormatLiteral(literal: ASLGraph.LiteralValue) {
if (ASLGraph.isLiteralString(literal)) {
return escapeFormatLiteralString(literal);
} else {
return literal.value;
}
}

/**
* Turns any literal value in a string or intrinsic functions that can generate a string representation.
*
* ```ts
* literalValue("value") // => "value"
* literalValue(1) // => "1"
* literalValue({ a: 1 }) // => "{ \"a\": "1" }""
* literalValue({ "a.$": "$.var" }) => intrinsicFormat('{ "a": {} }', JsonToString("$.var"))
* literalValue([1]) // => "[1]"
* literalValue(["$.var"]) => intrinsicFormat('[{}]', JsonToString("$.var"))
* ```
*/
export function stringifyLiteral(
value: ASLGraph.LiteralValue
): ASLGraph.IntrinsicFunction | ASLGraph.LiteralValue<string> {
if (ASLGraph.isLiteralNull(value)) {
return ASLGraph.literalValue("null");
} else if (ASLGraph.isLiteralArray(value)) {
return stringifyLiteralArray(value);
} else if (ASLGraph.isLiteralObject(value)) {
return stringifyLiteralObject(value);
}
return ASLGraph.isLiteralString(value)
? value
: ASLGraph.literalValue(String(value.value));

/**
* Handles stringifying a literal object using javascript during synth or intrinsic functions during runtime.
*
* literalValue({ a: 1 }) // => "{ \"a\": "1" }""
* literalValue({ "a.$": "$.var" }) => intrinsicFormat('{ "a": {} }', JsonToString("$.var"))
*/
function stringifyLiteralObject(
value: ASLGraph.LiteralValue<Record<string, ASLGraph.LiteralValueType>>
): ASLGraph.IntrinsicFunction | ASLGraph.LiteralValue<string> {
if (!value.containsJsonPath) {
return ASLGraph.literalValue(JSON.stringify(value.value), false);
}
const entries = Object.entries(value.value).map(([key, value]) => {
if (key.endsWith(".$")) {
// assuming that these values json path and not intrinsic
return [
key.substring(0, key.length - 2),
ASLGraph.intrinsicJsonToString(ASLGraph.jsonPath(value as string)),
] as const;
}
return [key, stringifyLiteral(ASLGraph.literalValue(value))] as const;
});

// stringify only returns literal values for nested values when
// all nested values can be stringified
if (entries.every(([_, value]) => ASLGraph.isLiteralValue(value))) {
// just stringify the original value.
return ASLGraph.literalValue(JSON.stringify(value.value));
}
return ASLGraph.intrinsicFormat(
ASLGraph.literalValue(
`\\{${entries
.map(
([key, value]) =>
`"${key}": ${
ASLGraph.isIntrinsicFunction(value)
? "{}"
: `'${ASLGraph.escapeFormatLiteralString(value)}'`
}`
)
.join(",")}\\}`
),
...entries
.map(([_, value]) => value)
.filter(ASLGraph.isIntrinsicFunction)
);
}

/**
* Handles stringifying a literal object using javascript during synth or intrinsic functions during runtime.
*
* literalValue([1]) // => "[1]"
* literalValue(["$.var"]) => intrinsicFormat('[{}]', JsonToString("$.var"))
*/
function stringifyLiteralArray(
value: ASLGraph.LiteralValue<ASLGraph.LiteralValueType[]>
): ASLGraph.IntrinsicFunction | ASLGraph.LiteralValue<string> {
const stringifyElements = value.value
.map((v) => ASLGraph.literalValue(v))
.map(stringifyLiteral);
if (stringifyElements.every(ASLGraph.isLiteralString)) {
return ASLGraph.literalValue(JSON.stringify(value.value));
}
// ["a", 1, var1, var2]
// States.Format('["a", 1, {}{}]', $.var1, $.var2)
return ASLGraph.intrinsicFormat(
stringifyElements
.map((s) =>
ASLGraph.isLiteralValue(s)
? `'${escapeFormatLiteralString(s)}'`
: "{}"
)
.join(","),
...stringifyElements.filter(ASLGraph.isIntrinsicFunction)
);
}
}
}

// to prevent the closure serializer from trying to import all of functionless.
Expand Down
4 changes: 4 additions & 0 deletions src/asl/states.ts
Expand Up @@ -139,6 +139,10 @@ export type CommonTaskFields = CommonFields & {
export type Task = CommonTaskFields & {
Type: "Task";
Resource: string;
TimeoutSeconds?: number;
TimeoutSecondsPath?: string;
HeartbeatSeconds?: number;
HeartbeatSecondsPath?: string;
};

/**
Expand Down
14 changes: 12 additions & 2 deletions src/asl/synth.ts
Expand Up @@ -345,6 +345,8 @@ export class ASL {
*/
private static readonly CatchState: string = "__catch";

private readonly contextParameter: undefined | ParameterDecl;

constructor(
readonly scope: Construct,
readonly role: aws_iam.IRole,
Expand Down Expand Up @@ -395,6 +397,8 @@ export class ASL {

const [inputParam, contextParam] = this.decl.parameters;

this.contextParameter = contextParam;

// get the State Parameters and ASLGraph states to initialize any provided parameters (assignment and binding).
const [paramInitializer, paramStates] =
this.evalParameterDeclForStateParameter(
Expand Down Expand Up @@ -1865,7 +1869,7 @@ export class ASL {
.map((output) =>
ASLGraph.isJsonPath(output)
? "{}"
: ASLGraph.escapeFormatString(output)
: ASLGraph.escapeFormatLiteral(output)
)
.join(""),
...jsonPaths.map(([, jp]) => jp)
Expand Down Expand Up @@ -2265,7 +2269,11 @@ export class ASL {
prop: PropAssignExpr | SpreadAssignExpr
): ASLGraph.LiteralValue | ASLGraph.JsonPath {
const output = evalExprToJsonPathOrLiteral(prop.expr);
return ASLGraph.isJsonPath(output) ? assignValue(output) : output;
return ASLGraph.isJsonPath(output) &&
// paths at $$ are immutable, it is not necessary to reference their value because it will not change.
!output.jsonPath.startsWith("$$.")
? assignValue(output)
: output;
}
}
);
Expand Down Expand Up @@ -2899,6 +2907,8 @@ export class ASL {
[`${FUNCTIONLESS_CONTEXT_NAME}.$`]: FUNCTIONLESS_CONTEXT_JSON_PATH,
...Object.fromEntries(
Array.from(variableReferences.values())
// the context parameter is resolved by using `$$.*` anywhere in the machine, it never needs to be passed in.
.filter((decl) => decl !== this.contextParameter)
.map((decl) =>
// assume there is an identifier name if it is in the lexical scope
this.getDeclarationName(decl as BindingDecl & { name: Identifier })
Expand Down
12 changes: 6 additions & 6 deletions src/node.ts
Expand Up @@ -58,6 +58,7 @@ import type { NodeCtor } from "./node-ctor";
import { NodeKind, NodeKindName, getNodeKindName } from "./node-kind";
import type { Span } from "./span";
import type { BlockStmt, CatchClause, Stmt } from "./statement";
import { anyOf } from "./util";

export type FunctionlessNode =
| Decl
Expand Down Expand Up @@ -88,6 +89,8 @@ export type BindingDecl =
| FunctionDecl
| FunctionExpr;

const functionClassOrMethod = anyOf(isFunctionLike, isClassLike, isMethodDecl);

export abstract class BaseNode<
Kind extends NodeKind,
Parent extends FunctionlessNode | undefined = FunctionlessNode
Expand Down Expand Up @@ -160,10 +163,7 @@ export abstract class BaseNode<
* @returns the name of the file this node originates from.
*/
public getFileName(): string {
if (
(isFunctionLike(this) || isClassLike(this) || isMethodDecl(this)) &&
this.filename
) {
if (functionClassOrMethod(this) && this.filename) {
return this.filename;
} else if (this.parent === undefined) {
throw new Error(`cannot get filename of orphaned node`);
Expand Down Expand Up @@ -242,8 +242,8 @@ export abstract class BaseNode<
* Finds the {@link CatchClause} that this Node should throw to.
*/
public findCatchClause(): CatchClause | undefined {
if (isBlockStmt(this) && (this as unknown as BlockStmt).isFinallyBlock()) {
return this.parent.findCatchClause();
if (isBlockStmt(this) && (<BlockStmt>this).isFinallyBlock()) {
return (<BlockStmt>this).parent.findCatchClause();
}
const scope = this.parent;
if (scope === undefined) {
Expand Down

0 comments on commit d117cde

Please sign in to comment.