Skip to content

Commit

Permalink
feat: add atLeastOne flag property
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Dec 16, 2024
1 parent bd979de commit d4746f3
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/interfaces/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export type FlagProps = {
* This is helpful if the default value contains sensitive data that shouldn't be published to npm.
*/
noCacheDefault?: boolean
/**
* At least one of these flags must be provided.
*/
atLeastOne?: string[]
}

export type ArgProps = {
Expand Down
26 changes: 23 additions & 3 deletions src/parser/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ export async function validate(parse: {input: ParserInput; output: ParserOutput}
}

if (flag.exactlyOne && flag.exactlyOne.length > 0) {
return [validateAcrossFlags(flag)]
return [validateExactlyOneAcrossFlags(flag)]
}

if (flag.atLeastOne && flag.atLeastOne.length > 0) {
return [validateAtLeastOneAcrossFlags(flag)]
}

return []
Expand Down Expand Up @@ -115,8 +119,8 @@ export async function validate(parse: {input: ParserInput; output: ParserOutput}
const getPresentFlags = (flags: Record<string, unknown>): string[] =>
Object.keys(flags).filter((key) => key !== undefined)

function validateAcrossFlags(flag: Flag<any>): Validation {
const base = {name: flag.name, validationFn: 'validateAcrossFlags'}
function validateExactlyOneAcrossFlags(flag: Flag<any>): Validation {
const base = {name: flag.name, validationFn: 'validateExactlyOneAcrossFlags'}
const intersection = Object.entries(parse.input.flags)
.map((entry) => entry[0]) // array of flag names
.filter((flagName) => parse.output.flags[flagName] !== undefined) // with values
Expand All @@ -131,6 +135,22 @@ export async function validate(parse: {input: ParserInput; output: ParserOutput}
return {...base, status: 'success'}
}

function validateAtLeastOneAcrossFlags(flag: Flag<any>): Validation {
const base = {name: flag.name, validationFn: 'validateAtLeastOneAcrossFlags'}
const intersection = Object.entries(parse.input.flags)
.map((entry) => entry[0]) // array of flag names
.filter((flagName) => parse.output.flags[flagName] !== undefined) // with values
.filter((flagName) => flag.atLeastOne && flag.atLeastOne.includes(flagName)) // and in the atLeastOne list
if (intersection.length === 0) {
// the command's atLeastOne may or may not include itself, so we'll use Set to add + de-dupe
const deduped = uniq(flag.atLeastOne?.map((flag) => `--${flag}`) ?? []).join(', ')
const reason = `At least one of the following must be provided: ${deduped}`
return {...base, reason, status: 'failed'}
}

return {...base, status: 'success'}
}

async function validateExclusive(name: string, flags: FlagRelationship[]): Promise<Validation> {
const base = {name, validationFn: 'validateExclusive'}
const resolved = await resolveFlags(flags)
Expand Down
53 changes: 53 additions & 0 deletions test/parser/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,59 @@ See more help with --help`)
})
})

describe('atLeastOne', () => {
it('throws if none are set', async () => {
let message = ''
try {
await parse([], {
flags: {
foo: Flags.string({atLeastOne: ['foo', 'bar']}),
bar: Flags.string({char: 'b', atLeastOne: ['foo', 'bar']}),
},
})
} catch (error: any) {
message = error.message
}

expect(message).to.include('At least one of the following must be provided: --bar, --foo')
})

it('succeeds if one is set', async () => {
const out = await parse(['--foo', 'a'], {
flags: {
foo: Flags.string({atLeastOne: ['foo', 'bar', 'baz']}),
bar: Flags.string({char: 'b', atLeastOne: ['foo', 'bar', 'baz']}),
baz: Flags.string({char: 'z'}),
},
})
expect(out.flags.foo).to.equal('a')
})

it('succeeds if some are set', async () => {
const out = await parse(['--bar', 'b'], {
flags: {
foo: Flags.string({atLeastOne: ['foo', 'bar', 'baz']}),
bar: Flags.string({char: 'b', atLeastOne: ['foo', 'bar', 'baz']}),
baz: Flags.string({char: 'z'}),
},
})
expect(out.flags.bar).to.equal('b')
})

it('succeeds if all are set', async () => {
const out = await parse(['--foo', 'a', '--bar', 'b', '--baz', 'c'], {
flags: {
foo: Flags.string({atLeastOne: ['foo', 'bar', 'baz']}),
bar: Flags.string({char: 'b', atLeastOne: ['foo', 'bar', 'baz']}),
baz: Flags.string({char: 'z'}),
},
})
expect(out.flags.foo).to.equal('a')
expect(out.flags.bar).to.equal('b')
expect(out.flags.baz).to.equal('c')
})
})

describe('allowNo', () => {
it('is undefined if not set', async () => {
const out = await parse([], {
Expand Down

0 comments on commit d4746f3

Please sign in to comment.