Skip to content

Commit

Permalink
feat(tokens): enable type coercion (#2680)
Browse files Browse the repository at this point in the history
Relax restrictions on input types for Token.toXxx in order to allow flexible type coercion.

This may be needed in situations where users want to force a token typed as one type to be represented as another type and generally allow tokens to be used as "type-system escape hatches".

Previously, this did not work:

    const port = new Token({ "Fn::GetAtt": [ "ResourceId", "Port" ] }).toString(); 
    new TcpPort(new Token(port).toNumber());

Also, this did not work:

    const port = new Token({ "Fn::GetAtt": [ "ResourceId", "Port" ]}).toNumber();

These were just examples. The point is that if a user reached the point where you actually need a token, they basically indicate to the framework that “I know what I am are doing”. It’s a sort of an “as any” at the framework level.

Fixes #2679
  • Loading branch information
Elad Ben-Israel authored Jun 2, 2019
1 parent dcfd9d2 commit 0f54698
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 16 deletions.
26 changes: 12 additions & 14 deletions packages/@aws-cdk/cdk/lib/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,16 @@ export class Token {
* on the string.
*/
public toString(): string {
const valueType = typeof this.valueOrFunction;
// Optimization: if we can immediately resolve this, don't bother
// registering a Token.
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
return this.valueOrFunction.toString();
// registering a Token (unless it's already a token).
if (typeof(this.valueOrFunction) === 'string') {
return this.valueOrFunction;
}

if (this.tokenStringification === undefined) {
this.tokenStringification = TokenMap.instance().registerString(this, this.displayName);
}

return this.tokenStringification;
}

Expand Down Expand Up @@ -139,9 +139,8 @@ export class Token {
* is constructing a `FnJoin` or a `FnSelect` on it.
*/
public toList(): string[] {
const valueType = typeof this.valueOrFunction;
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
throw this.newError('Got a literal Token value; only intrinsics can ever evaluate to lists.');
if (Array.isArray(this.valueOrFunction)) {
return this.valueOrFunction;
}

if (this.tokenListification === undefined) {
Expand All @@ -160,14 +159,13 @@ export class Token {
* other operations can and probably will destroy the token-ness of the value.
*/
public toNumber(): number {
// Optimization: if we can immediately resolve this, don't bother
// registering a Token.
if (typeof(this.valueOrFunction) === 'number') {
return this.valueOrFunction;
}

if (this.tokenNumberification === undefined) {
const valueType = typeof this.valueOrFunction;
// Optimization: if we can immediately resolve this, don't bother
// registering a Token.
if (valueType === 'number') { return this.valueOrFunction; }
if (valueType !== 'function') {
throw this.newError(`Token value is not number or lazy, can't represent as number: ${this.valueOrFunction}`);
}
this.tokenNumberification = TokenMap.instance().registerNumber(this);
}

Expand Down
88 changes: 86 additions & 2 deletions packages/@aws-cdk/cdk/test/test.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,14 @@ export = {

'can number-encode and resolve Token objects'(test: Test) {
// GIVEN
const stack = new Stack();
const x = new Token(() => 123);

// THEN
const encoded = x.toNumber();
test.equal(true, Token.isToken(encoded), 'encoded number does not test as token');

// THEN
const resolved = stack.node.resolve({ value: encoded });
const resolved = resolve({ value: encoded });
test.deepEqual(resolved, { value: 123 });

test.done();
Expand Down Expand Up @@ -522,6 +521,91 @@ export = {
const token = fn1();
test.throws(() => token.throwError('message!'), /Token created:/);
test.done();
},

'type coercion': (() => {
const tests: any = { };

const inputs = [
() => 'lazy',
'a string',
1234,
{ an_object: 1234 },
[ 1, 2, 3 ],
false
];

for (const input of inputs) {
// GIVEN
const stringToken = new Token(input).toString();
const numberToken = new Token(input).toNumber();
const listToken = new Token(input).toList();

// THEN
const expected = typeof(input) === 'function' ? input() : input;

tests[`${input}<string>.toNumber()`] = (test: Test) => {
test.deepEqual(resolve(new Token(stringToken).toNumber()), expected);
test.done();
};

tests[`${input}<list>.toNumber()`] = (test: Test) => {
test.deepEqual(resolve(new Token(listToken).toNumber()), expected);
test.done();
};

tests[`${input}<number>.toNumber()`] = (test: Test) => {
test.deepEqual(resolve(new Token(numberToken).toNumber()), expected);
test.done();
};

tests[`${input}<string>.toString()`] = (test: Test) => {
test.deepEqual(resolve(new Token(stringToken).toString()), expected);
test.done();
};

tests[`${input}<list>.toString()`] = (test: Test) => {
test.deepEqual(resolve(new Token(listToken).toString()), expected);
test.done();
};

tests[`${input}<number>.toString()`] = (test: Test) => {
test.deepEqual(resolve(new Token(numberToken).toString()), expected);
test.done();
};

tests[`${input}<string>.toList()`] = (test: Test) => {
test.deepEqual(resolve(new Token(stringToken).toList()), expected);
test.done();
};

tests[`${input}<list>.toList()`] = (test: Test) => {
test.deepEqual(resolve(new Token(listToken).toList()), expected);
test.done();
};

tests[`${input}<number>.toList()`] = (test: Test) => {
test.deepEqual(resolve(new Token(numberToken).toList()), expected);
test.done();
};
}

return tests;
})(),

'toXxx short circuts if the input is of the same type': {
'toNumber(number)'(test: Test) {
test.deepEqual(new Token(123).toNumber(), 123);
test.done();
},
'toList(list)'(test: Test) {
test.deepEqual(new Token([1, 2, 3]).toList(), [1, 2, 3]);
test.done();
},
'toString(string)'(test: Test) {
test.deepEqual(new Token('string').toString(), 'string'),
test.done();
}
}
};

Expand Down

0 comments on commit 0f54698

Please sign in to comment.