-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Framework: Tokens can be converted to strings #518
Changes from 16 commits
3796389
9469a71
e47741b
f6d01a8
6e79bb8
a72d14d
42becf3
1b60e45
d1fc358
58602a9
74f4fe7
b15684d
5753e31
4206b85
85cfa81
a3ec610
7361815
a7b41e2
6aa9b77
c5b3b68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { resolve, Token } from "../core/tokens"; | ||
import { CloudFormationToken, isIntrinsic } from "./cloudformation-token"; | ||
|
||
/** | ||
* Class for JSON routines that are framework-aware | ||
*/ | ||
export class CloudFormationJSON { | ||
/** | ||
* Turn an arbitrary structure potentially containing Tokens into JSON. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return type should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually returns a |
||
*/ | ||
public static stringify(obj: any): any { | ||
return new Token(() => { | ||
// Resolve inner value so that if they evaluate to literals, we maintain the type. | ||
// | ||
// Then replace intrinsics with a special sublcass of Token that overrides toJSON() to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "sublcass" => "subclass", "to and" => "and" (i think) |
||
// and deep-escapes the intrinsic, so if we resolve() the strings again it evaluates | ||
// to the right string. | ||
const resolved = resolve(obj); | ||
|
||
// We can just directly return this value, since resolve() will be called | ||
// on our return value anyway. | ||
return JSON.stringify(deepReplaceIntrinsics(resolved)); | ||
}); | ||
|
||
/** | ||
* Recurse into a structure, replace all intrinsics with | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "with..."? |
||
*/ | ||
function deepReplaceIntrinsics(x: any): any { | ||
if (isIntrinsic(x)) { | ||
return wrapIntrinsic(x); | ||
} | ||
|
||
if (Array.isArray(x)) { | ||
return x.map(deepReplaceIntrinsics); | ||
} | ||
|
||
if (typeof x === 'object') { | ||
for (const key of Object.keys(x)) { | ||
x[key] = deepReplaceIntrinsics(x[key]); | ||
} | ||
} | ||
|
||
return x; | ||
} | ||
|
||
function wrapIntrinsic(intrinsic: any): IntrinsicToken { | ||
return new IntrinsicToken(() => deepQuoteStringsForJSON(intrinsic)); | ||
} | ||
} | ||
} | ||
|
||
class IntrinsicToken extends CloudFormationToken { | ||
/** | ||
* Special handler that gets called when JSON.stringify() is used. | ||
*/ | ||
public toJSON() { | ||
return this.toString(); | ||
} | ||
} | ||
|
||
/** | ||
* Deep escape strings for use in a JSON context | ||
*/ | ||
function deepQuoteStringsForJSON(x: any): any { | ||
if (typeof x === 'string') { | ||
// Whenever we escape a string we strip off the outermost quotes | ||
// since we're already in a quoted context. | ||
const stringified = JSON.stringify(x); | ||
return stringified.substring(1, stringified.length - 1); | ||
} | ||
|
||
if (Array.isArray(x)) { | ||
return x.map(deepQuoteStringsForJSON); | ||
} | ||
|
||
if (typeof x === 'object') { | ||
for (const key of Object.keys(x)) { | ||
x[key] = deepQuoteStringsForJSON(x[key]); | ||
} | ||
} | ||
|
||
return x; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Token } from "../core/tokens"; | ||
|
||
/** | ||
* Base class for CloudFormation built-ins | ||
*/ | ||
export class CloudFormationToken extends Token { | ||
constructor(valueOrFunction: any, stringRepresentationHint?: string) { | ||
super(valueOrFunction, { | ||
joiner: CLOUDFORMATION_JOINER, | ||
stringRepresentationHint | ||
}); | ||
} | ||
} | ||
|
||
import { FnConcat } from "./fn"; | ||
|
||
/** | ||
* The default intrinsics Token engine for CloudFormation | ||
*/ | ||
export const CLOUDFORMATION_JOINER = { | ||
joinerName: 'CloudFormation', | ||
|
||
joinStringFragments(fragments: any[]): any { | ||
return new FnConcat(...fragments.map(x => isIntrinsic(x) ? x : `${x}`)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That fails in the case of |
||
} | ||
}; | ||
|
||
/** | ||
* Return whether the given value represents a CloudFormation intrinsic | ||
*/ | ||
export function isIntrinsic(x: any) { | ||
if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But |
||
|
||
const keys = Object.keys(x); | ||
if (keys.length !== 1) { return false; } | ||
|
||
return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import { Token } from '../core/tokens'; | ||
import { CloudFormationToken } from './cloudformation-token'; | ||
|
||
export class PseudoParameter extends Token { | ||
export class PseudoParameter extends CloudFormationToken { | ||
constructor(name: string) { | ||
super(() => ({ Ref: name })); | ||
super(() => ({ Ref: name }), name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this need to be lazy? We can plug in a concrete value |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import { Construct } from '../core/construct'; | ||
import { Token } from '../core/tokens'; | ||
import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; | ||
import { CloudFormationToken } from './cloudformation-token'; | ||
import { Condition } from './condition'; | ||
import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; | ||
import { IDependable, Referenceable, StackElement } from './stack'; | ||
|
@@ -82,7 +83,7 @@ export class Resource extends Referenceable { | |
* @param attributeName The name of the attribute. | ||
*/ | ||
public getAtt(attributeName: string): Token { | ||
return new Token(() => ({ 'Fn::GetAtt': [this.logicalId, attributeName] })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn’t need to be lazy |
||
return new CloudFormationToken(() => ({ 'Fn::GetAtt': [this.logicalId, attributeName] }), `${this.logicalId}.${attributeName}`); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import { App } from '../app'; | |
import { Construct, PATH_SEP } from '../core/construct'; | ||
import { resolve, Token } from '../core/tokens'; | ||
import { Environment } from '../environment'; | ||
import { CloudFormationToken } from './cloudformation-token'; | ||
import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; | ||
import { Resource } from './resource'; | ||
|
||
|
@@ -391,8 +392,8 @@ export abstract class Referenceable extends StackElement { | |
/** | ||
* Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID. | ||
*/ | ||
public get ref() { | ||
return new Token(() => ({ Ref: this.logicalId })); | ||
public get ref(): Token { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should just define a ‘Ref’ class for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for that. It doesn't add a lot of value and no user is ever going to instantiate it. |
||
return new CloudFormationToken(() => ({ Ref: this.logicalId }), `${this.logicalId}.Ref`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn’t need to be lazy |
||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wraps a token and
CloudFormationJSON.stringify
also returns aToken
. Can we eliminate one layer?