Skip to content

Commit

Permalink
feat: Implement custom validation for options. (#32)
Browse files Browse the repository at this point in the history
* feat: Implement custom validation for options.

* Add documentation.

Co-authored-by: Golo Roden <golo.roden@thenativeweb.io>
  • Loading branch information
Hannes Leutloff and goloroden authored Feb 10, 2020
1 parent 4afa2b2 commit 9ca8651
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 7 deletions.
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,35 @@ If an option is mandatory, set the `isRequired` property to `true`. For optional

If you want to give a dedicated name to the value, you can set it using the `parameterName` property. This sometimes makes sense, to e.g. show off that a parameter is not just a `string`, but a `url` or another domain-specific concept.

Last but not least, you can define whether an option is the default option of a command by setting the `defaultOption` property to `true`. In this case you can skip the option's flag, and just provide its value.
You can also define whether an option is the default option of a command by setting the `defaultOption` property to `true`. In this case you can skip the option's flag, and just provide its value.

Last but not least, you may specify a `validate` function for an option definition. Inside this function you are free to do whatever you need to do to ensure that the option's given value is valid. However, if you throw an exception from within this function, command-line-interface aborts the command's execution, and shows an error message:

```javascript
const hello = {
name: 'hello',
description: 'Say hello on the command line.',

optionDefinitions: [
{
name: 'name',
description: 'The name to use.',
type: 'string',
alias: 'n',
defaultValue: 'Jane',
validate (value) {
if (value.length > 20) {
throw new Error('The name must be less than 20 characters.');
}
}
}
],

handle ({ options }) {
console.log(`Hello ${options.name}!`);
}
};
```

### Implementing sub-commands

Expand Down Expand Up @@ -210,7 +238,7 @@ await runCli({
// ...
},

optionInvalid ({ optionDefinition }) {
optionInvalid ({ optionDefinition, reason }) {
// ...
},

Expand Down
3 changes: 2 additions & 1 deletion lib/Handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export interface Handlers {
ancestors: string[];
}) => void;

optionInvalid: ({ optionDefinition }: {
optionInvalid: ({ optionDefinition, reason }: {
optionDefinition: OptionDefinition;
reason: string;
}) => void;

optionMissing: ({ optionDefinition }: {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/OptionDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface OptionDefinition {
defaultValue?: any;
parameterName?: string;
isRequired?: boolean;
validate?: (value: any) => void;
}
4 changes: 2 additions & 2 deletions lib/runCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ const runCli = async function ({ rootCommand, argv, handlers = {}}: {
// eslint-disable-next-line no-console
console.error(`Unknown command '${unknownCommandName}'. Did you mean '${recommendedCommandName}'?`);
},
optionInvalid ({ optionDefinition }): void {
optionInvalid ({ reason }): void {
// eslint-disable-next-line no-console
console.error(`Option '${optionDefinition.name}' must be a ${optionDefinition.type}.`);
console.error(reason);
},
optionMissing ({ optionDefinition }): void {
// eslint-disable-next-line no-console
Expand Down
2 changes: 1 addition & 1 deletion lib/runCliRecursive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const runCliRecursive = async function ({
// eslint-disable-next-line unicorn/no-process-exit
return process.exit(1);
case 'EOPTIONINVALID': {
handlers.optionInvalid({ optionDefinition: ex.data.optionDefinition });
handlers.optionInvalid({ optionDefinition: ex.data.optionDefinition, reason: ex.message });

// eslint-disable-next-line unicorn/no-process-exit
return process.exit(1);
Expand Down
10 changes: 10 additions & 0 deletions lib/validateOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ const validateOptions = function ({ options, optionDefinitions }: {
throw new errors.InvalidOperation();
}
}

if (optionDefinition.validate) {
try {
optionDefinition.validate(value);
} catch (ex) {
throw new errors.OptionInvalid(ex.message, {
data: { optionDefinition }
});
}
}
}
};

Expand Down
12 changes: 12 additions & 0 deletions test/integration/runCliTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,18 @@ suite('Cli', (): void => {
assert.that(stderr).is.containing(`Die Option 'number' muss eine Zahl sein.`);
assert.that((process.exit as unknown as SinonStub).calledWith(1)).is.true();
});

test('displays validator exception message.', async (): Promise<void> => {
const command: string[] = [ 'number', '--number', '601' ];

await runCli({ rootCommand: extendedVariousCli, argv: command });

const { stderr, stdout } = stop();

assert.that(stdout).is.equalTo('');
assert.that(stderr).is.containing(`Number must not be bigger than 500.`);
assert.that((process.exit as unknown as SinonStub).calledWith(1)).is.true();
});
});

suite('various.required command', (): void => {
Expand Down
8 changes: 7 additions & 1 deletion test/shared/examples/various/commands/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ const number: Command<NumberOptions> = {
name: 'number',
type: 'number',
alias: 'n',
isRequired: true
description: 'Must not be bigger than 500.',
isRequired: true,
validate (value): void {
if (value > 500) {
throw new Error('Number must not be bigger than 500.');
}
}
}
],
handle ({ options }): void {
Expand Down

0 comments on commit 9ca8651

Please sign in to comment.