From 5b0730f742df2b15cc287c8c55b80e0e09b1f002 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 30 May 2019 10:58:00 +0300 Subject: [PATCH] feat(tokens): enable type coercion 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(); Fixes #2679 --- packages/@aws-cdk/cdk/lib/token.ts | 26 +++---- packages/@aws-cdk/cdk/test/test.tokens.ts | 89 ++++++++++++++++++++++- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index d7d64df131c61..bc4f3dca4e971 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -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; } @@ -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) { @@ -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); } diff --git a/packages/@aws-cdk/cdk/test/test.tokens.ts b/packages/@aws-cdk/cdk/test/test.tokens.ts index 1ea25183712e6..40cf95d468bf0 100644 --- a/packages/@aws-cdk/cdk/test/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/test.tokens.ts @@ -3,6 +3,7 @@ import { App as Root, findTokens, Fn, Stack, Token } from '../lib'; import { createTokenDouble, extractTokenDouble } from '../lib/encoding'; import { TokenMap } from '../lib/token-map'; import { evaluateCFN } from './evaluate-cfn'; +import { func } from 'fast-check/*'; export = { 'resolve a plain old object should just return the object'(test: Test) { @@ -467,7 +468,6 @@ export = { 'can number-encode and resolve Token objects'(test: Test) { // GIVEN - const stack = new Stack(); const x = new Token(() => 123); // THEN @@ -475,7 +475,7 @@ export = { 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(); @@ -522,6 +522,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}.toNumber()`] = (test: Test) => { + test.deepEqual(resolve(new Token(stringToken).toNumber()), expected); + test.done(); + }; + + tests[`${input}.toNumber()`] = (test: Test) => { + test.deepEqual(resolve(new Token(listToken).toNumber()), expected); + test.done(); + }; + + tests[`${input}.toNumber()`] = (test: Test) => { + test.deepEqual(resolve(new Token(numberToken).toNumber()), expected); + test.done(); + }; + + tests[`${input}.toString()`] = (test: Test) => { + test.deepEqual(resolve(new Token(stringToken).toString()), expected); + test.done(); + }; + + tests[`${input}.toString()`] = (test: Test) => { + test.deepEqual(resolve(new Token(listToken).toString()), expected); + test.done(); + }; + + tests[`${input}.toString()`] = (test: Test) => { + test.deepEqual(resolve(new Token(numberToken).toString()), expected); + test.done(); + }; + + tests[`${input}.toList()`] = (test: Test) => { + test.deepEqual(resolve(new Token(stringToken).toList()), expected); + test.done(); + }; + + tests[`${input}.toList()`] = (test: Test) => { + test.deepEqual(resolve(new Token(listToken).toList()), expected); + test.done(); + }; + + tests[`${input}.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(); + } } };