Skip to content

Commit

Permalink
refactor(cli): extract parsing and validation into separate class
Browse files Browse the repository at this point in the history
this makes the division of responsibilities more clear: the Validator parses cli args and validates their inputs against our types. The CLI is then free to accept the parsed args and simply trigger the provided commands
  • Loading branch information
tyler-dane committed Jan 5, 2025
1 parent 90d8729 commit df4731b
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 166 deletions.
178 changes: 12 additions & 166 deletions packages/scripts/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,39 @@ import { Command } from "commander";
import { runBuild } from "./commands/build";
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
import { startDeleteFlow } from "./commands/delete";
import {
Options_Cli,
Options_Cli_Build,
Options_Cli_Delete,
Schema_Options_Cli_Build,
Schema_Options_Cli_Delete,
Schema_Options_Cli_Root,
} from "./common/cli.types";
import { getPckgsTo, log } from "./common/cli.utils";
import { CliValidator } from "./cli.validator";

class CompassCli {
private program: Command;
private options: Options_Cli;
private validator: CliValidator;

constructor(args: string[]) {
this.program = this._createProgram();
this.validator = new CliValidator(this.program);
this.program.parse(args);
this.options = this._getCliOptions();
}

public async run() {
const { force, user } = this.options;
const options = this.validator.getCliOptions();
const { force, user } = options;
const cmd = this.program.args[0];

switch (true) {
case cmd === "build": {
await this._validateBuild();
await runBuild(this.options);
await this.validator.validateBuild(options);
await runBuild(options);
break;
}
case cmd === "delete": {
this._validateDelete();
this.validator.validateDelete(options);
await startDeleteFlow(user as string, force);
break;
}
default:
this._exitHelpfully("root", `${cmd as string} is not a supported cmd`);
this.validator.exitHelpfully(
"root",
`${cmd as string} is not a supported cmd`
);
}
}

Expand Down Expand Up @@ -78,156 +74,6 @@ class CompassCli {
);
return program;
}

private _exitHelpfully(cmd: "root" | "build" | "delete", msg?: string) {
msg && log.error(msg);

if (cmd === "root") {
console.log(this.program.helpInformation());
} else {
const command = this.program.commands.find(
(c) => c.name() === cmd
) as Command;
console.log(command.helpInformation());
}

process.exit(1);
}

private _getBuildOptions() {
const buildOpts: Options_Cli_Build = {};

const buildCmd = this.program.commands.find(
(cmd) => cmd.name() === "build"
);
if (buildCmd) {
const packages = this.program.args[1]?.split(",");
if (packages) {
buildOpts.packages = packages;
}

const environment = buildCmd?.opts()[
"environment"
] as Options_Cli_Build["environment"];
if (environment) {
buildOpts.environment = environment;
}

const clientId = buildCmd?.opts()[
"clientId"
] as Options_Cli_Build["clientId"];
if (clientId) {
buildOpts.clientId = clientId;
}
}
return buildOpts;
}

private _getCliOptions(): Options_Cli {
const options = this._mergeOptions();
const validOptions = this._validateOptions(options);

console.log("options", options);
console.log("validOptions:", validOptions);
return validOptions;
}

private _getDeleteOptions() {
const deleteOpts: Options_Cli_Delete = {};

const deleteCmd = this.program.commands.find(
(cmd) => cmd.name() === "delete"
);
if (deleteCmd) {
const user = deleteCmd?.opts()["user"] as Options_Cli["user"];
if (user) {
deleteOpts.user = user;
}
}

return deleteOpts;
}

private _mergeOptions = (): Options_Cli => {
const _options = this.program.opts();
let options: Options_Cli = {
..._options,
force: _options["force"] === true,
};

const buildOptions = this._getBuildOptions();
if (Object.keys(buildOptions).length > 0) {
options = {
...options,
...buildOptions,
};
}

const deleteOptions = this._getDeleteOptions();
if (Object.keys(deleteOptions).length > 0) {
options = {
...options,
...deleteOptions,
};
}

return options;
};

private async _validateBuild() {
if (!this.options.packages) {
this.options.packages = await getPckgsTo("build");
}

const unsupportedPackages = this.options.packages.filter(
(pkg) => !ALL_PACKAGES.includes(pkg)
);
if (unsupportedPackages.length > 0) {
this._exitHelpfully(
"build",
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
);
}
}

private _validateDelete() {
const { user } = this.options;
if (!user || typeof user !== "string") {
this._exitHelpfully("delete", "You must supply a user");
}
}

private _validateOptions(options: Options_Cli) {
const { data: rootData, error: rootError } =
Schema_Options_Cli_Root.safeParse(options);
if (rootError) {
this._exitHelpfully(
"root",
`Invalid CLI options: ${rootError.toString()}`
);
}

const { data: buildData, error: buildError } =
Schema_Options_Cli_Build.safeParse(options);
if (buildError) {
this._exitHelpfully(
"build",
`Invalid build options: ${buildError.toString()}`
);
}

const { data: deleteData, error: deleteError } =
Schema_Options_Cli_Delete.safeParse(options);
if (deleteError) {
this._exitHelpfully(
"delete",
`Invalid delete options: ${deleteError.toString()}`
);
}

const data: Options_Cli = { ...rootData, ...buildData, ...deleteData };
return data;
}
}

const cli = new CompassCli(process.argv);
Expand Down
168 changes: 168 additions & 0 deletions packages/scripts/src/cli.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Command } from "commander";

import { ALL_PACKAGES } from "./common/cli.constants";
import {
Options_Cli,
Options_Cli_Build,
Options_Cli_Delete,
Schema_Options_Cli_Build,
Schema_Options_Cli_Delete,
Schema_Options_Cli_Root,
} from "./common/cli.types";
import { getPckgsTo, log } from "./common/cli.utils";

export class CliValidator {
private program: Command;

constructor(program: Command) {
this.program = program;
}

public exitHelpfully(cmd: "root" | "build" | "delete", msg?: string) {
msg && log.error(msg);

if (cmd === "root") {
console.log(this.program.helpInformation());
} else {
const command = this.program.commands.find(
(c) => c.name() === cmd
) as Command;
console.log(command.helpInformation());
}

process.exit(1);
}

public getCliOptions(): Options_Cli {
const options = this._mergeOptions();
const validOptions = this._validateOptions(options);

return validOptions;
}

public async validateBuild(options: Options_Cli) {
if (!options.packages) {
options.packages = await getPckgsTo("build");
}

const unsupportedPackages = options.packages.filter(
(pkg) => !ALL_PACKAGES.includes(pkg)
);
if (unsupportedPackages.length > 0) {
this.exitHelpfully(
"build",
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
);
}
}

public validateDelete(options: Options_Cli) {
const { user } = options;
if (!user || typeof user !== "string") {
this.exitHelpfully("delete", "You must supply a user");
}
}

private _getBuildOptions() {
const buildOpts: Options_Cli_Build = {};

const buildCmd = this.program.commands.find(
(cmd) => cmd.name() === "build"
);
if (buildCmd) {
const packages = this.program.args[1]?.split(",");
if (packages) {
buildOpts.packages = packages;
}

const environment = buildCmd?.opts()[
"environment"
] as Options_Cli_Build["environment"];
if (environment) {
buildOpts.environment = environment;
}

const clientId = buildCmd?.opts()[
"clientId"
] as Options_Cli_Build["clientId"];
if (clientId) {
buildOpts.clientId = clientId;
}
}
return buildOpts;
}

private _getDeleteOptions() {
const deleteOpts: Options_Cli_Delete = {};

const deleteCmd = this.program.commands.find(
(cmd) => cmd.name() === "delete"
);
if (deleteCmd) {
const user = deleteCmd?.opts()["user"] as Options_Cli["user"];
if (user) {
deleteOpts.user = user;
}
}

return deleteOpts;
}

private _mergeOptions = (): Options_Cli => {
const _options = this.program.opts();
let options: Options_Cli = {
..._options,
force: _options["force"] === true,
};

const buildOptions = this._getBuildOptions();
if (Object.keys(buildOptions).length > 0) {
options = {
...options,
...buildOptions,
};
}

const deleteOptions = this._getDeleteOptions();
if (Object.keys(deleteOptions).length > 0) {
options = {
...options,
...deleteOptions,
};
}

return options;
};

private _validateOptions(options: Options_Cli) {
const { data: rootData, error: rootError } =
Schema_Options_Cli_Root.safeParse(options);
if (rootError) {
this.exitHelpfully(
"root",
`Invalid CLI options: ${rootError.toString()}`
);
}

const { data: buildData, error: buildError } =
Schema_Options_Cli_Build.safeParse(options);
if (buildError) {
this.exitHelpfully(
"build",
`Invalid build options: ${buildError.toString()}`
);
}

const { data: deleteData, error: deleteError } =
Schema_Options_Cli_Delete.safeParse(options);
if (deleteError) {
this.exitHelpfully(
"delete",
`Invalid delete options: ${deleteError.toString()}`
);
}

const data: Options_Cli = { ...rootData, ...buildData, ...deleteData };
return data;
}
}

0 comments on commit df4731b

Please sign in to comment.