Skip to content

Commit

Permalink
Merge pull request #360 from sparksuite/allow-accepts-to-be-callback
Browse files Browse the repository at this point in the history
Allow accepts to be callback
  • Loading branch information
WesCossick authored May 21, 2021
2 parents 2ff9b6f + f3286cc commit 766dc38
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "waterfall-cli",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"description": "Effortlessly create CLIs powered by Node.js",
"types": "dist/cjs/index.d.ts",
"files": [
Expand Down
30 changes: 30 additions & 0 deletions src/screens/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,34 @@ describe('#helpScreen()', () => {
'What you want to list (required) (accepts: toppings, crusts, two words)'
);
});

it('Emits help with dynamic async accepts properties', async () => {
process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-async-accepts',
'--help',
];

const result = removeFormatting(await helpScreen());

expect(result).toContain('What type of pizza to order (accepts: a1, b1, c1, d1)');
expect(result).toContain('Just used for testing (accepts: a2, b2, c2, d2)');
});

it('Emits help with dynamic sync accepts properties', async () => {
process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-sync-accepts',
'--help',
];

const result = removeFormatting(await helpScreen());

expect(result).toContain('What type of pizza to order (accepts: a1, b1, c1, d1)');
expect(result).toContain('Just used for testing (accepts: a2, b2, c2, d2)');
});
});
11 changes: 7 additions & 4 deletions src/screens/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default async function helpScreen(): Promise<string> {
table.push(['\nOPTIONS:']);

// List options
Object.entries(mergedSpec.options ?? {}).forEach(([option, details]) => {
for (const [option, details] of Object.entries(mergedSpec.options ?? {})) {
// Form full description
let fullDescription = '';

Expand All @@ -132,12 +132,15 @@ export default async function helpScreen(): Promise<string> {
}

if (details.accepts) {
fullDescription += chalk.gray.italic(` (accepts: ${details.accepts.join(', ')})`);
const arrayOrPromise = typeof details.accepts === 'function' ? details.accepts() : details.accepts;
const accepts = arrayOrPromise instanceof Promise ? await arrayOrPromise : arrayOrPromise;

fullDescription += chalk.gray.italic(` (accepts: ${accepts.join(', ')})`);
}

// Add to table
table.push([` --${option}${details.shorthand ? `, -${details.shorthand}` : ''}`, fullDescription]);
});
}
}

// Add line for commands
Expand Down Expand Up @@ -177,7 +180,7 @@ export default async function helpScreen(): Promise<string> {
fullDescription += chalk.gray.italic(` (${mergedSpec.data.type})`);
}

if (mergedSpec.data.accepts) {
if (mergedSpec.data.accepts && typeof mergedSpec.data.accepts !== 'function') {
fullDescription += chalk.gray.italic(` (accepts: ${mergedSpec.data.accepts.join(', ')})`);
}

Expand Down
2 changes: 2 additions & 0 deletions src/utils/get-all-program-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ describe('#getAllProgramCommands()', () => {
'order',
'order descriptionless-data',
'order dine-in',
'order dynamic-async-accepts',
'order dynamic-sync-accepts',
'order float-data',
'order integer-data',
'order to-go',
Expand Down
36 changes: 27 additions & 9 deletions src/utils/get-command-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ export type CommandSpec<Input extends CommandInput = EmptyCommandInput> = OmitEx
: ExcludeMe
: ExcludeMe;

/** A finite array of acceptable option values. Invalid values will be rejected. */
accepts: NonNullable<Input['options'][Option]> extends Array<string | number>
? Input['options'][Option]
/** A finite array of acceptable option values or callback providing same. Invalid values will be rejected. */
accepts: NonNullable<Input['options'][Option]> extends
| string[]
| number[]
| (() => string[] | number[])
| (() => Promise<string[] | number[]>)
? Input['options'][Option] | (() => Promise<Input['options'][Option]>)
: ExcludeMe;
}>;
}
Expand All @@ -103,8 +107,14 @@ export type CommandSpec<Input extends CommandInput = EmptyCommandInput> = OmitEx
: ExcludeMe
: ExcludeMe;

/** A finite array of acceptable data values. Invalid data will be rejected. */
accepts: NonNullable<Input['data']> extends Array<string | number> ? Input['data'] : ExcludeMe;
/** A finite array of acceptable data values or callback providing same. Invalid data will be rejected. */
accepts: NonNullable<Input['data']> extends
| string[]
| number[]
| (() => string[] | number[])
| (() => Promise<string[] | number[]>)
? Input['data'] | (() => Promise<Input['data']>)
: ExcludeMe;

/** Whether to ignore anything that looks like flags/options once data is reached. Useful if you expect your data to contain things that would otherwise appear to be flags/options. */
ignoreFlagsAndOptions?: true;
Expand All @@ -131,14 +141,14 @@ export interface GenericCommandSpec {
shorthand?: string;
required?: true;
type?: 'integer' | 'float';
accepts?: string[] | number[];
accepts?: string[] | number[] | (() => string[] | number[]) | (() => Promise<string[] | number[]>);
};
};
data?: {
description?: string;
required?: true;
type?: 'integer' | 'float';
accepts?: string[] | number[];
accepts?: string[] | number[] | (() => string[] | number[]) | (() => Promise<string[] | number[]>);
ignoreFlagsAndOptions?: true;
};
}
Expand Down Expand Up @@ -180,8 +190,16 @@ export default async function getCommandSpec(directory: string): Promise<Generic

// Return
try {
const spec = (await import(specFilePath)) as { default: GenericCommandSpec } | GenericCommandSpec;
return 'default' in spec ? spec.default : spec;
let spec = (await import(specFilePath)) as { default: GenericCommandSpec } | GenericCommandSpec;

spec = 'default' in spec ? spec.default : spec;

if (spec.data?.accepts && typeof spec.data.accepts === 'function') {
const arrayOrPromise = spec.data.accepts();
spec.data.accepts = arrayOrPromise instanceof Promise ? await arrayOrPromise : arrayOrPromise;
}

return spec;
} catch (error) {
throw new PrintableError(
`${String(error)}\n\nEncountered this error while importing the spec file at: ${chalk.bold(truncatedPath)}`
Expand Down
68 changes: 68 additions & 0 deletions src/utils/get-organized-arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,72 @@ describe('#getOrganizedArguments()', () => {
values: [],
});
});

it('Handles having dynamic async option accept values', async () => {
(process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-async-accepts',
'--test',
'a2',
]),
expect(await getOrganizedArguments()).toStrictEqual({
command: 'order dynamic-async-accepts',
flags: [],
options: ['test'],
values: ['a2'],
});
});

it('Handles having dynamic sync option accept values', async () => {
(process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-sync-accepts',
'--test',
'a2',
]),
expect(await getOrganizedArguments()).toStrictEqual({
command: 'order dynamic-sync-accepts',
flags: [],
options: ['test'],
values: ['a2'],
});
});

it('Handles having dynamic async data accept values', async () => {
(process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-async-accepts',
'b1',
]),
expect(await getOrganizedArguments()).toStrictEqual({
command: 'order dynamic-async-accepts',
flags: [],
options: [],
values: [],
data: 'b1',
});
});

it('Handles having dynamic sync data accept values', async () => {
(process.argv = [
'/path/to/node',
path.join(testProjectsPath, 'pizza-ordering', 'cli', 'entry.js'),
'order',
'dynamic-sync-accepts',
'b1',
]),
expect(await getOrganizedArguments()).toStrictEqual({
command: 'order dynamic-sync-accepts',
flags: [],
options: [],
values: [],
data: 'b1',
});
});
});
8 changes: 6 additions & 2 deletions src/utils/get-organized-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ export default async function getOrganizedArguments(): Promise<OrganizedArgument
// Store details
previousOption = argument;
nextIsOptionValue = true;
nextValueAccepts = details.accepts || undefined;

const arrayOrPromise =
typeof details.accepts === 'function' ? details.accepts() : details.accepts || undefined;
nextValueAccepts = arrayOrPromise instanceof Promise ? await arrayOrPromise : arrayOrPromise;

nextValueType = details.type || undefined;
organizedArguments.options.push(option);
}
Expand Down Expand Up @@ -282,7 +286,7 @@ export default async function getOrganizedArguments(): Promise<OrganizedArgument
}

// Validate data, if necessary
if (mergedSpec.data.accepts) {
if (mergedSpec.data.accepts && mergedSpec.data.accepts instanceof Array) {
// @ts-expect-error: TypeScript is confused here...
if (!mergedSpec.data.accepts.includes(organizedArguments.data)) {
throw new PrintableError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = async function() {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
async function dataAllows() {
await new Promise((resolve) => setTimeout(resolve, 10));
return ['a1', 'b1', 'c1', 'd1'];
}

async function testAllows() {
await new Promise((resolve) => setTimeout(resolve, 10));
return ['a2', 'b2', 'c2', 'd2'];
}

module.exports = {
description: 'Just used for testing',
data: {
description: 'What type of pizza to order',
accepts: dataAllows,
},
options: {
test: {
description: 'Just used for testing',
accepts: testAllows,
},
},
acceptsPassThroughArgs: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = async function() {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function dataAllows() {
return ['a1', 'b1', 'c1', 'd1'];
}

function testAllows() {
return ['a2', 'b2', 'c2', 'd2'];
}


module.exports = {
description: 'Just used for testing',
data: {
description: 'What type of pizza to order',
accepts: dataAllows,
},
options: {
test: {
description: 'Just used for testing',
accepts: testAllows,
},
},
acceptsPassThroughArgs: false,
};

0 comments on commit 766dc38

Please sign in to comment.