From b11eaabb0050961094f412d85c2bbbf4b183cd40 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 28 Feb 2022 13:40:27 -0600 Subject: [PATCH] feat: duration flag --- messages/messages.md | 8 +++ package.json | 2 +- src/exported.ts | 1 + src/flags/duration.ts | 71 ++++++++++++++++++ test/unit/flags/duration.test.ts | 119 +++++++++++++++++++++++++++++++ yarn.lock | 8 +-- 6 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 src/flags/duration.ts create mode 100644 test/unit/flags/duration.test.ts diff --git a/messages/messages.md b/messages/messages.md index 5ff591e1..31da845a 100644 --- a/messages/messages.md +++ b/messages/messages.md @@ -65,3 +65,11 @@ No file found: %s. # flags.existingFile.errors.NotAFile No file found: %s. + +# flags.duration.errors.InvalidInput + +The value must be an integer. + +# flags.duration.errors.DurationBounds + +The value must be between %s and %s (inclusive). diff --git a/package.json b/package.json index c3616fae..42b2ad42 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "/messages" ], "dependencies": { - "@oclif/core": "^1.2.0", + "@oclif/core": "^1.3.6", "@salesforce/core": "3.7.3", "@salesforce/kit": "^1.5.17", "@salesforce/ts-types": "^1.5.20", diff --git a/src/exported.ts b/src/exported.ts index 5bd1d2dd..25135f39 100644 --- a/src/exported.ts +++ b/src/exported.ts @@ -19,3 +19,4 @@ export { requiredOrgFlag, requiredHubFlag } from './flags/orgFlags'; export { buildIdFlag } from './flags/salesforceId'; export { apiVersionFlag } from './flags/apiVersion'; export { existingDirectory, existingFile } from './flags/fsFlags'; +export { buildDurationFlag, DurationFlagConfig } from './flags/duration'; diff --git a/src/flags/duration.ts b/src/flags/duration.ts new file mode 100644 index 00000000..5e63ddeb --- /dev/null +++ b/src/flags/duration.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { Flags } from '@oclif/core'; +import { Definition } from '@oclif/core/lib/interfaces'; +import { Messages } from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages'); + +type DurationUnit = Lowercase; + +export interface DurationFlagConfig { + unit: Required; + defaultValue?: number; + min?: number; + max?: number; +} + +/** + * Duration flag with built-in default and min/max validation + * You must specify a unit + * Defaults to undefined if you don't specify a default + * + * @example + * import { SfCommand, buildDurationFlag } from '@salesforce/sf-plugins-core'; + * public static flags = { + * 'wait': buildDurationFlag({ min: 1, unit: , defaultValue: 33 })({ + * char: 'w', + * description: 'Wait time in minutes' + * }), + * } + */ +export const buildDurationFlag = (durationConfig: DurationFlagConfig): Definition => { + return Flags.build({ + parse: async (input: string) => validate(input, durationConfig), + default: durationConfig.defaultValue + ? async () => toDuration(durationConfig.defaultValue as number, durationConfig.unit) + : undefined, + }); +}; + +const validate = (input: string, config: DurationFlagConfig): Duration => { + const { min, max, unit } = config || {}; + let parsedInput: number; + + try { + parsedInput = parseInt(input, 10); + if (typeof parsedInput !== 'number' || isNaN(parsedInput)) { + throw messages.createError('flags.duration.errors.InvalidInput'); + } + } catch (e) { + throw messages.createError('flags.duration.errors.InvalidInput'); + } + + if (min && parsedInput < min) { + throw messages.createError('flags.duration.errors.DurationBounds', [min, max]); + } + if (max && parsedInput > max) { + throw messages.createError('flags.duration.errors.DurationBounds', [min, max]); + } + return toDuration(parsedInput, unit); +}; + +const toDuration = (parsedInput: number, unit: DurationUnit): Duration => { + return Duration[unit](parsedInput); +}; diff --git a/test/unit/flags/duration.test.ts b/test/unit/flags/duration.test.ts new file mode 100644 index 00000000..89e769ac --- /dev/null +++ b/test/unit/flags/duration.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Parser } from '@oclif/core'; +import { Messages } from '@salesforce/core'; +import { expect } from 'chai'; +import { Duration } from '@salesforce/kit'; +import { buildDurationFlag } from '../../../src/flags/duration'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages'); + +describe('duration flag', () => { + describe('no default, hours', () => { + const buildProps = { + flags: { + wait: buildDurationFlag({ + unit: 'hours', + })({ description: 'test', char: 'w' }), + }, + }; + it('passes', async () => { + const out = await Parser.parse(['--wait=10'], buildProps); + expect(out.flags.wait.quantity).to.equal(10); + expect(out.flags.wait.unit).to.equal(Duration.Unit.HOURS); + }); + it('passes with default', async () => { + const out = await Parser.parse([], buildProps); + expect(out.flags.wait).to.equal(undefined); + }); + }); + + describe('validation with no options and weeks unit', () => { + const defaultValue = 33; + const buildProps = { + flags: { + wait: buildDurationFlag({ + unit: 'weeks', + defaultValue, + })({ description: 'test', char: 'w' }), + }, + }; + it('passes', async () => { + const out = await Parser.parse(['--wait=10'], buildProps); + expect(out.flags.wait.quantity).to.equal(10); + expect(out.flags.wait.unit).to.equal(Duration.Unit.WEEKS); + }); + it('passes with default', async () => { + const out = await Parser.parse([], buildProps); + expect(out.flags.wait.quantity).to.equal(33); + }); + }); + + describe('validation with all options', () => { + const min = 1; + const max = 60; + const defaultValue = 33; + const buildProps = { + flags: { + wait: buildDurationFlag({ + defaultValue, + min, + max, + unit: 'minutes', + })({ description: 'test', char: 'w' }), + }, + }; + it('passes', async () => { + const out = await Parser.parse(['--wait=10'], buildProps); + expect(out.flags.wait.quantity).to.equal(10); + }); + it('min passes', async () => { + const out = await Parser.parse([`--wait=${min}`], buildProps); + expect(out.flags.wait.quantity).to.equal(min); + }); + it('max passes', async () => { + const out = await Parser.parse([`--wait=${max}`], buildProps); + expect(out.flags.wait.quantity).to.equal(max); + }); + it('default works', async () => { + const out = await Parser.parse([], buildProps); + expect(out.flags.wait.quantity).to.equal(defaultValue); + }); + describe('failures', () => { + it('below min fails', async () => { + try { + const out = await Parser.parse([`--wait=${min - 1}`], buildProps); + + throw new Error(`Should have thrown an error ${JSON.stringify(out)}`); + } catch (err) { + const error = err as Error; + expect(error.message).to.equal(messages.getMessage('flags.duration.errors.DurationBounds', [1, 60])); + } + }); + it('above max fails', async () => { + try { + const out = await Parser.parse([`--wait=${max + 1}`], buildProps); + throw new Error(`Should have thrown an error ${JSON.stringify(out)}`); + } catch (err) { + const error = err as Error; + expect(error.message).to.equal(messages.getMessage('flags.duration.errors.DurationBounds', [1, 60])); + } + }); + it('invalid input', async () => { + try { + const out = await Parser.parse(['--wait=abc}'], buildProps); + throw new Error(`Should have thrown an error ${JSON.stringify(out)}`); + } catch (err) { + const error = err as Error; + expect(error.message).to.equal(messages.getMessage('flags.duration.errors.InvalidInput')); + } + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b553ee07..edf3a0af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -457,10 +457,10 @@ is-wsl "^2.1.1" tslib "^2.0.0" -"@oclif/core@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@oclif/core/-/core-1.2.0.tgz#f1110b1fe868e439f94f8b4ffad5dd8acf862294" - integrity sha512-h1n8NEAUzaL3+wky7W1FMeySmJWQpYX1LhWMltFY/ScvmapZzee7D9kzy/XI/ZIWWfz2ZYCTMD1wOKXO6ueynw== +"@oclif/core@^1.3.6": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.3.6.tgz#b3c3b3c865c33d88aa6e7f8f596a7939720c4d68" + integrity sha512-WSb5uyHlfTcN2HQT1miKDe90AhaiZ5Di0jmyqWlPZA0XE+xvJgMPOAyQdxxVed+XpkP6AhCPJEoIlZvQb1y1Xw== dependencies: "@oclif/linewrap" "^1.0.0" "@oclif/screen" "^3.0.2"