-
Notifications
You must be signed in to change notification settings - Fork 1.2k
WIP: nonEmpty param option support #10678
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,11 +120,11 @@ | |
| // Default value. If not provided, a param must be supplied. | ||
| default?: T | build.Expression<T>; | ||
|
|
||
| // default: false | ||
| immutable?: boolean; | ||
|
|
||
| // Defines how the CLI will prompt for the value of the param if it's not in .env files | ||
| input?: ParamInput<T>; | ||
|
|
||
| // Reject the empty string and empty array as valid input. | ||
| nonEmpty?: boolean; | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -272,7 +272,7 @@ | |
| return pv; | ||
| } | ||
|
|
||
| setDelimiter(delimiter: string) { | ||
| this.delimiter = delimiter; | ||
| } | ||
|
|
||
|
|
@@ -299,7 +299,7 @@ | |
| if (this.rawValue.includes("[")) { | ||
| // Convert quotes to apostrophes | ||
| const unquoted = this.rawValue.replace(/'/g, '"'); | ||
| return JSON.parse(unquoted); | ||
| } | ||
|
|
||
| // Continue to handle something like "a,b,c" | ||
|
|
@@ -418,7 +418,7 @@ | |
| } | ||
| if (paramDefault && !canSatisfyParam(param, paramDefault)) { | ||
| throw new FirebaseError( | ||
| "Parameter " + param.name + " has default value " + paramDefault + " of wrong type", | ||
| ); | ||
| } | ||
| paramValues[param.name] = await promptParam(param, firebaseConfig.projectId, paramDefault); | ||
|
|
@@ -571,6 +571,7 @@ | |
| prompt, | ||
| param.input, | ||
| resolvedDefault, | ||
| param.nonEmpty, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is nonEmpty not part of input? |
||
| (res: string[]) => res, | ||
| ); | ||
| } else if (isTextInput(param.input)) { | ||
|
|
@@ -580,15 +581,21 @@ | |
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptText<string[]>(prompt, param.input, resolvedDefault, (res: string): string[] => { | ||
| return res.split(param.delimiter || ","); | ||
| }); | ||
| return promptText<string[]>( | ||
| prompt, | ||
| param.input, | ||
| resolvedDefault, | ||
| param.nonEmpty, | ||
| (res: string): string[] => { | ||
| return res.split(param.delimiter || ","); | ||
| }, | ||
| ); | ||
| } else if (isResourceInput(param.input)) { | ||
| prompt = `Select values for ${param.label || param.name}:`; | ||
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptResourceStrings(prompt, param.input, projectId); | ||
| return promptResourceStrings(prompt, param.input, projectId, param.nonEmpty); | ||
| } else { | ||
| assertExhaustive(param.input); | ||
| } | ||
|
|
@@ -602,7 +609,7 @@ | |
| const defaultToText: TextInput<string> = { text: {} }; | ||
| param.input = defaultToText; | ||
| } | ||
| const isTruthyInput = (res: string) => ["true", "y", "yes", "1"].includes(res.toLowerCase()); | ||
| let prompt: string; | ||
|
|
||
| if (isSelectInput(param.input)) { | ||
|
|
@@ -619,7 +626,7 @@ | |
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptText<boolean>(prompt, param.input, resolvedDefault, isTruthyInput); | ||
| return promptText<boolean>(prompt, param.input, resolvedDefault, false, isTruthyInput); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please always leave a comment on any boolean literal ever used as a parameter |
||
| } else if (isResourceInput(param.input)) { | ||
| throw new FirebaseError("Boolean params cannot have Cloud Resource selector inputs"); | ||
| } else { | ||
|
|
@@ -643,7 +650,7 @@ | |
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptResourceString(prompt, param.input, projectId, resolvedDefault); | ||
| return promptResourceString(prompt, param.input, projectId, param.nonEmpty, resolvedDefault); | ||
| } else if (isMultiSelectInput(param.input)) { | ||
| throw new FirebaseError("Non-list params cannot have multi selector inputs"); | ||
| } else if (isSelectInput(param.input)) { | ||
|
|
@@ -658,7 +665,13 @@ | |
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptText<string>(prompt, param.input, resolvedDefault, (res: string) => res); | ||
| return promptText<string>( | ||
| prompt, | ||
| param.input, | ||
| resolvedDefault, | ||
| param.nonEmpty, | ||
| (res: string) => res, | ||
| ); | ||
| } else { | ||
| assertExhaustive(param.input); | ||
| } | ||
|
|
@@ -693,7 +706,7 @@ | |
| if (param.description) { | ||
| prompt += ` \n(${param.description})`; | ||
| } | ||
| return promptText<number>(prompt, param.input, resolvedDefault, (res: string) => { | ||
| return promptText<number>(prompt, param.input, resolvedDefault, false, (res: string) => { | ||
| if (isNaN(+res)) { | ||
| return { message: `"${res}" could not be converted to a number.` }; | ||
| } | ||
|
|
@@ -713,16 +726,17 @@ | |
| prompt: string, | ||
| input: ResourceInput, | ||
| projectId: string, | ||
| disallowEmpty: boolean | undefined, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why disallowEmpty and nonEmpty? Also, why mandatory but supports undefined? |
||
| resolvedDefault?: string, | ||
| ): Promise<string> { | ||
|
Berlioz marked this conversation as resolved.
|
||
| const notFound = new FirebaseError(`No instances of ${input.resource.type} found.`); | ||
| switch (input.resource.type) { | ||
| case "storage.googleapis.com/Bucket": | ||
| const buckets = (await listBuckets(projectId)).map((b) => b.name); | ||
| if (buckets.length === 0) { | ||
| throw notFound; | ||
| } | ||
| const forgedInput: SelectInput<string> = { | ||
| select: { | ||
| options: buckets.map((bucketName: string): SelectOptions<string> => { | ||
| return { label: bucketName, value: bucketName }; | ||
|
|
@@ -734,40 +748,55 @@ | |
| logger.warn( | ||
| `Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`, | ||
| ); | ||
| return promptText<string>(prompt, { text: {} }, resolvedDefault, (res: string) => res); | ||
| return promptText<string>( | ||
| prompt, | ||
| { text: {} }, | ||
| resolvedDefault, | ||
| disallowEmpty, | ||
| (res: string) => res, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| async function promptResourceStrings( | ||
| prompt: string, | ||
| input: ResourceInput, | ||
| projectId: string, | ||
| disallowEmpty: boolean | undefined, | ||
| ): Promise<string[]> { | ||
| const notFound = new FirebaseError(`No instances of ${input.resource.type} found.`); | ||
| switch (input.resource.type) { | ||
| case "storage.googleapis.com/Bucket": | ||
| const buckets = (await listBuckets(projectId)).map((b) => b.name); | ||
| if (buckets.length === 0) { | ||
| throw notFound; | ||
| } | ||
| const forgedInput: MultiSelectInput = { | ||
| multiSelect: { | ||
| options: buckets.map((bucketName: string): SelectOptions<string> => { | ||
| return { label: bucketName, value: bucketName }; | ||
| }), | ||
| }, | ||
| }; | ||
| return promptSelectMultiple<string>(prompt, forgedInput, undefined, (res: string[]) => res); | ||
| return promptSelectMultiple<string>( | ||
| prompt, | ||
| forgedInput, | ||
| undefined, | ||
| disallowEmpty, | ||
| (res: string[]) => res, | ||
| ); | ||
| default: | ||
| logger.warn( | ||
| `Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`, | ||
| ); | ||
| return promptText<string[]>(prompt, { text: {} }, undefined, (res: string) => res.split(",")); | ||
| return promptText<string[]>(prompt, { text: {} }, undefined, disallowEmpty, (res: string) => | ||
| res.split(","), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| type retryInput = { message: string }; | ||
| function shouldRetry(obj: any): obj is retryInput { | ||
| return typeof obj === "object" && (obj as retryInput).message !== undefined; | ||
| } | ||
|
|
||
|
|
@@ -775,6 +804,7 @@ | |
| prompt: string, | ||
| textInput: TextInput<T>, | ||
| resolvedDefault: T | undefined, | ||
| disallowEmpty: boolean | undefined, | ||
| converter: (res: string) => T | retryInput, | ||
| ): Promise<T> { | ||
| const res = await input({ | ||
|
|
@@ -788,16 +818,20 @@ | |
| textInput.text.validationErrorMessage || | ||
| `Input did not match provided validator ${userRe.toString()}, retrying...`, | ||
| ); | ||
| return promptText<T>(prompt, textInput, resolvedDefault, converter); | ||
| return promptText<T>(prompt, textInput, resolvedDefault, disallowEmpty, converter); | ||
| } | ||
| } | ||
| if (disallowEmpty && res === "") { | ||
| logger.error(`Input cannot be the empty string, retrying...`); | ||
| return promptText<T>(prompt, textInput, resolvedDefault, disallowEmpty, converter); | ||
| } | ||
| // TODO(vsfan): the toString() is because PromptOnce()'s return type of string | ||
| // is wrong--it will return the type of the default if selected. Remove this | ||
| // hack once we fix the prompt.ts metaprogramming. | ||
| const converted = converter(res.toString()); | ||
| if (shouldRetry(converted)) { | ||
| logger.error(converted.message); | ||
| return promptText<T>(prompt, textInput, resolvedDefault, converter); | ||
| return promptText<T>(prompt, textInput, resolvedDefault, disallowEmpty, converter); | ||
| } | ||
| return converted; | ||
| } | ||
|
|
@@ -831,6 +865,7 @@ | |
| prompt: string, | ||
| input: MultiSelectInput, | ||
| resolvedDefault: T[] | undefined, | ||
| disallowEmpty: boolean | undefined, | ||
| converter: (res: string[]) => T[] | retryInput, | ||
| ): Promise<T[]> { | ||
| const response = await checkbox({ | ||
|
|
@@ -847,7 +882,11 @@ | |
| const converted = converter(response); | ||
| if (shouldRetry(converted)) { | ||
| logger.error(converted.message); | ||
| return promptSelectMultiple<T>(prompt, input, resolvedDefault, converter); | ||
| return promptSelectMultiple<T>(prompt, input, resolvedDefault, disallowEmpty, converter); | ||
| } | ||
| if (disallowEmpty && Array.isArray(converted) && converted.length === 0) { | ||
| logger.error(`Input cannot be empty, retrying...`); | ||
| return promptSelectMultiple<T>(prompt, input, resolvedDefault, disallowEmpty, converter); | ||
| } | ||
|
Berlioz marked this conversation as resolved.
|
||
| return converted; | ||
| } | ||
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 wonder how hard it is to make the field only present for the right types. At minimum you can say: