Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript (v3): Libs - Add more common steps, allow for default values, and refactor. #6720

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading