-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support complex flag relationships #468
Conversation
src/interfaces/parser.ts
Outdated
@@ -112,6 +112,16 @@ export type FlagProps = { | |||
hidden?: boolean; | |||
required?: boolean; | |||
dependsOn?: string[]; | |||
relationships?: { | |||
dependsOn?: { | |||
type: 'all' | 'atLeastOne'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
none
could allow exclusive to go away?
src/interfaces/parser.ts
Outdated
relationships?: { | ||
dependsOn?: { | ||
type: 'all' | 'atLeastOne'; | ||
flags: string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flags: string[] | Array<{flag: string, value: any}>
would allow you to say things like:
flag A can't be used if flag B has value X (exclusive)
or
you can't use flag A unless flag B has value Y (dependsOn)
or even (allow or disallow multiple values from a flag)
[{ flag: 'a', value: 'x'}, {flag: 'a', value: 'y'}]
|
||
function validateExclusive(name: string, exclusive: string[]) { | ||
for (const also of exclusive) { | ||
// do not enforce exclusivity for flags that were defaulted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider making this configurable. Flag B exists, but can't be used unless you make Flag A something other than its default
src/parser/validate.ts
Outdated
let foundAtLeastOne = false | ||
for (const flag of dependsOnFlags) { | ||
if (parse.output.flags[flag]) { | ||
foundAtLeastOne = true | ||
break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let foundAtLeastOne = false | |
for (const flag of dependsOnFlags) { | |
if (parse.output.flags[flag]) { | |
foundAtLeastOne = true | |
break | |
const foundAtLeastOne = dependsOnFlags.some((flag) => parse.output.flags[flag]) |
src/interfaces/parser.ts
Outdated
flags: string[]; | ||
}; | ||
exclusive?: { | ||
type: 'all' | 'atLeastOne'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checking my understanding:
'all' => it's an error if you use all the flags listed?
'atLeastOne' => it's an error if you use any of the flags listed?
src/config/config.ts
Outdated
@@ -751,6 +751,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm | |||
helpGroup: flag.helpGroup, | |||
allowNo: flag.allowNo, | |||
dependsOn: flag.dependsOn, | |||
relationships: flag.relationships, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would we want to use union types to make it not possible to use both relationships and any of the simple ones (dependsOn, exclusive, exactlyOne) so that it doesn't get really nasty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel comfortable not putting any guard rails up for that. People can make a mess of their flags if they want
For the sf side, we could always add some linting rules to guide people away from doing too much mixing and matching
@@ -86,6 +86,12 @@ export type DefaultContext<T> = { | |||
export type Default<T> = T | ((context: DefaultContext<T>) => Promise<T>) | |||
export type DefaultHelp<T> = T | ((context: DefaultContext<T>) => Promise<string | undefined>) | |||
|
|||
export type FlagRelationship = string | {name: string; when: (flags: Record<string, unknown>) => Promise<boolean>}; | |||
export type Relationship = { | |||
type: 'all' | 'some' | 'none'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider one
or 1
since we've already got an exactlyOne
since it happened often enough.
src/parser/validate.ts
Outdated
|
||
async function validateRelationships(name: string, flag: CompletableFlag<any>) { | ||
if (!flag.relationships) return | ||
for (const relationship of flag.relationships) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
worth parallelizing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ux idea: as written, you'll get a failure on the first validation to throw. For the user, they'll fix one error and then hit the next.
You could run this with Promise.allSettled and maybe display all the errors in the output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approved with some ideas/suggestions
}) | ||
}) | ||
|
||
describe('type: some', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testing ideas:
it looks like you've always got length = 1 for relationships. I'd be interested in maybe a "big test" that
- tests an array of several relationships
- includes relationships of different types
{ | ||
type: 'all', | ||
flags: [ | ||
'cookies', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
src/parser/validate.ts
Outdated
parse.output.metadata.flags[flag] && | ||
parse.output.metadata.flags[flag].setFromDefault |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parse.output.metadata.flags[flag] && | |
parse.output.metadata.flags[flag].setFromDefault | |
parse.output.metadata.flags[flag]?.setFromDefault |
etc on the others.
src/parser/validate.ts
Outdated
function validateExactlyOne(name: string, exactlyOne: FlagRelationship[]) { | ||
for (const flag of exactlyOne || []) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defaults in the signature
function validateExactlyOne(name: string, exactlyOne: FlagRelationship[]) { | |
for (const flag of exactlyOne || []) { | |
function validateExactlyOne(name: string, exactlyOne: FlagRelationship[] = []) { | |
for (const flag of exactlyOne) { |
src/parser/validate.ts
Outdated
|
||
async function validateRelationships(name: string, flag: CompletableFlag<any>) { | ||
if (!flag.relationships) return | ||
for (const relationship of flag.relationships) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ux idea: as written, you'll get a failure on the first validation to throw. For the user, they'll fix one error and then hit the next.
You could run this with Promise.allSettled and maybe display all the errors in the output.
src/parser/validate.ts
Outdated
@@ -54,43 +56,13 @@ export function validate(parse: { | |||
} | |||
} | |||
|
|||
function validateFlags() { | |||
async function validateFlags() { | |||
for (const [name, flag] of Object.entries(parse.input.flags)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to see if parallelizing this would help with perf (both the various validations for each flag, AND doing all the flags together)
src/parser/validate.ts
Outdated
const keys = Object.keys(resolved).reduce((acc, key) => { | ||
if (resolved[key]) acc.push(key) | ||
return acc | ||
}, [] as string[]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure why this reduce needs to be here. Another design would be
Object.entries(resolved).forEach(
and use 1 && conditional to check what you're checking to get to an error
- the value is truthy AND
2.parse.output.metadata.flags[flag]?.setFromDefault
is falsy AND parse.output.metadata.flags[name]?.setFromDefault
is falsy ANDparse.output.flags[flag]
is truthy
--> throw!
that way it's only 1 iteration, the conditionals can be in whatever order is most likely to resolve soonest and it'll fail on the first error
Lay groundwork to support more advanced flag relationship validation
Example usage
Closes oclif/oclif#323