appcenter-cli
Version:
Command line tool for Visual Studio App Center
208 lines (175 loc) • 8.29 kB
text/typescript
import { OptionsDescription, OptionDescription, PositionalOptionsDescription, PositionalOptionDescription } from "./option-parser";
import { inspect } from "util";
import { assign } from "lodash";
const debug = require("debug")("appcenter-cli:util:commandline:option-decorators");
const optionDescriptionKey = Symbol("OptionParameters");
const positionalDescriptionKey = Symbol("PositionalParameters");
const unknownRequiredsKey = Symbol("UnknownRequireds");
const unknownDefaultValueKey = Symbol("UnknownDefaultValues");
const unknownHelpTextKey = Symbol("UnknownDescription");
export const classHelpTextKey = Symbol("ClassHelpText");
export function getOptionsDescription(target: any): OptionsDescription {
// option description can be overridden in children class
function getRecursive(accumulator: OptionsDescription, target: any): OptionsDescription {
if (target) {
getRecursive(accumulator, Object.getPrototypeOf(target));
} else {
return accumulator;
}
if (target.hasOwnProperty(optionDescriptionKey)) {
assign(accumulator, target[optionDescriptionKey]);
}
return accumulator;
}
return getRecursive({}, target);
}
function getLocalOptionsDescription(target: any): OptionsDescription {
if (!target.hasOwnProperty(optionDescriptionKey)) {
target[optionDescriptionKey] = {};
}
return target[optionDescriptionKey];
}
export function getPositionalOptionsDescription(target: any): PositionalOptionsDescription {
function getRecursive(accumulator: PositionalOptionsDescription, target: any): PositionalOptionsDescription {
if (!target || !target.hasOwnProperty(positionalDescriptionKey)) {
return accumulator;
}
let newOpts: any = [];
if (target.hasOwnProperty(positionalDescriptionKey)) {
newOpts = target[positionalDescriptionKey];
}
return getRecursive(newOpts.concat(accumulator), Object.getPrototypeOf(target));
}
return getRecursive([], target);
}
function getLocalPositionalOptionsDescription(target: any): PositionalOptionsDescription {
if (!target.hasOwnProperty(positionalDescriptionKey)) {
target[positionalDescriptionKey] = [];
}
return target[positionalDescriptionKey];
}
export function getClassHelpText(target: any): string {
return target[classHelpTextKey];
}
interface PropertyDecorator {
(proto: any, propertyKey: string | Symbol): void;
}
interface PropertyDecoratorBuilder<T> {
(input: T): PropertyDecorator;
}
function updateUnknownRequireds(option: OptionDescription | PositionalOptionDescription, propertyKey: string, proto: any) {
if (proto[unknownRequiredsKey] && proto[unknownRequiredsKey].has(propertyKey)) {
option.required = true;
proto[unknownRequiredsKey].delete(propertyKey);
}
}
function updateUnknownDefaultValues(option: OptionDescription | PositionalOptionDescription, propertyKey: string, proto: any) {
if (proto[unknownDefaultValueKey] && proto[unknownDefaultValueKey].has(propertyKey)) {
option.defaultValue = proto[unknownDefaultValueKey].get(propertyKey);
proto[unknownDefaultValueKey].delete(propertyKey);
}
}
function updateUnknownHelpTexts(option: OptionDescription | PositionalOptionDescription, propertyKey: string, proto: any) {
if (proto[unknownHelpTextKey] && proto[unknownHelpTextKey].has(propertyKey)) {
option.helpText = proto[unknownHelpTextKey].get(propertyKey);
proto[unknownHelpTextKey].delete(propertyKey);
}
}
function updateUnknowns(option: OptionDescription | PositionalOptionDescription, propertyKey: string, proto: any) {
updateUnknownRequireds(option, propertyKey, proto);
updateUnknownDefaultValues(option, propertyKey, proto);
updateUnknownHelpTexts(option, propertyKey, proto);
}
function makeStringDecorator(descriptionFieldName: string): PropertyDecoratorBuilder<string> {
return function decoratorBuilder(name: string): PropertyDecorator {
return function paramDecorator(proto: any, propertyKey: string): void {
const optionsDescription = getLocalOptionsDescription(proto);
const option = optionsDescription[propertyKey] || {};
(option as any)[descriptionFieldName] = name;
updateUnknowns(option, propertyKey, proto);
optionsDescription[propertyKey] = option;
};
};
}
function makeBoolDecorator(descriptionFieldName: string): PropertyDecorator {
return function paramDecorator(proto: any, propertyKey: string): void {
const optionsDescription = getLocalOptionsDescription(proto);
const option = optionsDescription[propertyKey] || {};
(option as any)[descriptionFieldName] = true;
updateUnknowns(option, propertyKey, proto);
optionsDescription[propertyKey] = option;
};
}
// Short and long name decorators
export const shortName = makeStringDecorator("shortName");
export const longName = makeStringDecorator("longName");
export const hasArg = makeBoolDecorator("hasArg");
export const common = makeBoolDecorator("common");
function makePositionalDecorator<T>(descriptionFieldName: string): PropertyDecoratorBuilder<T> {
return function positionalDecoratorBuilder(value: T): PropertyDecorator {
return function positionalDecorator(proto: any, propertyKey: string): void {
const optionsDescription = getLocalPositionalOptionsDescription(proto);
let option = optionsDescription.find((opt) => opt.propertyName === propertyKey);
if (option === undefined) {
option = { propertyName: propertyKey, name: "", position: -1 };
optionsDescription.push(option);
updateUnknowns(option, propertyKey, proto);
}
(option as any)[descriptionFieldName] = value;
};
};
}
export const position = makePositionalDecorator<number>("position");
export const name = makePositionalDecorator<string>("name");
//
// Logic or handling decorators that apply to both positional and
// flag arguments. Needs to be slightly special since we may not
// know which one the parameter is until a later decorator runs.
//
function saveDecoratedValue(proto: any, propertyKey: string | Symbol, descriptionProperty: string, value: any, unknownFieldKey: Symbol) {
const flagOpts: any = getLocalOptionsDescription(proto);
if (flagOpts.hasOwnProperty(propertyKey.toString())) {
flagOpts[propertyKey.toString()][descriptionProperty] = value;
return;
}
const positionalOpts: any[] = getLocalPositionalOptionsDescription(proto);
const opt = positionalOpts.find((opt) => opt.propertyName === propertyKey);
if (opt !== undefined) {
opt[descriptionProperty] = value;
return;
}
const unknownValues = proto[unknownFieldKey as any] = proto[unknownFieldKey as any] || new Map<string, string>();
unknownValues.set(propertyKey.toString(), value);
}
// Required is special, since it has to work on both flag and positional parameters.
// If it's the first decorator, stick name in a set to check later once we know
// which one it is
export function required(proto: any, propertyKey: string): void {
saveDecoratedValue(proto, propertyKey, "required", true, unknownRequiredsKey);
}
// DefaultValue is also special, since it has to work with both as well. Same
// basic logic
export function defaultValue(value: string | string[]): PropertyDecorator {
return function defaultValueDecorator(proto: any, propertyKey: string): void {
saveDecoratedValue(proto, propertyKey, "defaultValue", value, unknownDefaultValueKey);
};
}
// Decorator factory to give a consolidated helptext API across class & parameter
export function help(helpText: string) : {(...args: any[]): any} {
return function helpDecoratorFactory(...args: any[]): any {
debug(`@help decorator called with ${args.length} arguments: ${inspect(args)}`);
if (args.length === 1) {
const ctor = args[0];
ctor[classHelpTextKey] = helpText;
return ctor;
}
// Typescript docs are incorrect - property decorators get three args, and the last one is
// undefined
if (args.length === 3 && typeof args[0] === "object" && args[2] === undefined) {
const proto = args[0];
const propertyName = args[1] as string;
return saveDecoratedValue(proto, propertyName, "helpText", helpText, unknownHelpTextKey);
}
throw new Error("@help not valid in this location");
};
}