Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add cli first class validation #8374

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

Merged
merged 9 commits into from
Jul 11, 2023
Merged

feat: add cli first class validation #8374

merged 9 commits into from
Jul 11, 2023

Conversation

Emyrk
Copy link
Member

@Emyrk Emyrk commented Jul 7, 2023

What this does

A standard way to validate cli flags.

Problem

Validating cli flags tends to be in the cli handler body. This just adds some extra if !valid { return err } type code blocks that add length to the handlers. We also do if flag == "" { return xerrors.Errorf("<flag> must be specified")} errors.

These validation & required errors are not standardized. So the messaging & display of these errors really depends on how much care the developer put in at the time.

This PR aims to standardize how we handle cli flag validation and "require" such that all returned errors will be standard and ideally all errors are returned in a group if possible (this needs to get better on the cmd handling).

Future work

Make the returned standard errors pretty.

Example

An example of requiring a value be set, and adding validation to said value.

clibase.Option{
	Name:        "Coderd (Primary) Access URL",
	Description: "URL to communicate with coderd. This should match the access URL of the Coder deployment.",
	Flag:        "primary-access-url",
	Env:         "CODER_PRIMARY_ACCESS_URL",
	YAML:        "primaryAccessURL",
	// Makes the user provide a value for this option.
	Required:    true,
	// Standard way to validate the input values from the user.
	Value: clibase.Validate(&primaryAccessURL, func(value *clibase.URL) error {
		if !(value.Scheme == "http" || value.Scheme == "https") {
			return xerrors.Errorf("'--primary-access-url' value must be http or https: url=%s", primaryAccessURL.String())
		}
		return nil
	}),
	Group:  &externalProxyOptionGroup,
	Hidden: false,
},

Before

Screenshot from 2023-07-07 10-39-49

After

Screenshot from 2023-07-07 10-40-24

Screenshot from 2023-07-07 11-10-19

@Emyrk Emyrk requested a review from ammario July 7, 2023 15:09
@@ -23,6 +23,10 @@ const (
type Option struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// Required means this value must be set by some means. It requires
// `ValueSource != ValueSourceNone`
// If `Default` is set, then `Required` is ignored.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make Required a composable Validation rule instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It cannot be a validation rule because the validation rule is only called on Set(). If the user never provides a value, and no default is set, then Set() is never called.

So it has to happen in the flag parsing logic.

@@ -16,6 +16,49 @@ import (
"gopkg.in/yaml.v3"
)

// Validator is a wrapper around a pflag.Value that allows for validation
// of the value after or before it has been set.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this implements pflag.Value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's a function Required[T any]() ValidateFunc[T]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's a function Required[T any]() ValidateFunc[T]

I was trying, but unfortunately Validate() can only validate inputs. If there is no input, Set() is never called for this to trigger on.

The only way I could think of making it work is have some post-processing step for the option? It felt easier to make it a flag on the Options and just touch it at the flag parsing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a common enough validation requirement that making it apart of the Option field is justified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The downside is that if Default is set, then Required is ignored. Maybe we should throw an error somewhere if you configure an option like that.

Copy link
Member

@ammario ammario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the improvement and idea. Well done!

@Emyrk Emyrk requested a review from mafredri July 10, 2023 16:58
Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how nicely you plugged in this feature without much extra code.

I did notice a slight inconsistency though:

Required: true

image

Required: false

image

With required flags, the actual error is lost and the message becomes incorrect (the value isn't missing, just didn't pass validation).

(I simply played around a bit by modifying an option in config-ssh, picked randomly)

		{
			Flag:          "ssh-option",
			FlagShorthand: "o",
			Env:           "CODER_SSH_CONFIG_OPTS",
			Description:   "Specifies additional SSH options to embed in each host stanza.",
			Required:      true,
			Value: clibase.Validate(clibase.StringArrayOf(&sshConfigOpts.sshOptions), func(value *clibase.StringArray) error {
				for _, v := range *value {
					if !strings.Contains(v, " ") {
						return xerrors.Errorf("ssh option %q must contain a space", v)
					}
				}
				return nil
			}),
		},

validate func(T) error
}

func Validate[T pflag.Value](opt T, validate ...func(value T) error) *Validator[T] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use-case for variadic validate funcs? Do we foresee passing multiple funcs? Typically I'm all for it, but with generics it seems to hurt VSCode autocomplete-ability (autocompletes to []func 😒).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eww on the autocomplete.

The only rational was a comment by @johnstcn that it might make sense to create building block style validators that you could reuse. But I can put it back to a regular, and then we can create a CombineValidator or something that allows combining.

I had no strong opinions either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh my autocomplete works fine in Goland. Still we can make the varadic some helper func. Or make it a variadic when we need that

@Emyrk
Copy link
Member Author

Emyrk commented Jul 11, 2023

{
			Flag:          "ssh-option",
			FlagShorthand: "o",
			Env:           "CODER_SSH_CONFIG_OPTS",
			Description:   "Specifies additional SSH options to embed in each host stanza.",
			Required:      true,
			Value: clibase.Validate(clibase.StringArrayOf(&sshConfigOpts.sshOptions), func(value *clibase.StringArray) error {
				for _, v := range *value {
					if !strings.Contains(v, " ") {
						return xerrors.Errorf("ssh option %q must contain a space", v)
					}
				}
				return nil
			}),
		},

This is a really good catch thanks. I moved the Required check too far up the command run stack.

After this PR I think some of that code should be refactored to accumulate all errors and make nicer error messages.

$ coder config-ssh -o test                             
parsing flags ([config-ssh -o test]) for "coder config-ssh": invalid argument "test" for "-o, --ssh-option" flag: ssh option "test" must contain a space

@Emyrk
Copy link
Member Author

Emyrk commented Jul 11, 2023

Added a unit test for the invalid case with a required field

@Emyrk Emyrk merged commit bc102d6 into main Jul 11, 2023
@Emyrk Emyrk deleted the stevenmasley/validate branch July 11, 2023 13:59
@github-actions github-actions bot locked and limited conversation to collaborators Jul 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants