Refactor command configuration so that we can have a central place for configuring validation, options, telemetry, etc.
Update commands:
OK, to make things more tangible, what if we had something like this:
// base Command class that contains logic shared by all commands
abstract class Command {
// arrays that hold delegates for configuring the command
public options: CommandOption[] = [];
public validations: ((args: CommandArgs) => boolean | string)[] = [];
public telemetry: ((args: CommandArgs) => void)[] = [];
protected telemetryProperties: any = {};
// we configure the command directly in the constructor so that we don't
// need to do any extra calls anywhere else
constructor() {
// we break down configuration into separate functions for readability
this.#initOptions();
this.#initValidations();
this.#initTelemetry();
// these functions must be defined with # so that they're truly private
// otherwise you'll get a ts2415 error (Types have separate declarations of a private property 'x'.)
// another way to avoid it is to init everything directly in the constructor
// without breaking it down into separate functions
// `private` in TS is a design-time flag and private members end-up being
// regular class properties that would collide on runtime, which is why we need the extra `#`
}
#initTelemetry(): void {
// rather than configuring the telemetryProperties object directly,
// we return a function that we'll run later on, because when the command
// is instantiated, args haven't been parsed and validated yet
this.telemetry.push(
(args) => {
this.telemetryProperties.query = args.options.query;
this.telemetryProperties.output = args.options.output || 'json';
}
);
}
#initOptions(): void {
this.options.push(
{ option: '--query [query]' },
{
option: '-o, --output [output]',
autocomplete: ['csv', 'json', 'text']
},
{ option: '--verbose' },
{ option: '--debug' }
);
}
#initValidations(): void {
this.validations.push(
// common validation logic that we move from the CLI runtime to the Command
(args) => this.validateRequiredOptions(args),
(args) => this.validateOptionSets(args),
// command-specific validation function; it's up to us to decide if
// each validation is a separate function or if we want to move them
// as-is from the current `validate` method in each command
(args) => {
if (args.options.output) {
const outputOption = this.options.find(o => o.option.indexOf('--output') > -1);
if (outputOption!.autocomplete!.indexOf(args.options.output) < 0) {
return `${args.options.output} is not a valid output. Allowed values are ${outputOption!.autocomplete!.join(', ')}`;
}
}
return true;
}
);
}
// validation logic from the CLI runtime that we move to the Command
private validateRequiredOptions(args: CommandArgs): boolean | string {
}
private validateOptionSets(args: CommandArgs): boolean | string {
}
// called by the CLI runtime to validate command's args
public validate(args: CommandArgs): boolean | string {
for (const validation of this.validations) {
const result = validation(args);
if (typeof result === 'string') {
return result;
}
}
return true;
}
}
// sample command
class SpoSiteGetCommand extends Command {
constructor() {
// we call super to include config from the base command class
super();
this.#initOptions();
this.#initValidations();
this.#initTelemetry();
}
#initTelemetry(): void {
this.telemetry.push(
(args) => {
this.telemetryProperties.url = typeof args.options.url !== 'undefined';
this.telemetryProperties.id = typeof args.options.id !== 'undefined';
}
);
}
#initOptions(): void {
this.options.push(
{ option: '--url [url]' },
{ option: '--id [id]' }
);
}
#initValidations(): void {
this.validations.push(
(args) => {
if (args.options.url) {
return validation.isValidSharePointUrl(args.options.url);
}
return true;
}
);
}
}
I haven't included everything in the example above (types, option sets, aliases, default properties, processing options), but I hope the example gives a clearer idea of the possible direction we could take. Features missing from this example would be basically 'more of the same' (array with functions that the runtime would execute).
Originally posted by @waldekmastykarz in #3218 (comment)
Refactor command configuration so that we can have a central place for configuring validation, options, telemetry, etc.
Update commands:
OK, to make things more tangible, what if we had something like this:
I haven't included everything in the example above (types, option sets, aliases, default properties, processing options), but I hope the example gives a clearer idea of the possible direction we could take. Features missing from this example would be basically 'more of the same' (array with functions that the runtime would execute).
Originally posted by @waldekmastykarz in #3218 (comment)