lib/apiScenario/variableEnv.ts (323 lines of code) (raw):

import { Variable, StringVariable, SecureStringVariable, ObjectVariable, SecureObjectVariable, VarValue, ArrayVariable, } from "./apiScenarioTypes"; import { jsonPatchApply } from "./diffUtils"; const variableRegex = /\$\(([A-Za-z_$][A-Za-z0-9_]*)\)/; const pathVariableRegex = /\{([A-Za-z_$][A-Za-z0-9_]*)\}/; export class EnvironmentVariables { [key: string]: string; } export class VariableEnv { protected baseEnv?: VariableEnv; protected data: { [key: string]: Variable } = {}; protected defaultValue: { [key: string]: Variable } = {}; public constructor(baseEnv?: VariableEnv) { if (baseEnv !== undefined) { this.baseEnv = baseEnv; } } public *getVariables(): Iterable<[string, Variable]> { const visitedSet = new Set<string>(); for (const key of Object.keys(this.data)) { if (!visitedSet.has(key)) { visitedSet.add(key); yield [key, this.get(key)!]; } } if (this.baseEnv !== undefined) { for (const [key, value] of this.baseEnv.getVariables()) { if (!visitedSet.has(key)) { visitedSet.add(key); yield [key, value]; } } } } public clear() { for (const key of Object.keys(this.data)) { delete this.data[key]; } } public getKeyList(): string[] { const keys = this.baseEnv?.getKeyList() ?? []; return [...keys, ...Object.keys(this.data)]; } public setBaseEnv(baseEnv: VariableEnv) { this.baseEnv = baseEnv; } public setDefaultValue(defaultValue: { [key: string]: Variable }) { this.defaultValue = defaultValue; } public getType(key: string): Variable["type"] | undefined { return this.get(key)?.type; } public getString(key: string): string | undefined { const val = this.get(key); if (val?.type === "string" || val?.type === "secureString") { return val.value; } return undefined; } public getObject(key: string): { [key: string]: VarValue } | undefined { const val = this.get(key); if (val?.type === "object" || val?.type === "secureObject") { return val.value; } return undefined; } public getArray(key: string): VarValue[] | undefined { const val = this.get(key); if (val?.type === "array") { return val.value; } return undefined; } public getBool(key: string): boolean | undefined { const val = this.get(key); if (val?.type === "bool") { return val.value; } return undefined; } public getInt(key: string): number | undefined { const val = this.get(key); if (val && val.type === "int") { return val.value; } return undefined; } public get(key: string): Variable | undefined { return this.data[key] ?? this.baseEnv?.get(key) ?? this.defaultValue[key]; } public getRequiredString(key: string): string { const val = this.getRequired(key); if (val.type !== "string" && val.type !== "secureString") { throw new Error(`Variable ${key} is not a string`); } return val.value as string; } public getRequiredObject(key: string): { [key: string]: VarValue } { const val = this.getRequired(key); if (val.type !== "object" && val.type !== "secureObject") { throw new Error(`Variable ${key} is not an object`); } return val.value as { [key: string]: VarValue }; } public getRequiredArray(key: string): VarValue[] { const val = this.getRequired(key); if (val.type !== "array") { throw new Error(`Variable ${key} is not an array`); } return val.value as VarValue[]; } public getRequiredBool(key: string): boolean { const val = this.getRequired(key); if (val.type !== "bool") { throw new Error(`Variable ${key} is not a boolean`); } return val.value as boolean; } public getRequiredInt(key: string): number { const val = this.getRequired(key); if (val.type !== "int") { throw new Error(`Variable ${key} is not an integer`); } return val.value as number; } public getRequired(key: string): Variable { const val = this.get(key); if (val?.value === undefined) { throw new Error(`Variable is required but is not found in VariableEnv: ${key}`); } return val; } public set(key: string, value: Variable) { this.data[key] = value; } public output(key: string, value: Variable) { this.baseEnv?.set(key, value); } public setBatch(values: { [key: string]: Variable }): VariableEnv { for (const [key, value] of Object.entries(values)) { this.set(key, value); } return this; } public setBatchEnv(environmentVariables: EnvironmentVariables): VariableEnv { for (const [key, value] of Object.entries(environmentVariables)) { const varType = this.getType(key) ?? "string"; if (varType !== "string" && varType !== "secureString") { throw new Error(`String value is not assignable to variable ${key} of type ${varType}`); } this.set(key, { type: varType, value, }); } return this; } public resolve() { this.baseEnv?.resolve(); for (const key of Object.keys(this.data)) { const val = this.data[key]; switch (val.type) { case "string": case "secureString": this.doResolveStringVariable(key, val); break; case "object": case "secureObject": case "array": this.doResolveObjectVariable(key, val); break; case "bool": case "int": default: break; } } } private doResolveStringVariable(key: string, val: StringVariable | SecureStringVariable) { if (val.value && variableRegex.test(val.value)) { const globalRegex = new RegExp(variableRegex, "g"); const replaceArray: Array<[number, number, string | undefined]> = []; let match; while ((match = globalRegex.exec(val.value))) { const refKey = match[1]; const refVal = refKey === key ? this.baseEnv?.get(key) ?? val : this.get(refKey); if (refVal && refVal.type !== "string" && refVal.type !== "secureString") { throw new Error(`Invalid reference variable type: ${refKey}`); } if (refVal?.type === "secureString") { val.type = "secureString"; } replaceArray.push([match.index, match.index + match[0].length, refVal?.value]); } let r; while ((r = replaceArray.pop())) { if (r[2] !== undefined) { val.value = val.value.substring(0, r[0]) + r[2] + val.value.substring(r[1]); } } } } private doResolveObjectVariable( key: string, val: ObjectVariable | SecureObjectVariable | ArrayVariable ) { if (val.value) { val.value = this.resolveObjectValues(val.value); } else if (val.patches) { const refVal = this.baseEnv?.get(key); if ( refVal && refVal.type !== "object" && refVal.type !== "secureObject" && refVal.type !== "array" ) { throw new Error(`Invalid reference variable type: ${key}`); } if (refVal?.type === "secureObject") { val.type = "secureObject"; } if (refVal?.value) { val.value = refVal.value; val.value = jsonPatchApply(val.value, val.patches); } } } public tryResolveString(source: string): string { if (variableRegex.test(source)) { const globalRegex = new RegExp(variableRegex, "g"); const replaceArray: Array<[number, number, string]> = []; let match; while ((match = globalRegex.exec(source))) { const variable = this.get(match[1]); if (variable === undefined) { continue; } if (variable.type !== "string" && variable.type !== "secureString") { throw new Error(`Variable type is not string: ${match[1]}`); } replaceArray.push([match.index, match.index + match[0].length, variable.value!.toString()]); } let r; while ((r = replaceArray.pop())) { source = source.substring(0, r[0]) + r[2] + source.substring(r[1]); } } return source; } public resolveString(source: string, isPathVariable?: boolean): string { return this.resolveStringWithRegex(source, isPathVariable || false) as string; } private resolveStringWithRegex( source: string, isPathVariable: boolean ): string | number | boolean { const regex = isPathVariable ? pathVariableRegex : variableRegex; if (regex.test(source)) { const globalRegex = new RegExp(regex, "g"); const replaceArray: Array<[number, number, string]> = []; let match; while ((match = globalRegex.exec(source))) { const variable = this.getRequired(match[1]); if ( variable.type !== "string" && variable.type !== "secureString" && variable.type !== "int" && variable.type !== "bool" ) { throw new Error(`Variable type is not string, int, bool: ${match[1]}`); } if (match.index === 0 && match[0].length === source.length) { return variable.value!; } replaceArray.push([match.index, match.index + match[0].length, variable.value!.toString()]); } let r; while ((r = replaceArray.pop())) { source = source.substring(0, r[0]) + r[2] + source.substring(r[1]); } } return source; } public resolveObjectValues<T>(obj: T): T { if (typeof obj === "string") { return this.resolveStringWithRegex(obj, false) as unknown as T; } return this.resolveObjectValuesWithRegex(obj); } private resolveObjectValuesWithRegex<T>(obj: T): T { if (typeof obj !== "object") { return obj; } if (obj === null || obj === undefined) { return obj; } if (Array.isArray(obj)) { return (obj as any[]).map((v) => { if (typeof v === "string") { return this.resolveStringWithRegex(v, false); } return this.resolveObjectValuesWithRegex(v); }) as unknown as T; } const result: any = {}; for (const key of Object.keys(obj)) { const newKey = this.resolveStringWithRegex(key, false).toString(); if (typeof (obj as any)[key] === "string") { result[newKey] = this.resolveStringWithRegex((obj as any)[key], false); } else { result[newKey] = this.resolveObjectValuesWithRegex((obj as any)[key]); } } return result; } }