diff --git a/javascriptv3/example_code/libs/scenario/index.js b/javascriptv3/example_code/libs/scenario/index.js index eaa95d7ac30..04450e02d34 100644 --- a/javascriptv3/example_code/libs/scenario/index.js +++ b/javascriptv3/example_code/libs/scenario/index.js @@ -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, +}; diff --git a/javascriptv3/example_code/libs/scenario/scenario.js b/javascriptv3/example_code/libs/scenario/scenario.js index 3853ebd9457..a90867d951d 100644 --- a/javascriptv3/example_code/libs/scenario/scenario.js +++ b/javascriptv3/example_code/libs/scenario/scenario.js @@ -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 */ @@ -133,52 +134,154 @@ export class ScenarioInput extends Step { * @param {Record} 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} 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} 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} 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} 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} state + * @param {string} message + */ + async _handleConfirm(state, message) { + const result = await this.prompter.confirm({ + message, + }); + + state[this.name] = result; } } @@ -240,9 +343,9 @@ export class ScenarioAction extends Step { export class Scenario { /** - * @type {Record} + * @type { { earlyExit: boolean } & Record} */ - state = {}; + state; /** * @type {(ScenarioOutput | ScenarioInput | ScenarioAction | Scenario)[]} @@ -257,7 +360,7 @@ 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 }; } /** @@ -265,6 +368,13 @@ export class Scenario { */ 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 { diff --git a/javascriptv3/example_code/libs/scenario/steps-common.js b/javascriptv3/example_code/libs/scenario/steps-common.js index 49e07c8e47e..c4ce4e268eb 100644 --- a/javascriptv3/example_code/libs/scenario/steps-common.js +++ b/javascriptv3/example_code/libs/scenario/steps-common.js @@ -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} */ state) => { + if (!state[stateKey]) { + state.earlyExit = true; + } + }, + ); + +/** + * @param {Scenarios} scenarios + */ +export const confirm = (scenarios) => + new scenarios.ScenarioInput("confirmContinue", "Continue?", { + type: "confirm", }); diff --git a/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/import-steps.js b/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/import-steps.js index 7c519b53fca..f3129f14f0f 100644 --- a/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/import-steps.js +++ b/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/import-steps.js @@ -26,6 +26,7 @@ export const doImport = new ScenarioInput( "Do you want to import DICOM images into your datastore?", { type: "confirm", + default: true, }, ); diff --git a/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/verify-steps.js b/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/verify-steps.js index fc8d4711cd3..a408d368d7c 100644 --- a/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/verify-steps.js +++ b/javascriptv3/example_code/medical-imaging/scenarios/health-image-sets/verify-steps.js @@ -69,6 +69,7 @@ export const doVerify = new ScenarioInput( "Do you want to verify the imported images?", { type: "confirm", + default: true, }, ); diff --git a/javascriptv3/example_code/medical-imaging/tests/hlth-img-deploy.unit.test.js b/javascriptv3/example_code/medical-imaging/tests/hlth-img-deploy.unit.test.js index 31ffeb45f24..2b4c35d254d 100644 --- a/javascriptv3/example_code/medical-imaging/tests/hlth-img-deploy.unit.test.js +++ b/javascriptv3/example_code/medical-imaging/tests/hlth-img-deploy.unit.test.js @@ -242,6 +242,7 @@ describe("deploy-steps", () => { stateFilePath, JSON.stringify({ name: deploySteps.name, + earlyExit: false, deployStack: true, getStackName: stackName, getDatastoreName: datastoreName, diff --git a/javascriptv3/example_code/medical-imaging/tests/hlth-img-image-frame.unit.test.js b/javascriptv3/example_code/medical-imaging/tests/hlth-img-image-frame.unit.test.js index 23f4e58d3a2..c84d7a82909 100644 --- a/javascriptv3/example_code/medical-imaging/tests/hlth-img-image-frame.unit.test.js +++ b/javascriptv3/example_code/medical-imaging/tests/hlth-img-image-frame.unit.test.js @@ -25,6 +25,7 @@ const { getImageSetMetadata, outputImageFrameIds } = await import( describe("image-frame-steps", () => { const mockState = { + earlyExit: false, stackOutputs: { BucketName: "input-bucket", DatastoreID: "datastore-123", diff --git a/javascriptv3/example_code/medical-imaging/tests/hlth-img-import.unit.test.js b/javascriptv3/example_code/medical-imaging/tests/hlth-img-import.unit.test.js index c71fa601410..41729925720 100644 --- a/javascriptv3/example_code/medical-imaging/tests/hlth-img-import.unit.test.js +++ b/javascriptv3/example_code/medical-imaging/tests/hlth-img-import.unit.test.js @@ -30,6 +30,7 @@ const { describe("importSteps", () => { const mockState = { name: "import-steps", + earlyExit: false, stackOutputs: { BucketName: "input-bucket", DatastoreID: "datastore-123", diff --git a/javascriptv3/example_code/medical-imaging/tests/hlth-img-verify.unit.test.js b/javascriptv3/example_code/medical-imaging/tests/hlth-img-verify.unit.test.js index 37e05dcb2e4..a569900f9f0 100644 --- a/javascriptv3/example_code/medical-imaging/tests/hlth-img-verify.unit.test.js +++ b/javascriptv3/example_code/medical-imaging/tests/hlth-img-verify.unit.test.js @@ -22,6 +22,7 @@ const { doVerify, decodeAndVerifyImages } = await import( describe("verifySteps", () => { const mockState = { + earlyExit: false, stackOutputs: { BucketName: "input-bucket", DatastoreID: "datastore-123", diff --git a/javascriptv3/example_code/medical-imaging/tests/htl-img-image-set.unit.test.js b/javascriptv3/example_code/medical-imaging/tests/htl-img-image-set.unit.test.js index e869cb90475..5a35f9c76ec 100644 --- a/javascriptv3/example_code/medical-imaging/tests/htl-img-image-set.unit.test.js +++ b/javascriptv3/example_code/medical-imaging/tests/htl-img-image-set.unit.test.js @@ -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",