Skip to content

Commit 0ec9895

Browse files
authored
Support for writing params of type List during discovery (#1283)
1 parent 27ce234 commit 0ec9895

File tree

5 files changed

+131
-8
lines changed

5 files changed

+131
-8
lines changed

spec/fixtures/sources/commonjs-params/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ params.defineInt("ANOTHER_INT", {
1919
},
2020
});
2121

22+
params.defineList("LIST_PARAM", {input: { multiSelect: { options: [{ value: "c" }, { value: "d" }, { value: "e" }]}}})
23+
2224
params.defineSecret("SUPER_SECRET_FLAG");
2325

2426
// N.B: invocation of the precanned internal params should not affect the manifest

spec/params/params.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ describe("Params value extraction", () => {
2727
process.env.PI = "3.14159";
2828
process.env.TRUE = "true";
2929
process.env.FALSE = "false";
30+
process.env.LIST = JSON.stringify(["a", "b", "c"]);
31+
process.env.BAD_LIST = JSON.stringify(["a", 22, "c"]);
32+
process.env.ESCAPED_LIST = JSON.stringify(["f\to\no"]);
3033
process.env.A_SECRET_STRING = "123456supersecret";
3134
});
3235

@@ -42,6 +45,9 @@ describe("Params value extraction", () => {
4245
delete process.env.PI;
4346
delete process.env.TRUE;
4447
delete process.env.FALSE;
48+
delete process.env.LIST;
49+
delete process.env.BAD_LIST;
50+
delete process.env.ESCAPED_LIST;
4551
delete process.env.A_SECRET_STRING;
4652
});
4753

@@ -61,6 +67,11 @@ describe("Params value extraction", () => {
6167
const falseParam = params.defineBoolean("FALSE");
6268
expect(falseParam.value()).to.be.false;
6369

70+
const listParam = params.defineList("LIST");
71+
expect(listParam.value()).to.deep.equal(["a", "b", "c"]);
72+
73+
const listParamWithEscapes = params.defineList("ESCAPED_LIST");
74+
expect(listParamWithEscapes.value()).to.deep.equal(["f\to\no"]);
6475
const secretParam = params.defineSecret("A_SECRET_STRING");
6576
expect(secretParam.value()).to.equal("123456supersecret");
6677
});
@@ -97,6 +108,17 @@ describe("Params value extraction", () => {
97108

98109
const stringToBool = params.defineBoolean("A_STRING");
99110
expect(stringToBool.value()).to.equal(false);
111+
112+
const listToInt = params.defineInt("LIST");
113+
expect(listToInt.value()).to.equal(0);
114+
});
115+
116+
it("falls back on the javascript zero values in case a list param's is unparsable as string[]", () => {
117+
const notAllStrings = params.defineList("BAD_LIST");
118+
expect(notAllStrings.value()).to.deep.equal([]);
119+
120+
const intToList = params.defineList("AN_INT");
121+
expect(intToList.value()).to.deep.equal([]);
100122
});
101123

102124
it("returns a boolean value for Comparison expressions", () => {
@@ -177,6 +199,18 @@ describe("Params value extraction", () => {
177199
expect(trueParam.cmp("<=", false).value()).to.be.false;
178200
});
179201

202+
it("can test list params for equality but not < or >", () => {
203+
const p1 = params.defineList("LIST");
204+
const p2 = params.defineList("ESCAPED_LIST");
205+
206+
expect(p1.equals(p1).value()).to.be.true;
207+
expect(p1.notEquals(p1).value()).to.be.false;
208+
expect(p1.equals(p2).value()).to.be.false;
209+
expect(p1.notEquals(p2).value()).to.be.true;
210+
211+
expect(() => p1.greaterThan(p1).value()).to.throw;
212+
});
213+
180214
it("can select the output of a ternary expression based on the comparison", () => {
181215
const trueExpr = params.defineString("A_STRING").equals(params.defineString("SAME_STRING"));
182216
expect(trueExpr.thenElse(1, 0).value()).to.equal(1);

spec/runtime/loader.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,13 @@ describe("loadStack", () => {
365365
},
366366
},
367367
},
368+
{
369+
name: "LIST_PARAM",
370+
type: "list",
371+
input: {
372+
multiSelect: { options: [{ value: "c" }, { value: "d" }, { value: "e" }] },
373+
},
374+
},
368375
{ name: "SUPER_SECRET_FLAG", type: "secret" },
369376
],
370377
},

src/params/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
ParamOptions,
3535
SecretParam,
3636
StringParam,
37+
ListParam,
3738
InternalExpression,
3839
} from "./types";
3940

@@ -164,3 +165,16 @@ export function defineFloat(name: string, options: ParamOptions<number> = {}): F
164165
registerParam(param);
165166
return param;
166167
}
168+
169+
/**
170+
* Declare a list param.
171+
*
172+
* @param name The name of the environment variable to use to load the param.
173+
* @param options Configuration options for the param.
174+
* @returns A Param with a `string[]` return type for `.value`.
175+
*/
176+
export function defineList(name: string, options: ParamOptions<string[]> = {}): ListParam {
177+
const param = new ListParam(name, options);
178+
registerParam(param);
179+
return param;
180+
}

src/params/types.ts

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,26 @@ export abstract class Expression<T extends string | number | boolean | string[]>
5757
}
5858
}
5959

60-
function quoteIfString<T extends string | number | boolean | string[]>(literal: T): T {
61-
// TODO(vsfan@): CEL's string escape semantics are slightly different than Javascript's, what do we do here?
62-
return typeof literal === "string" ? (`"${literal}"` as T) : literal;
63-
}
64-
6560
function valueOf<T extends string | number | boolean | string[]>(arg: T | Expression<T>): T {
6661
return arg instanceof Expression ? arg.runtimeValue() : arg;
6762
}
63+
/**
64+
* Returns how an entity (either an Expression or a literal value) should be represented in CEL.
65+
* - Expressions delegate to the .toString() method, which is used by the WireManifest
66+
* - Strings have to be quoted explicitly
67+
* - Arrays are represented as []-delimited, parsable JSON
68+
* - Numbers and booleans are not quoted explicitly
69+
*/
6870
function refOf<T extends string | number | boolean | string[]>(arg: T | Expression<T>): string {
69-
return arg instanceof Expression ? arg.toString() : quoteIfString(arg).toString();
71+
if (arg instanceof Expression) {
72+
return arg.toString();
73+
} else if (typeof arg === "string") {
74+
return `"${arg}"`;
75+
} else if (Array.isArray(arg)) {
76+
return JSON.stringify(arg);
77+
} else {
78+
return arg.toString();
79+
}
7080
}
7181

7282
/**
@@ -123,9 +133,9 @@ export class CompareExpression<
123133
const right = valueOf(this.rhs);
124134
switch (this.cmp) {
125135
case "==":
126-
return left === right;
136+
return Array.isArray(left) ? this.arrayEquals(left, right as string[]) : left === right;
127137
case "!=":
128-
return left !== right;
138+
return Array.isArray(left) ? !this.arrayEquals(left, right as string[]) : left !== right;
129139
case ">":
130140
return left > right;
131141
case ">=":
@@ -139,6 +149,11 @@ export class CompareExpression<
139149
}
140150
}
141151

152+
/** @internal */
153+
arrayEquals(a: string[], b: string[]): boolean {
154+
return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
155+
}
156+
142157
toString() {
143158
const rhsStr = refOf(this.rhs);
144159
return `${this.lhs} ${this.cmp} ${rhsStr}`;
@@ -159,6 +174,7 @@ type ParamValueType = "string" | "list" | "boolean" | "int" | "float" | "secret"
159174
type ParamInput<T> =
160175
| { text: TextInput<T> }
161176
| { select: SelectInput<T> }
177+
| { multiSelect: MultiSelectInput }
162178
| { resource: ResourceInput };
163179

164180
/**
@@ -201,6 +217,15 @@ export interface SelectInput<T = unknown> {
201217
options: Array<SelectOptions<T>>;
202218
}
203219

220+
/**
221+
* Specifies that a Param's value should be determined by having the user select
222+
* a subset from a list of pre-canned options interactively at deploy-time.
223+
* Will result in errors if used on Params of type other than string[].
224+
*/
225+
export interface MultiSelectInput {
226+
options: Array<SelectOptions<string>>;
227+
}
228+
204229
/**
205230
* One of the options provided to a SelectInput, containing a value and
206231
* optionally a human-readable label to display in the selection interface.
@@ -464,3 +489,44 @@ export class BooleanParam extends Param<boolean> {
464489
return new TernaryExpression(this, ifTrue, ifFalse);
465490
}
466491
}
492+
493+
/**
494+
* A parametrized value of String[] type that will be read from .env files
495+
* if present, or prompted for by the CLI if missing.
496+
*/
497+
export class ListParam extends Param<string[]> {
498+
static type: ParamValueType = "list";
499+
500+
/** @internal */
501+
runtimeValue(): string[] {
502+
const val = JSON.parse(process.env[this.name]);
503+
if (!Array.isArray(val) || !(val as string[]).every((v) => typeof v === "string")) {
504+
return [];
505+
}
506+
return val as string[];
507+
}
508+
509+
/** @hidden */
510+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
511+
greaterThan(rhs: string[] | Expression<string[]>): CompareExpression<string[]> {
512+
throw new Error(">/< comparison operators not supported on params of type List");
513+
}
514+
515+
/** @hidden */
516+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
517+
greaterThanOrEqualTo(rhs: string[] | Expression<string[]>): CompareExpression<string[]> {
518+
throw new Error(">/< comparison operators not supported on params of type List");
519+
}
520+
521+
/** @hidden */
522+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
523+
lessThan(rhs: string[] | Expression<string[]>): CompareExpression<string[]> {
524+
throw new Error(">/< comparison operators not supported on params of type List");
525+
}
526+
527+
/** @hidden */
528+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
529+
lessThanorEqualTo(rhs: string[] | Expression<string[]>): CompareExpression<string[]> {
530+
throw new Error(">/< comparison operators not supported on params of type List");
531+
}
532+
}

0 commit comments

Comments
 (0)