I’m a fan of Kong the CLI argument parser, I use it to automatically create manual pages, (bash) completion and do a lot of validation inside it. (This guy also has other really interesting packages, and he’s a huge fan of Go’s struct tags).

I wanted to do validation inside Kong as well, for this Kong has validation, which is just a method (Validate() error) on a type. Now most types I have are just string or any other builtin type. For validation to work, all these types must be re-typed to another type on which the method can be applied, which is annoying. I wanted a better way…

For instance I have this field in a struct for a sub-command:

User string `arg:"" name:"loginname|UID" help:"User or UID to list." optional:"" completion:"c user list --comp"`

The completion tag is my invention, see https://github.com/miekg/gompletely, on how that works, anyway, I want to syntax check this User and with Kong’s custom mappers, you can do the following.

loginMapper := kong.MapperFunc(func(ctx *kong.DecodeContext, v reflect.Value) error {
        var login string
        if err := ctx.Scan.PopValueInto("string", &login); err != nil {
                return err
        }
        v.SetString(login)

        if err := dbuser.Valid(login); err == nil {
                return nil
        }

        // might be uid...
        uid, err := strconv.ParseUint(login, 10, 64)
        if err != nil {
                return err
        }
        if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
                return err
        }
        return nil
})

This code calls some other checking function which are not that important, and defines a kong.MapperFunc which can be used when parsing the command line:

c := Cli{}
kctx := kong.Parse(&c, kong.NamedMapper("login", loginMapper))

And with this you get a type:"login", which you can apply to all the places where you want this check to happen:

User string `arg:"" name:"loginname|UID" help:"User or UID to list." optional:"" completion:"c user list --comp" type:"login"`

No extra methods, no new types, just type:"login". Love it.

Update

A (string)slice arguments, that mapper becomes slightly more complex, but still elegant, although I could refactor this a bit and make it even shorter.

oginMapper = kong.MapperFunc(func(ctx *kong.DecodeContext, v reflect.Value) error {
        if v.Type().String() == "[]string" {
                logins := ctx.Scan.PopWhile(func(t kong.Token) bool { return !t.IsEOL() })
                names := []string{}
                for i := range logins {
                        names = append(names, logins[i].Value.(string))
                        if err := dbuser.Valid(logins[i].Value.(string)); err == nil {
                                continue
                        }
                        uid, err := strconv.ParseUint(logins[i].Value.(string), 10, 64)
                        if err != nil {
                                return err
                        }
                        if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
                                return err
                        }
                }
                v.Set(reflect.ValueOf(names))
                return nil
        }

        var login string
        if err := ctx.Scan.PopValueInto("login mapper", &login); err != nil {
                return err
        }
        if login == "" {
                return nil
        }
        v.SetString(login)

        if err := dbuser.Valid(login); err == nil {
                return nil
        }
        uid, err := strconv.ParseUint(login, 10, 64)
        if err != nil {
                return err
        }
        if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
                return err
        }
        return nil
})