From 0f54698ac98f9d1412d6a7f2a6b2859042d2a8f2 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 2 Jun 2019 08:28:20 +0300 Subject: [PATCH] feat(tokens): enable type coercion (#2680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/@aws-cdk/cdk/lib/token.ts | 26 ++++--- packages/@aws-cdk/cdk/test/test.tokens.ts | 88 ++++++++++++++++++++++- 2 files changed, 98 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..845d9f389806a 100644 --- a/packages/@aws-cdk/cdk/test/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/test.tokens.ts @@ -467,7 +467,6 @@ export = { 'can number-encode and resolve Token objects'(test: Test) { // GIVEN - const stack = new Stack(); const x = new Token(() => 123); // THEN @@ -475,7 +474,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 +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}.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(); + } } };