packages/@alicloud/ros-cdk-core/lib/private/template-lang.ts (194 lines of code) (raw):
import { Lazy } from "../lazy";
import { Reference } from "../reference";
import {
DefaultTokenResolver,
IFragmentConcatenator,
IPostProcessor,
IResolvable,
IResolveContext,
} from "../resolvable";
import { TokenizedStringFragments } from "../string-fragments";
import { Token } from "../token";
import { Intrinsic } from "./intrinsic";
import { resolve } from "./resolve";
/**
* Routines that know how to do operations at the ROS document language level
*/
export class RosTemplateLang {
/**
* Turn an arbitrary structure potentially containing Tokens into a JSON string.
*
* Returns a Token which will evaluate to ROS expression that
* will be evaluated by ROS to the JSON representation of the
* input structure.
*
* All Tokens substituted in this way must return strings, or the evaluation
* in ROS will fail.
*
* @param obj The object to stringify
* @param space Indentation to use (default: no pretty-printing)
*/
public static toJSON(obj: any, space?: number): string {
// This works in two stages:
//
// First, resolve everything. This gets rid of the lazy evaluations, evaluation
// to the real types of things (for example, would a function return a string, an
// intrinsic, or a number? We have to resolve to know).
//
// We then to through the returned result, identify things that evaluated to
// ROS intrinsics, and re-wrap those in Tokens that have a
// toJSON() method returning their string representation. If we then call
// JSON.stringify() on that result, that gives us essentially the same
// string that we started with, except with the non-token characters quoted.
//
// {"field": "${TOKEN}"} --> {\"field\": \"${TOKEN}\"}
//
// A final resolve() on that string (done by the framework) will yield the string
// we're after.
//
// Resolving and wrapping are done in go using the resolver framework.
class IntrinsincWrapper extends DefaultTokenResolver {
constructor() {
super(ROS_CONCAT);
}
public resolveToken(
t: IResolvable,
context: IResolveContext,
postProcess: IPostProcessor
) {
// Return References directly, so their type is maintained and the references will
// continue to work. Only while preparing, because we do need the final value of the
// token while resolving.
if (Reference.isReference(t) && context.preparing) {
return wrap(t);
}
// Deep-resolve and wrap. This is necessary for Lazy tokens so we can see "inside" them.
return wrap(super.resolveToken(t, context, postProcess));
}
public resolveString(
fragments: TokenizedStringFragments,
context: IResolveContext
) {
return wrap(super.resolveString(fragments, context));
}
public resolveList(l: string[], context: IResolveContext) {
return wrap(super.resolveList(l, context));
}
}
// We need a ResolveContext to get started so return a Token
return Lazy.stringValue({
produce: (ctx: IResolveContext) =>
JSON.stringify(
resolve(obj, {
preparing: ctx.preparing,
scope: ctx.scope,
resolver: new IntrinsincWrapper(),
}),
undefined,
space
),
});
function wrap(value: any): any {
return isIntrinsic(value)
? new JsonToken(deepQuoteStringsForJSON(value))
: value;
}
}
/**
* Produce a ROS expression to concat two arbitrary expressions when resolving
*/
public static concat(left: any | undefined, right: any | undefined): any {
if (left === undefined && right === undefined) {
return "";
}
const parts = new Array<any>();
if (left !== undefined) {
parts.push(left);
}
if (right !== undefined) {
parts.push(right);
}
// Some case analysis to produce minimal expressions
if (parts.length === 1) {
return parts[0];
}
if (
parts.length === 2 &&
typeof parts[0] === "string" &&
typeof parts[1] === "string"
) {
return parts[0] + parts[1];
}
// Otherwise return a Join intrinsic (already in the target document language to avoid taking
// circular dependencies on FnJoin & friends)
return { "Fn::Join": ["", minimalRosTemplateJoin("", parts)] };
}
}
/**
* Token that also stringifies in the toJSON() operation.
*/
class JsonToken extends Intrinsic {
/**
* 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;
}
const ROS_CONCAT: IFragmentConcatenator = {
join(left: any, right: any) {
return RosTemplateLang.concat(left, right);
},
};
/**
* Default Token resolver for ROS templates
*/
export const ROS_TOKEN_RESOLVER = new DefaultTokenResolver(ROS_CONCAT);
/**
* Do an intelligent ROS join on the given values, producing a minimal expression
*/
export function minimalRosTemplateJoin(
delimiter: string,
values: any[]
): any[] {
let i = 0;
while (i < values.length) {
const el = values[i];
if (isSplicableFnJoinIntrinsic(el)) {
values.splice(i, 1, ...el["Fn::Join"][1]);
} else if (
i > 0 &&
isPlainString(values[i - 1]) &&
isPlainString(values[i])
) {
values[i - 1] += delimiter + values[i];
values.splice(i, 1);
} else {
i += 1;
}
}
return values;
function isPlainString(obj: any): boolean {
return typeof obj === "string" && !Token.isUnresolved(obj);
}
function isSplicableFnJoinIntrinsic(obj: any): boolean {
if (!isIntrinsic(obj)) {
return false;
}
if (Object.keys(obj)[0] !== "Fn::Join") {
return false;
}
const [delim, list] = obj["Fn::Join"];
if (delim !== delimiter) {
return false;
}
if (Token.isUnresolved(list)) {
return false;
}
if (!Array.isArray(list)) {
return false;
}
return true;
}
}
/**
* Do an intelligent ROS merge list on the given values, producing a minimal expression
*/
export function minimalRosTemplateListMerge(
values: any[]
): any[] {
let i = 0;
while (i < values.length) {
const el = values[i];
if (isSplicableFnListMergeIntrinsic(el)) {
values.splice(i, 1, ...el["Fn::ListMerge"]);
} else {
i += 1;
}
}
return values;
function isSplicableFnListMergeIntrinsic(obj: any): boolean {
if (!isIntrinsic(obj)) {
return false;
}
if (Object.keys(obj)[0] !== "Fn::ListMerge") {
return false;
}
const list = obj["Fn::ListMerge"];
if (Token.isUnresolved(list)) {
return false;
}
return Array.isArray(list);
}
}
/**
* Return whether the given value represents a ROS intrinsic
*/
function isIntrinsic(x: any) {
if (Array.isArray(x) || x === null || typeof x !== "object") {
return false;
}
const keys = Object.keys(x);
if (keys.length !== 1) {
return false;
}
return keys[0] === "Ref" || isNameOfRosIntrinsic(keys[0]);
}
export function isNameOfRosIntrinsic(name: string): boolean {
if (!name.startsWith("Fn::")) {
return false;
}
// these are 'fake' intrinsics, only usable inside the parameter overrides of a ROS CodePipeline Action
return name !== "Fn::GetArtifactAtt" && name !== "Fn::GetParam";
}