Skip to content

Commit

Permalink
JavaScript (v3): Libs - Add more common steps, allow for default valu…
Browse files Browse the repository at this point in the history
…es, and refactor. (awsdocs#6720)
  • Loading branch information
cpyle0819 authored and shepazon committed Aug 23, 2024
1 parent c54aeea commit 958b238
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 32 deletions.
8 changes: 8 additions & 0 deletions javascriptv3/example_code/libs/scenario/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import * as scenario from "./scenario.js";
import * as scenarioParser from "./scenario-parser.js";

export * from "./scenario.js";
export * from "./scenario-parser.js";

export default {
...scenario,
...scenarioParser,
};
166 changes: 138 additions & 28 deletions javascriptv3/example_code/libs/scenario/scenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ export class ScenarioOutput extends Step {
/**
* @typedef {{
* type: "confirm" | "input" | "multi-select" | "select",
* choices: (string | { name: string, value: string })[] }
* choices: (string | { name: string, value: string })[],
* default: string | string[] | boolean }
* } ScenarioInputOptions
*/

Expand All @@ -133,52 +134,154 @@ export class ScenarioInput extends Step {
* @param {Record<string, any>} state
* @param {StepHandlerOptions} [stepHandlerOptions]
*/
async handle(state, stepHandlerOptions) {
async handle(state, stepHandlerOptions = {}) {
if (this.stepOptions.skipWhen(state)) {
console.log(`Skipping step: ${this.name}`);
return;
}
super.handle(state, stepHandlerOptions);
const message =

if (stepHandlerOptions.confirmAll && this.stepOptions.default) {
state[this.name] = this.stepOptions.default;
return state[this.name];
} else if (stepHandlerOptions.confirmAll && !this.stepOptions.default) {
if (this.stepOptions?.type === "confirm") {
state[this.name] = true;
return true;
}
throw new Error(
`Error handling ScenarioInput. confirmAll was selected for ${this.name} but no default was provided.`,
);
}

const message = this._getPrompt(state);

switch (this.stepOptions?.type) {
case "multi-select":
await this._handleMultiSelect(state, message);
break;
case "select":
await this._handleSelect(state, message);
break;
case "input":
await this._handleInput(state, message);
break;
case "confirm":
await this._handleConfirm(state, message);
break;
default:
throw new Error(
`Error handling ScenarioInput, ${this.stepOptions?.type} is not supported.`,
);
}

return state[this.name];
}

/**
* @param {Record<string, any>} state
*/
_getPrompt(state) {
const prompt =
typeof this.prompt === "function" ? this.prompt(state) : this.prompt;
const message =
this.stepOptions.type !== "confirm" && this.stepOptions.default
? `${prompt} (${this.stepOptions.default})`
: prompt;

if (!message) {
return;
throw new Error(`Error handling ScenarioInput. Missing prompt.`);
}

return message;
}

_getChoices() {
if (this.choices) {
return this.choices;
}

const choices =
this.choices =
this.stepOptions?.choices &&
typeof this.stepOptions?.choices[0] === "string"
? this.stepOptions?.choices.map((s) => ({ name: s, value: s }))
: this.stepOptions?.choices;

if (this.stepOptions?.type === "multi-select") {
state[this.name] = await this.prompter.checkbox({
message,
choices,
});
} else if (this.stepOptions?.type === "select") {
state[this.name] = await this.prompter.select({
return this.choices;
}

/**
* @param {Record<string, any>} state
* @param {string} message
*/
async _handleMultiSelect(state, message) {
const result = await this.prompter.checkbox({
message,
choices: this._getChoices(),
});

if (!result.length && this.stepOptions.default) {
state[this.name] = this.stepOptions.default;
} else if (!result.length) {
throw new Error(
`Error handing ScenarioInput. Result of ${this.name} was empty.`,
);
} else {
state[this.name] = result;
}
}

/**
* @param {Record<string, any>} state
* @param {string} message
*/
async _handleSelect(state, message) {
if (this.stepOptions?.type === "select") {
const result = await this.prompter.select({
message,
choices,
choices: this._getChoices(),
});
} else if (this.stepOptions?.type === "input") {
state[this.name] = await this.prompter.input({ message });
} else if (this.stepOptions?.type === "confirm") {
if (stepHandlerOptions?.confirmAll) {
state[this.name] = true;
return true;

if (!result && this.stepOptions.default) {
state[this.name] = this.stepOptions.default;
} else if (!result) {
throw new Error(
`Error handing ScenarioInput. Result of ${this.name} was empty.`,
);
} else {
state[this.name] = result;
}
}
}

state[this.name] = await this.prompter.confirm({
message,
});
} else {
/**
* @param {Record<string, any>} state
* @param {string} message
*/
async _handleInput(state, message) {
const result = await this.prompter.input({ message });

if (!result && this.stepOptions.default) {
state[this.name] = this.stepOptions.default;
} else if (!result) {
throw new Error(
`Error handling ScenarioInput, ${this.stepOptions?.type} is not supported.`,
`Error handing ScenarioInput. Result of ${this.name} was empty.`,
);
} else {
state[this.name] = result;
}
}

return state[this.name];
/**
* @param {Record<string, any>} state
* @param {string} message
*/
async _handleConfirm(state, message) {
const result = await this.prompter.confirm({
message,
});

state[this.name] = result;
}
}

Expand Down Expand Up @@ -240,9 +343,9 @@ export class ScenarioAction extends Step {

export class Scenario {
/**
* @type {Record<string, any>}
* @type { { earlyExit: boolean } & Record<string, any>}
*/
state = {};
state;

/**
* @type {(ScenarioOutput | ScenarioInput | ScenarioAction | Scenario)[]}
Expand All @@ -257,14 +360,21 @@ export class Scenario {
constructor(name, stepsOrScenarios = [], initialState = {}) {
this.name = name;
this.stepsOrScenarios = stepsOrScenarios.filter((s) => !!s);
this.state = { ...initialState, name };
this.state = { ...initialState, name, earlyExit: false };
}

/**
* @param {StepHandlerOptions} stepHandlerOptions
*/
async run(stepHandlerOptions) {
for (const stepOrScenario of this.stepsOrScenarios) {
/**
* Add an escape hatch for actions that terminate the scenario early.
*/
if (this.state.earlyExit) {
return;
}

if (stepOrScenario instanceof Scenario) {
await stepOrScenario.run(stepHandlerOptions);
} else {
Expand Down
19 changes: 15 additions & 4 deletions javascriptv3/example_code/libs/scenario/steps-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ export const loadState = new Scenarios.ScenarioAction(
* @param {string} stateKey
*/
export const exitOnFalse = (scenarios, stateKey) =>
new scenarios.ScenarioAction(`exitOn${stateKey}False`, (state) => {
if (!state[stateKey]) {
process.exit(0);
}
new scenarios.ScenarioAction(
`exitOn${stateKey}False`,
(/** @type { { earlyExit: boolean } & Record<string, any>} */ state) => {
if (!state[stateKey]) {
state.earlyExit = true;
}
},
);

/**
* @param {Scenarios} scenarios
*/
export const confirm = (scenarios) =>
new scenarios.ScenarioInput("confirmContinue", "Continue?", {
type: "confirm",
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const doImport = new ScenarioInput(
"Do you want to import DICOM images into your datastore?",
{
type: "confirm",
default: true,
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const doVerify = new ScenarioInput(
"Do you want to verify the imported images?",
{
type: "confirm",
default: true,
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ describe("deploy-steps", () => {
stateFilePath,
JSON.stringify({
name: deploySteps.name,
earlyExit: false,
deployStack: true,
getStackName: stackName,
getDatastoreName: datastoreName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const { getImageSetMetadata, outputImageFrameIds } = await import(

describe("image-frame-steps", () => {
const mockState = {
earlyExit: false,
stackOutputs: {
BucketName: "input-bucket",
DatastoreID: "datastore-123",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
describe("importSteps", () => {
const mockState = {
name: "import-steps",
earlyExit: false,
stackOutputs: {
BucketName: "input-bucket",
DatastoreID: "datastore-123",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const { doVerify, decodeAndVerifyImages } = await import(

describe("verifySteps", () => {
const mockState = {
earlyExit: false,
stackOutputs: {
BucketName: "input-bucket",
DatastoreID: "datastore-123",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { getManifestFile, parseManifestFile, outputImageSetIds } = await import(

describe("image-set-steps", () => {
const mockState = {
earlyExit: false,
stackOutputs: {
BucketName: "input-bucket",
DatastoreID: "datastore-123",
Expand Down

0 comments on commit 958b238

Please sign in to comment.