From 8e6d44dbb7d2150994096d6b8d5847c19f4208ad Mon Sep 17 00:00:00 2001 From: Priyansh Garg Date: Sun, 18 Aug 2024 17:31:26 +0530 Subject: [PATCH] Verify flags and configs for subcommands (#64) Co-authored-by: itsspriyansh --- src/commands/android/constants.ts | 5 +- src/commands/android/interfaces.ts | 10 -- src/commands/android/subcommands/common.ts | 111 +++++++++++++++++- .../android/subcommands/connect/index.ts | 14 ++- src/commands/android/subcommands/help.ts | 34 ++++++ src/commands/android/subcommands/index.ts | 6 + .../android/subcommands/interfaces.ts | 26 ++++ src/commands/android/utils/common.ts | 21 ++-- 8 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 src/commands/android/subcommands/help.ts create mode 100644 src/commands/android/subcommands/interfaces.ts diff --git a/src/commands/android/constants.ts b/src/commands/android/constants.ts index 0fe09f4..b086e61 100644 --- a/src/commands/android/constants.ts +++ b/src/commands/android/constants.ts @@ -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: { @@ -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' diff --git a/src/commands/android/interfaces.ts b/src/commands/android/interfaces.ts index 3e5223c..a64b66c 100644 --- a/src/commands/android/interfaces.ts +++ b/src/commands/android/interfaces.ts @@ -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 { diff --git a/src/commands/android/subcommands/common.ts b/src/commands/android/subcommands/common.ts index d124618..b4f26e3 100644 --- a/src/commands/android/subcommands/common.ts +++ b/src/commands/android/subcommands/common.ts @@ -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) { @@ -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 + }; +} diff --git a/src/commands/android/subcommands/connect/index.ts b/src/commands/android/subcommands/connect/index.ts index f53e206..8d6d307 100644 --- a/src/commands/android/subcommands/connect/index.ts +++ b/src/commands/android/subcommands/connect/index.ts @@ -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 { - 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); diff --git a/src/commands/android/subcommands/help.ts b/src/commands/android/subcommands/help.ts new file mode 100644 index 0000000..40048a9 --- /dev/null +++ b/src/commands/android/subcommands/help.ts @@ -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; +}; diff --git a/src/commands/android/subcommands/index.ts b/src/commands/android/subcommands/index.ts index acc2c94..a2ff1ee 100644 --- a/src/commands/android/subcommands/index.ts +++ b/src/commands/android/subcommands/index.ts @@ -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'; @@ -27,6 +28,11 @@ export class AndroidSubcommand { } async run(): Promise { + if (this.options.help) { + showHelp(this.subcommand); + + return true; + } this.loadEnvFromDotEnv(); const javaInstalled = checkJavaInstallation(this.rootDir); diff --git a/src/commands/android/subcommands/interfaces.ts b/src/commands/android/subcommands/interfaces.ts new file mode 100644 index 0000000..eae016d --- /dev/null +++ b/src/commands/android/subcommands/interfaces.ts @@ -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[]; +} diff --git a/src/commands/android/utils/common.ts b/src/commands/android/utils/common.ts index 6e4e492..ab7ea2a 100644 --- a/src/commands/android/utils/common.ts +++ b/src/commands/android/utils/common.ts @@ -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); @@ -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;