@oclif/core
Version:
base library for oclif CLIs
639 lines (638 loc) • 27.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flag = flag;
exports.flags = flags;
exports.combinationOf = combinationOf;
/**
* Establish a constraint on a single flag.
*
* @example
* flag('foo').is.requiredAny()
*
* @param flagName The flag to constrain
*/
function flag(flagName) {
return new ConstraintImpl([flagName]);
}
/**
* Establish a constraint on multiple flags.
*
* @example
* flags('foo', 'bar').are.mutuallyExclusive()
*
* @param flagNames The flags to constrain
*/
function flags(...flagNames) {
return new ConstraintImpl(flagNames);
}
/**
* Declare a set of flags to be evaluated as one instead of separately.
*
* @example
* flag('foo').is.dependentOn(combinationOf('bar', 'baz'))
*
* @param flagNames Flags to be combined
*/
function combinationOf(...flagNames) {
return {
flags: flagNames,
type: 'all',
};
}
class ConstraintImpl {
/**
* No-op chain property allowing constraints to be more human-readable.
*
* @example
* flags('foo', 'bar').are.mutuallyExclusive()
*/
are = this;
/**
* No-op chain property allowing constraints to be more human-readable.
*
* @example
* flag('foo').is.dependentOn('bar')
*/
is = this;
constrainedFlags;
constraintApplicatorFunctionHolder = new ConstraintApplicatorFunctionHolder();
topLevelCondition;
underConstructionCondition;
constructor(constrainedFlags) {
this.constrainedFlags = constrainedFlags;
}
/**
* Chain property allowing constraint conditions to be combined with logical AND.
*
* By default, logical operators are evaluated left-to-right, but groups can be created with additional {@code when}/{@code unless} clauses.
*
* @example <caption> when someFn returns true AND someOtherFn returns true, using --foo requires --bar to be used as well.</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(someFn).and.thisIsTrue(someOtherFn)
*
* @example <caption>--foo requires --bar when (fnA returns true AND fnB returns true) OR fnC returns true</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(fnA).and.thisIsTrue(fnB).or.thisIsTrue(fnC)
*
* @example <caption>--foo requires --bar when fnA returns true AND (fnB returns true OR fnC returns true)</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(fnA).and.when.thisIsTrue(fnB).or.thisIsTrue(fnC)
*/
get and() {
if (this.topLevelCondition === undefined) {
throw new Error(`Misconfigured constraint on ${createFlagString(this.constrainedFlags)}: 'and' requires a 'when' or 'unless'.`);
}
if (this.underConstructionCondition) {
throw new Error(`Misconfigured constraint on ${createFlagString(this.constrainedFlags)}: 'and' cannot directly follow '${this.underConstructionCondition.getName()}'`);
}
this.topLevelCondition = new AndCondition(this.topLevelCondition);
this.underConstructionCondition = this.topLevelCondition;
return this;
}
/**
* Chain property allowing constraint conditions to be combined with logical OR.
*
* By default, logical operators are evaluated left-to-right, but groups can be created with additional {@code when}/{@code unless} clauses.
*
* @example <caption> when EITHER someFn OR someOtherFn return true, using --foo requires --bar to be used as well.</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(someFn).or.thisIsTrue(someOtherFn)
*
* @example <caption>--foo requires --bar when (fnA returns true AND fnB returns true) OR fnC returns true</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(fnA).and.thisIsTrue(fnB).or.thisIsTrue(fnC)
*
* @example <caption>--foo requires --bar when fnA returns true AND (fnB returns true OR fnC returns true)</caption>
* flag('foo').is.dependentOn('bar').when.thisIsTrue(fnA).and.when.thisIsTrue(fnB).or.thisIsTrue(fnC)
*
*/
get or() {
if (this.topLevelCondition === undefined) {
throw new Error(`Misconfigured constraint on ${createFlagString(this.constrainedFlags)}: 'or' requires a 'when' or 'unless'.`);
}
if (this.underConstructionCondition) {
throw new Error(`Misconfigured constraint on ${createFlagString(this.constrainedFlags)}: 'or' cannot directly follow '${this.underConstructionCondition.getName()}'`);
}
this.topLevelCondition = new OrCondition(this.topLevelCondition);
this.underConstructionCondition = this.topLevelCondition;
return this;
}
/**
* Chain property allowing constraints to be conditional upon a certain criterion NOT being met.
*
* @example
* flag('foo').is.dependentOn('bar').unless.thisIsTrue(someFn)
*/
get unless() {
const newUnless = new UnlessCondition();
if (this.topLevelCondition === undefined) {
this.topLevelCondition = newUnless;
}
// istanbul ignore else - All cases covered
if (this.underConstructionCondition === undefined) {
this.underConstructionCondition = newUnless;
}
else if (this.underConstructionCondition instanceof UnaryOpCondition) {
this.underConstructionCondition.setCondition(newUnless);
this.underConstructionCondition = newUnless;
}
else if (this.underConstructionCondition instanceof BinaryCondition) {
this.underConstructionCondition.setRight(newUnless);
this.underConstructionCondition = newUnless;
}
else {
throw new TypeError('UNKNOWN CONDITION TYPE');
}
return this;
}
/**
* Chain property allowing constraints to be conditional upon a certain criterion being met.
*
* @example
* flag('foo').is.dependentOn('bar').when.thisIsTrue(someFn)
*/
get when() {
const newWhen = new WhenCondition();
if (this.topLevelCondition === undefined) {
this.topLevelCondition = newWhen;
}
// istanbul ignore else - All cases covered
if (this.underConstructionCondition === undefined) {
this.underConstructionCondition = newWhen;
}
else if (this.underConstructionCondition instanceof UnaryOpCondition) {
this.underConstructionCondition.setCondition(newWhen);
this.underConstructionCondition = newWhen;
}
else if (this.underConstructionCondition instanceof BinaryCondition) {
this.underConstructionCondition.setRight(newWhen);
this.underConstructionCondition = newWhen;
}
else {
throw new TypeError('UNKNOWN CONDITION TYPE');
}
return this;
}
_evaluateAgainstFlags(flags) {
let conditionSatisfied = false;
try {
conditionSatisfied = this.topLevelCondition ? this.topLevelCondition.isSatisfied(flags) : true;
}
catch (error) {
return {
name: this.constrainedFlags.join(','),
reason: `Error evaluating constraint conditions on ${createFlagString(this.constrainedFlags)}: ${error.message}`,
status: 'failed',
validationFn: 'constraintCondition',
};
}
try {
const applicationResult = this.constraintApplicatorFunctionHolder.applyConstraintApplicatorFunction(flags);
return {
name: this.constrainedFlags.join(','),
reason: applicationResult,
status: conditionSatisfied && applicationResult !== '' ? 'failed' : 'success',
validationFn: this.constraintApplicatorFunctionHolder.constraintType ?? '',
};
}
catch (error) {
// istanbul ignore next
return {
name: this.constrainedFlags.join(','),
reason: `Error evaluating constraint on ${createFlagString(this.constrainedFlags)}: ${error.message}`,
status: 'failed',
validationFn: 'constraintApplication',
};
}
}
/**
* Chain method allowing constraint to be made conditional upon EVERY established criterion being true.
*
* @example <caption>If --flagA is 'someVal' AND --flagB is 'someOtherVal', then using --foo requires using --bar too</caption>
* flag('foo').is.dependentOn('bar').when.allFlagCriteriaSatisfied({
* flagA: (v) => v === 'someVal',
* flagB: (v) => v !== 'someOtherVal'
* })
*
* @param criterionTester An object whose keys are flag names and whose values are functions that accept the
* value of that flag and return a boolean.
*/
allFlagCriteriaSatisfied(criterionTester) {
// istanbul ignore else - All cases covered
if (this.underConstructionCondition === undefined) {
throw new Error(`Misconfigured constraint condition on ${createFlagString(this.constrainedFlags)}: allFlagCriteriaSatisfied must immediately follow a when/unless/and/or`);
}
else if (this.underConstructionCondition instanceof UnaryOpCondition) {
this.underConstructionCondition.setCondition(new AllFlagCriteriaSatisfiedCondition(criterionTester));
this.underConstructionCondition = undefined;
}
else if (this.underConstructionCondition instanceof BinaryCondition) {
this.underConstructionCondition.setRight(new AllFlagCriteriaSatisfiedCondition(criterionTester));
this.underConstructionCondition = undefined;
}
else {
throw new TypeError('UNKNOWN CONDITION TYPE');
}
return this;
}
/**
* Chain method allowing constraint to be made conditional upon ANY established criterion being true.
*
* @example <caption>If --flagA is 'someVal' OR --flagB is 'someOtherVal', then using --foo requires using --bar too</caption>
* flag('foo').is.dependentOn('bar').when.anyFlagCriterionSatisfied({
* flagA: (v) => v === 'someVal',
* flagB: (v) => v !== 'someOtherVal'
* })
*
* @param criterionTester An object whose keys are flag names and whose values are functions that accept the
* value of that flag and return a boolean.
*/
anyFlagCriterionSatisfied(criterionTester) {
// istanbul ignore else - All cases covered
if (this.underConstructionCondition === undefined) {
throw new Error(`Misconfigured constraint condition on ${createFlagString(this.constrainedFlags)}: anyFlagCriterionSatisfied must immediately follow a when/unless/and/or`);
}
else if (this.underConstructionCondition instanceof UnaryOpCondition) {
this.underConstructionCondition.setCondition(new AnyFlagCriterionSatisfiedCondition(criterionTester));
this.underConstructionCondition = undefined;
}
else if (this.underConstructionCondition instanceof BinaryCondition) {
this.underConstructionCondition.setRight(new AnyFlagCriterionSatisfiedCondition(criterionTester));
this.underConstructionCondition = undefined;
}
else {
throw new TypeError('UNKNOWN CONDITION TYPE');
}
return this;
}
/**
* Chain method allowing the constrained flags to require the presence of at least one of the flags specified here.
*
* @example <caption>If --foo is used, then EITHER --bar OR --baz must be used as well</caption>
* flag('foo').is.dependentOn('bar', 'baz')
*
* @example <caption>If --foo is used, then BOTH --bar AND --baz must be used as well</caption>
* flag('foo').is.dependentOn(combinationOf('bar', 'baz'))
*
* @example <caption>If --foo is used, then EITHER --bar OR the combination of --baz1 and --baz2 must be used as well</caption>
* flag('foo').is.dependentOn('bar', combinationOf('baz1', 'baz2'))
*
* @param dependencyFlagGroups
*/
dependentOn(...dependencyFlagGroups) {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('dependentOn', (flags) => {
const foundConstraintFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundConstraintFlags.length === 0) {
return '';
}
for (const dependencyFlagGroup of dependencyFlagGroups) {
if (typeof dependencyFlagGroup === 'string') {
if (dependencyFlagGroup in flags && flags[dependencyFlagGroup] !== undefined) {
return '';
}
}
else {
const foundFlagsInDependencyGroup = filterFlagsPresentInInput(dependencyFlagGroup.flags, flags);
if (foundFlagsInDependencyGroup.length === dependencyFlagGroup.flags.length) {
return '';
}
}
}
const multipleConstrainedFlags = this.constrainedFlags.length > 1;
const header = multipleConstrainedFlags
? `Flags ${createFlagString(this.constrainedFlags)} require`
: `Flag ${createFlagString(this.constrainedFlags)} requires`;
return `${header} at least one of the following${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(dependencyFlagGroups)}.`;
});
return this;
}
/**
* Chain method allowing the constrained flags to be made exclusive with the flags provided here.
*
* @example <caption>Neither --foo1 nor --foo2 can be used with --bar OR --baz</caption>
* flags('foo1', 'foo2').are.exclusiveWith('bar', 'baz')
*
* @example <caption>Neither --foo1 nor --foo2 can be used with the combination of --bar and --baz, but may be used with --bar or --baz separately</caption>
* flags('foo1', 'foo2').are.exclusiveWith(combinationOf('bar', 'baz'))
*
* @example <caption>Neither --foo1 nor --foo2 can be used with --bar, or with the combination of --baz1 and --baz2</caption>
* flags('foo1', 'foo2').are.exclusiveWith('bar', combinationOf('baz1', 'baz2'))
*
* @param exclusionFlagGroups
*/
exclusiveWith(...exclusionFlagGroups) {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('exclusiveWith', (flags) => {
const foundConstraintFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundConstraintFlags.length === 0) {
return '';
}
let exclusionGroupFound = false;
for (const exclusionFlagGroup of exclusionFlagGroups) {
if (typeof exclusionFlagGroup === 'string') {
if (exclusionFlagGroup in flags && flags[exclusionFlagGroup] !== undefined) {
exclusionGroupFound = true;
break;
}
}
else {
const foundFlagsInExclusionGroup = filterFlagsPresentInInput(exclusionFlagGroup.flags, flags);
if (foundFlagsInExclusionGroup.length === exclusionFlagGroup.flags.length) {
exclusionGroupFound = true;
break;
}
}
}
if (!exclusionGroupFound) {
return '';
}
const multipleConstrainedFlags = this.constrainedFlags.length > 1;
const header = multipleConstrainedFlags ? 'Flags' : 'Flag';
return `${header} ${createFlagString(this.constrainedFlags)} cannot be used with any of the following${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(exclusionFlagGroups)}.`;
});
return this;
}
/**
* Establish a group of flags as mutually dependent, meaning that they must either be used together or not at all.
*
* @example <caption>--foo cannot be used without --bar, and vice versa</caption>
* flags('foo', 'bar').are.mutuallyDependent()
*/
mutuallyDependent() {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('mutuallyDependent', (flags) => {
const foundFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundFlags.length === 0 || foundFlags.length === this.constrainedFlags.length) {
return '';
}
return `The following flags are mutually dependent${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(this.constrainedFlags)}. Found only ${createFlagString(foundFlags)}.`;
});
return this;
}
/**
* Establish a group of flags as mutually exclusive, meaning that at most one of them can be used simultaneously.
*
* @example <caption>--foo and --bar cannot both be used at the same time</caption>
* flags('foo', 'bar').are.mutuallyExclusive()
*/
mutuallyExclusive() {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('mutuallyExclusive', (flags) => {
const foundFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundFlags.length <= 1) {
return '';
}
return `The following flags are mutually exclusive${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(this.constrainedFlags)}. Found: ${createFlagString(foundFlags)}.`;
});
return this;
}
/**
* Establish a group of flags as being collectively required.
*
* @example <caption>--foo and --bar are both always required</caption>
* flags('foo', 'bar').are.requiredAll()
*/
requiredAll() {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('requiredAll', (flags) => {
const foundFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundFlags.length === this.constrainedFlags.length) {
return '';
}
const requirement = this.constrainedFlags.length > 1 ? 'These flags are required' : 'This flag is required';
const findings = foundFlags.length > 0 ? `Found only: ${createFlagString(foundFlags)}.` : 'Found none.';
return `${requirement}${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(this.constrainedFlags)}. ${findings}`;
});
return this;
}
/**
* Establish that at least one of the constrained flags must always be used.
*
* @example <caption>Must use at least one of --foo, --bar, or --baz</caption>
* flags('foo', 'bar', 'baz').are.requiredAny()
*/
requiredAny() {
this.constraintApplicatorFunctionHolder.setConstraintApplicator('requiredAny', (flags) => {
const foundFlags = filterFlagsPresentInInput(this.constrainedFlags, flags);
if (foundFlags.length > 0) {
return '';
}
return `Must provide at least one of these flags${this.topLevelCondition ? ' under current circumstances:' : ':'} ${createFlagString(this.constrainedFlags)}.`;
});
return this;
}
/**
* Establish that at least N of the specified flags must be used.
*
* @example <caption>At least 2 of the 3 flags --foo, --bar, and --baz must be used</caption>
* flags('foo', 'bar', 'baz').are.requiredAtLeastN(2)
*
* @param n
*/
requiredAtLeastN(n) {
this.constraintApplicatorFunctionHolder.setConstraintApplicator(`requiredAtLeast${n}`, (flags) => required(n, 'AT_LEAST_N', this.constrainedFlags, flags, this.topLevelCondition !== undefined));
return this;
}
/**
* Establish that at most N of the specified flags must be used.
*
* @example <caption>No more than 2 of the 3 flags --foo, --bar, and --baz may be used</caption>
* flags('foo', 'bar', 'baz').are.requiredAtMostN(2)
*
* @param n
*/
requiredAtMostN(n) {
this.constraintApplicatorFunctionHolder.setConstraintApplicator(`requiredAtMost${n}`, (flags) => required(n, 'AT_MOST_N', this.constrainedFlags, flags, this.topLevelCondition !== undefined));
return this;
}
/**
* Establish that exactly N of the specified flags must be used.
*
* @example <caption>Exactly 2 of the 3 flags --foo, --bar, and --baz must be used</caption>
* flags('foo', 'bar', 'baz').are.requiredExactlyN(2)
*
* @param n
*/
requiredExactlyN(n) {
this.constraintApplicatorFunctionHolder.setConstraintApplicator(`requiredExactly${n}`, (flags) => required(n, 'EXACTLY_N', this.constrainedFlags, flags, this.topLevelCondition !== undefined));
return this;
}
/**
* Chain method allowing the constraint to be made contingent on the return of a method that accepts all flags.
*
* @example <caption>--foo1 and --foo2 are required if --bar is equal to --baz</caption>
* flags('foo1', 'foo2').are.requiredAll().when.thisIsTrue((flags) => flags.bar === flags.baz)
*
* @param flagTester A method that accepts the flag values mapped by their name, and returns a boolean
*/
thisIsTrue(flagTester) {
// istanbul ignore else - All cases covered
if (this.underConstructionCondition === undefined) {
throw new Error(`Misconfigured constraint condition on ${createFlagString(this.constrainedFlags)}: thisIsTrue must immediately follow a when/unless/and/or`);
}
else if (this.underConstructionCondition instanceof UnaryOpCondition) {
this.underConstructionCondition.setCondition(new ThisIsTrueCondition(flagTester));
this.underConstructionCondition = undefined;
}
else if (this.underConstructionCondition instanceof BinaryCondition) {
this.underConstructionCondition.setRight(new ThisIsTrueCondition(flagTester));
this.underConstructionCondition = undefined;
}
else {
throw new TypeError('UNKNOWN CONDITION TYPE');
}
return this;
}
}
class ConstraintApplicatorFunctionHolder {
constraintType;
constraintApplicatorFunction;
applyConstraintApplicatorFunction(flags) {
return (this.constraintApplicatorFunction ??
function () {
return '';
})(flags);
}
setConstraintApplicator(constraintType, constraintFunction) {
if (this.constraintApplicatorFunction) {
// This error is meant to be seen by the developer of the command, not its user.
throw new Error(`Misconfigured Constraint: Cannot apply multiple kinds of constraint within one statement: ${this.constraintType}, ${constraintType}. Use multiple constraint expressions instead.`);
}
this.constraintType = constraintType;
this.constraintApplicatorFunction = constraintFunction;
}
}
const Requirement = {
AT_LEAST_N: {
fn: (n, other) => other >= n,
label: 'at least',
},
AT_MOST_N: {
fn: (n, other) => other <= n,
label: 'at most',
},
EXACTLY_N: {
fn: (n, other) => other === n,
label: 'exactly',
},
};
function required(n, requirementType, soughtFlags, providedFlags, hasConditions) {
const foundFlags = filterFlagsPresentInInput(soughtFlags, providedFlags);
if (Requirement[requirementType].fn(n, foundFlags.length)) {
return '';
}
return `Must provide ${Requirement[requirementType].label} ${n} of the following${hasConditions ? ' under current circumstances:' : ':'} ${createFlagString(soughtFlags)}. Found ${foundFlags.length}.`;
}
function filterFlagsPresentInInput(flagsToSeek, flags) {
return flagsToSeek.filter((f) => f in flags && flags[f] !== undefined);
}
function createFlagString(flags) {
const processedGroups = flags.map((f) => {
if (typeof f === 'string') {
return `--${f}`;
}
return `combination of ${f.flags.map((f) => `--${f}`).join(' and ')}`;
});
return processedGroups.join(', ');
}
class Condition {
}
class UnaryOpCondition extends Condition {
condition;
setCondition(condition) {
// istanbul ignore if - should be unreachable
if (this.condition) {
throw new Error(`Duplicate conditions applied to '${this.getName()}' clause: '${this.condition.getName()}' and '${condition.getName()}'.`);
}
this.condition = condition;
}
}
class WhenCondition extends UnaryOpCondition {
getName() {
return 'when';
}
isSatisfied(flags) {
if (this.condition) {
return this.condition.isSatisfied(flags);
}
throw new Error("'when' expression without any conditions");
}
}
class UnlessCondition extends UnaryOpCondition {
getName() {
return 'unless';
}
isSatisfied(flags) {
if (this.condition) {
return !this.condition.isSatisfied(flags);
}
throw new Error("'unless' expression without any conditions");
}
}
class AllFlagCriteriaSatisfiedCondition extends Condition {
tester;
constructor(tester) {
super();
this.tester = tester;
}
getName() {
return 'allFlagCriteriaSatisfied';
}
isSatisfied(flags) {
for (const testedFlag of Object.keys(this.tester)) {
if (!this.tester[testedFlag](flags[testedFlag])) {
return false;
}
}
return true;
}
}
class AnyFlagCriterionSatisfiedCondition extends Condition {
tester;
constructor(tester) {
super();
this.tester = tester;
}
getName() {
return 'anyFlagCriterionSatisfied';
}
isSatisfied(flags) {
for (const testedFlag of Object.keys(this.tester)) {
if (this.tester[testedFlag](flags[testedFlag])) {
return true;
}
}
return false;
}
}
class ThisIsTrueCondition extends Condition {
tester;
constructor(tester) {
super();
this.tester = tester;
}
getName() {
return 'thisIsTrue';
}
isSatisfied(flags) {
return this.tester(flags);
}
}
class BinaryCondition extends Condition {
left;
right;
constructor(left) {
super();
this.left = left;
}
setRight(right) {
this.right = right;
}
}
class AndCondition extends BinaryCondition {
getName() {
return 'and';
}
isSatisfied(flags) {
return this.left.isSatisfied(flags) && (this.right ? this.right.isSatisfied(flags) : true);
}
}
class OrCondition extends BinaryCondition {
getName() {
return 'or';
}
isSatisfied(flags) {
return this.left.isSatisfied(flags) || (this.right ? this.right.isSatisfied(flags) : false);
}
}