Skip to content

Commit

Permalink
Verify flags and configs for subcommands (#64)
Browse files Browse the repository at this point in the history
Co-authored-by: itsspriyansh <[email protected]>
  • Loading branch information
garg3133 and itsspriyansh authored Aug 18, 2024
1 parent dc71b8e commit 8e6d44d
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 28 deletions.
5 changes: 3 additions & 2 deletions src/commands/android/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import inquirer from 'inquirer';
import path from 'path';
import os from 'os';

import {AvailableOptions, AvailableSubcommands, SdkBinary} from './interfaces';
import {AvailableOptions, SdkBinary} from './interfaces';
import {AvailableSubcommands} from './subcommands/interfaces';

export const AVAILABLE_OPTIONS: AvailableOptions = {
help: {
Expand Down Expand Up @@ -34,7 +35,7 @@ export const AVAILABLE_OPTIONS: AvailableOptions = {
export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = {
connect: {
description: 'Connect to a device',
options: [
flags: [
{
name: 'wireless',
description: 'Connect a real device wirelessly'
Expand Down
10 changes: 0 additions & 10 deletions src/commands/android/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ export interface Options {
[key: string]: string | string[] | boolean;
}

export interface AvailableSubcommands {
[key: string]: {
description: string;
options: {
name: string;
description: string;
}[];
}
}

export type Platform = 'windows' | 'linux' | 'mac';

export interface OtherInfo {
Expand Down
111 changes: 110 additions & 1 deletion src/commands/android/subcommands/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import colors from 'ansi-colors';

import Logger from '../../../logger';
import {symbols} from '../../../utils';
import {SdkBinary} from '../interfaces';
import {AVAILABLE_SUBCOMMANDS} from '../constants';
import {Options, SdkBinary} from '../interfaces';
import ADB from '../utils/appium-adb';
import {CliConfig, SubcommandOptionsVerificationResult} from './interfaces';
import {showHelp} from './help';

const deviceStateWithColor = (state: string) => {
switch (state) {
Expand Down Expand Up @@ -74,3 +77,109 @@ export function showMissingBinaryHelp(binaryName: SdkBinary) {
Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android --standalone')} to setup missing requirements.`);
Logger.log(`(Remove the ${colors.gray('--standalone')} flag from the above command if setting up for testing.)\n`);
}

export function verifyOptions(subcommand: string, options: Options): SubcommandOptionsVerificationResult | false {
const optionsPassed = Object.keys(options).filter(option => options[option] !== false);

const allowedFlags = AVAILABLE_SUBCOMMANDS[subcommand].flags;
const allowedFlagNames = allowedFlags.map(flag => flag.name);

// Divide the optionsPassed array in two arrays: flagsPassed and configsPassed.
// flagsPassed contains the flags that are available for the subcommand.
// configsPassed contains the config options with string or boolean values corresponding to the flag.
const flagsPassed = optionsPassed.filter(option => allowedFlagNames.includes(option));
const configsPassed = optionsPassed.filter(option => !allowedFlagNames.includes(option));

// CHECK THE VALIDITY OF FLAG(s) PASSED

if (flagsPassed.length > 1) {
// A subcommand can only take one flag at a time.
Logger.log(`${colors.red(`Too many flags passed for '${subcommand}' subcommand:`)} ${flagsPassed.join(', ')} ${colors.gray('(only one expected)')}`);
showHelp(subcommand);

return false;
}

if (allowedFlags.length && flagsPassed.length === 0) {
// If the subcommand expects a flag but it is not passed:
// - if instead some other options are passed, throw error (we don't know if the options passed are configs and for which flag).
// - if no other options are passed, then we can prompt them for the flag and related configs.
if (configsPassed.length > 0) {
Logger.log(`${colors.red(`Unknown flag(s) passed for '${subcommand}' subcommand:`)} ${configsPassed.join(', ')}`);
showHelp(subcommand);

return false;
}

return {
subcommandFlag: '',
configs: []
};
}

// CHECK THE VALIDITY OF CONFIGS PASSED

const subcommandFlag = flagsPassed[0] || ''; // '' if no flag is allowed for the subcommand.

if (configsPassed.length === 0) {
// If no configs are passed, then we simply return and continue with the default subcommand flow.
return {
subcommandFlag,
configs: []
};
}

let allowedConfigs: CliConfig[] = [];
let configsFor = '';
if (!allowedFlags.length) {
allowedConfigs = AVAILABLE_SUBCOMMANDS[subcommand].cliConfigs || [];
configsFor = ` for '${subcommand}' subcommand`;
} else {
allowedConfigs = allowedFlags.find(flag => flag.name === subcommandFlag)?.cliConfigs || [];
configsFor = ` for '--${subcommandFlag}' flag`;
}

if (allowedConfigs.length) {
// Check if the passed configs are valid.
const configNames: string[] = allowedConfigs.map(config => config.name);

const configAliases: string[] = [];
allowedConfigs.forEach(config => configAliases.push(...config.alias));
configNames.push(...configAliases);

const unknownConfigs = configsPassed.filter(option => !configNames.includes(option));
if (unknownConfigs.length) {
Logger.log(`${colors.red(`Unknown config(s) passed${configsFor}:`)} ${unknownConfigs.join(', ')}`);
showHelp(subcommand);

return false;
}

// set main config in `options` if config aliases are passed.
const aliasToMainConfig: {[key: string]: string} = {};
allowedConfigs.forEach(config => {
config.alias.forEach(alias => {
aliasToMainConfig[alias] = config.name;
});
});

configsPassed.forEach((configName) => {
if (aliasToMainConfig[configName]) {
// `configName` is an alias
const mainConfig = aliasToMainConfig[configName];
options[mainConfig] = options[configName];
}
});
} else {
// if no configs are allowed for the flag but still some options are passed, then throw error.
Logger.log(`${colors.red(`Unknown config(s) passed${configsFor}:`)} ${configsPassed.join(', ')} ${colors.gray('(none expected)')}`);
showHelp(subcommand);

return false;
}

return {
subcommandFlag,
configs: configsPassed
};
}
14 changes: 12 additions & 2 deletions src/commands/android/subcommands/connect/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {Options, Platform} from '../../interfaces';
import {showConnectedRealDevices} from '../common';
import {verifyOptions, showConnectedRealDevices} from '../common';
import {connectWirelessAdb} from './wireless';

export async function connect(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
if (options.wireless) {
const verifyResult = verifyOptions('connect', options);
if (!verifyResult) {
return false;
}

const subcommandFlag = verifyResult.subcommandFlag;
if (subcommandFlag === '') {
// flag not passed by the user -- prompt user for the flag
}

if (subcommandFlag === 'wireless') {
await showConnectedRealDevices();

return await connectWirelessAdb(sdkRoot, platform);
Expand Down
34 changes: 34 additions & 0 deletions src/commands/android/subcommands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import colors from 'ansi-colors';

import Logger from '../../../logger';
import {AVAILABLE_SUBCOMMANDS} from '../constants';
import {Subcommand} from './interfaces';

export function showHelp(subcommand: string) {
const subcmd = AVAILABLE_SUBCOMMANDS[subcommand];

const subcmdFlagUsage = subcmd.flags?.length ? ' [flag]' : '';
Logger.log(`Usage: ${colors.cyan(`npx @nightwatch/mobile-helper android ${subcommand}${subcmdFlagUsage} [configs]`)}\n`);

const subcmdFlagsHelp = getSubcommandFlagsHelp(subcmd);
if (subcmdFlagsHelp) {
Logger.log(colors.yellow('Available flags:'));
Logger.log(subcmdFlagsHelp);
}
}

export const getSubcommandFlagsHelp = (subcmd: Subcommand) => {
let output = '';
const longest = (xs: string[]) => Math.max.apply(null, xs.map(x => x.length));

if (subcmd.flags && subcmd.flags.length > 0) {
const optionLongest = longest(subcmd.flags.map(flag => `--${flag.name}`));
subcmd.flags.forEach(flag => {
const flagStr = `--${flag.name}`;
const optionPadding = new Array(Math.max(optionLongest - flagStr.length + 3, 0)).join('.');
output += ` ${flagStr} ${colors.grey(optionPadding)} ${colors.gray(flag.description)}\n`;
});
}

return output;
};
6 changes: 6 additions & 0 deletions src/commands/android/subcommands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Logger from '../../../logger';
import {getPlatformName} from '../../../utils';
import {Options, Platform} from '../interfaces';
import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common';
import {showHelp} from './help';
import {connect} from './connect';
import {install} from './install';

Expand All @@ -27,6 +28,11 @@ export class AndroidSubcommand {
}

async run(): Promise<boolean> {
if (this.options.help) {
showHelp(this.subcommand);

return true;
}
this.loadEnvFromDotEnv();

const javaInstalled = checkJavaInstallation(this.rootDir);
Expand Down
26 changes: 26 additions & 0 deletions src/commands/android/subcommands/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// configs for subcommand options
export interface CliConfig {
name: string,
alias: string[],
description: string,
usageHelp: string,
}

export interface Subcommand {
description: string;
cliConfigs?: CliConfig[];
flags: {
name: string;
description: string;
cliConfigs?: CliConfig[];
}[];
}

export interface AvailableSubcommands {
[key: string]: Subcommand;
}

export interface SubcommandOptionsVerificationResult {
subcommandFlag: string;
configs: string[];
}
21 changes: 8 additions & 13 deletions src/commands/android/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import path from 'path';
import untildify from 'untildify';
import which from 'which';

import Logger from '../../../logger';
import {symbols} from '../../../utils';
import {ABI, AVAILABLE_OPTIONS, AVAILABLE_SUBCOMMANDS, DEFAULT_CHROME_VERSIONS, DEFAULT_FIREFOX_VERSION, SDK_BINARY_LOCATIONS} from '../constants';
import {
ABI, AVAILABLE_OPTIONS, AVAILABLE_SUBCOMMANDS,
DEFAULT_CHROME_VERSIONS, DEFAULT_FIREFOX_VERSION, SDK_BINARY_LOCATIONS
} from '../constants';
import {Platform, SdkBinary} from '../interfaces';
import Logger from '../../../logger';
import {getSubcommandFlagsHelp} from '../subcommands/help';

export const getAllAvailableOptions = () => {
const mainOptions = Object.keys(AVAILABLE_OPTIONS);
Expand Down Expand Up @@ -216,23 +220,14 @@ export const getSubcommandHelp = (): string => {
output += ' The following subcommands are used for different operations on Android SDK:\n\n';
output += `${colors.yellow('Subcommands and Subcommand-Options:')}\n`;

const longest = (xs: string[]) => Math.max.apply(null, xs.map(x => x.length));

Object.keys(AVAILABLE_SUBCOMMANDS).forEach(subcommand => {
const subcmd = AVAILABLE_SUBCOMMANDS[subcommand];
const subcmdOptions = subcmd.options?.map(option => `[--${option.name}]`).join(' ') || '';
const subcmdOptions = subcmd.flags?.map(flag => `[--${flag.name}]`).join(' ') || '';

output += ` ${colors.cyan(subcommand)} ${subcmdOptions}\n`;
output += ` ${colors.gray(subcmd.description)}\n`;

if (subcmd.options && subcmd.options.length > 0) {
const optionLongest = longest(subcmd.options.map(option => `--${option.name}`));
subcmd.options.forEach(option => {
const optionStr = `--${option.name}`;
const optionPadding = new Array(Math.max(optionLongest - optionStr.length + 3, 0)).join('.');
output += ` ${optionStr} ${colors.grey(optionPadding)} ${colors.gray(option.description)}\n`;
});
}
output += getSubcommandFlagsHelp(subcmd);
});

return output;
Expand Down

0 comments on commit 8e6d44d

Please sign in to comment.