Skip to content

Commit

Permalink
fix(sfn): can't override toStateJson() from other languages (aws#24593)
Browse files Browse the repository at this point in the history
If any part of a state's JSON representation is `null`, that value will be replaced by `undefined` when jsii sends data to the other language, resulting in a change of semantics.

Multi-language APIs cannot differentiate between `null` and `undefined` as non-JS languages typically fail to distinguish between them... In order to address that, a `JsonNull` value was added which serializes to `null` (via Javascript's standard `toJSON` method), which must be used in such cases where `null` may need to cross the language boundary.

The `JsonPath.DISCARD` value is now a string-token representation of the `JsonNull` instance.

Fixes aws#14639
Fixes aws/jsii#3999

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
RomainMuller authored and homakk committed Mar 28, 2023
1 parent 22773d3 commit 9a5a91c
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 4 deletions.
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-stepfunctions/lib/fields.ts
@@ -1,4 +1,4 @@
import { Token, IResolvable } from '@aws-cdk/core';
import { Token, IResolvable, JsonNull } from '@aws-cdk/core';
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, renderInExpression, jsonPathFromAny } from './private/json-path';

/**
Expand All @@ -9,9 +9,9 @@ import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, rende
*/
export class JsonPath {
/**
* Special string value to discard state input, output or result
* Special string value to discard state input, output or result.
*/
public static readonly DISCARD = 'DISCARD';
public static readonly DISCARD = Token.asString(JsonNull.INSTANCE, { displayHint: 'DISCARD (JSON `null`)' });

/**
* Instead of using a literal string, get the value from a JSON path
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts
@@ -1,3 +1,4 @@
import { Token } from '@aws-cdk/core';
import { IConstruct, Construct, Node } from 'constructs';
import { Condition } from '../condition';
import { FieldUtils, JsonPath } from '../fields';
Expand Down Expand Up @@ -579,7 +580,7 @@ export function renderJsonPath(jsonPath?: string): undefined | null | string {
if (jsonPath === undefined) { return undefined; }
if (jsonPath === JsonPath.DISCARD) { return null; }

if (!jsonPath.startsWith('$')) {
if (!Token.isUnresolved(jsonPath) && !jsonPath.startsWith('$')) {
throw new Error(`Expected JSON path to start with '$', got: ${jsonPath}`);
}
return jsonPath;
Expand Down
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions/test/state.test.ts
@@ -0,0 +1,30 @@
import * as cdk from '@aws-cdk/core';
import { FakeTask } from './integ.state-machine-credentials';
import { renderGraph } from './private/render-util';
import { JsonPath } from '../lib';

test('JsonPath.DISCARD can be used to discard a state\'s output', () => {
const stack = new cdk.Stack();

const task = new FakeTask(stack, 'my-state', {
inputPath: JsonPath.DISCARD,
outputPath: JsonPath.DISCARD,
resultPath: JsonPath.DISCARD,
});

expect(renderGraph(task)).toEqual({
StartAt: 'my-state',
States: {
'my-state': {
End: true,
Type: 'Task',
Resource: expect.any(String),
Parameters: expect.any(Object),
// The important bits:
InputPath: null,
OutputPath: null,
ResultPath: null,
},
},
});
});
21 changes: 21 additions & 0 deletions packages/@aws-cdk/core/lib/token.ts
Expand Up @@ -232,6 +232,27 @@ export class Tokenization {
}
}

/**
* An object which serializes to the JSON `null` literal, and which can safely
* be passed across languages where `undefined` and `null` are not different.
*/
export class JsonNull {
/** The canonical instance of `JsonNull`. */
public static readonly INSTANCE = new JsonNull();

private constructor() { }

/** Obtains the JSON representation of this object (`null`) */
public toJSON(): any {
return null;
}

/** Obtains the string representation of this object (`'null'`) */
public toString(): string {
return 'null';
}
}

/**
* Options for the 'reverse()' operation
*/
Expand Down

0 comments on commit 9a5a91c

Please sign in to comment.