chinampa/pkg/command/arguments.go
Roberto Hidalgo 725347ec48 error on unexpected arguments, test out logger, add .milpa
courtesy of the department of departamental recursiveness
2023-03-20 22:18:09 -06:00

299 lines
7.5 KiB
Go

// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package command
import (
"fmt"
"strings"
"git.rob.mx/nidito/chinampa/pkg/errors"
"github.com/spf13/cobra"
)
func contains(haystack []string, needle string) bool {
for _, validValue := range haystack {
if needle == validValue {
return true
}
}
return false
}
// Arguments is an ordered list of Argument.
type Arguments []*Argument
func (args *Arguments) AllKnown() map[string]any {
col := map[string]any{}
for _, arg := range *args {
col[arg.Name] = arg.ToValue()
}
return col
}
func (args *Arguments) AllKnownStr() map[string]string {
col := map[string]string{}
for _, arg := range *args {
col[arg.Name] = arg.ToString()
}
return col
}
func anySliceToStringSlice(src any) []string {
res := []string{}
switch d := src.(type) {
case []string:
res = d
case []any:
for _, valI := range d {
res = append(res, valI.(string))
}
}
return res
}
func (args *Arguments) Parse(supplied []string) error {
parsed := []string{}
for idx, arg := range *args {
argumentProvided := idx < len(supplied)
if !argumentProvided {
if arg.Default != nil {
if arg.Variadic {
defaultSlice := anySliceToStringSlice(arg.Default)
arg.provided = &defaultSlice
} else {
defaultString := arg.Default.(string)
if defaultString != "" {
arg.provided = &[]string{defaultString}
}
}
}
continue
}
if arg.Variadic {
values := append([]string{}, supplied[idx:]...)
arg.SetValue(values)
} else {
arg.SetValue([]string{supplied[idx]})
}
parsed = append(parsed, *arg.provided...)
}
if len(parsed) != len(supplied) {
return errors.BadArguments{Msg: fmt.Sprintf("Unexpected arguments provided: %s", supplied)}
}
return nil
}
func (args *Arguments) AreValid() error {
for _, arg := range *args {
if err := arg.Validate(); err != nil {
return err
}
}
return nil
}
// CompletionFunction is called by cobra when asked to complete arguments.
func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string, toComplete string) ([]string, cobra.ShellCompDirective) {
expectedArgLen := len(*args)
values := []string{}
directive := cobra.ShellCompDirectiveError
if cc.HasAvailableSubCommands() && len(provided) < 1 {
// if this is an "index" command and user is providing first argument
// allow completions instead of erroring by default
directive = cobra.ShellCompDirectiveDefault
}
if expectedArgLen > 0 {
argsCompleted := len(provided)
lastArg := (*args)[len(*args)-1]
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
lastArg.Command.Options.Parse(cc.Flags())
if err := args.Parse(provided); err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
}
directive = cobra.ShellCompDirectiveDefault
if argsCompleted < expectedArgLen || hasVariadicArg {
var arg *Argument
if hasVariadicArg && argsCompleted >= expectedArgLen {
// completing a variadic argument
arg = lastArg
} else {
// completing regular argument (maybe variadic!)
arg = (*args)[argsCompleted]
}
if arg.Values != nil {
var err error
arg.Values.command = lastArg.Command
arg.Command = lastArg.Command
values, directive, err = arg.Resolve(toComplete)
if err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
}
} else {
directive = cobra.ShellCompDirectiveError
}
values = cobra.AppendActiveHelp(values, arg.Description)
}
if toComplete != "" && directive != cobra.ShellCompDirectiveFilterFileExt && directive != cobra.ShellCompDirectiveFilterDirs {
filtered := []string{}
for _, value := range values {
if strings.HasPrefix(value, toComplete) {
filtered = append(filtered, value)
}
}
values = filtered
}
}
return values, directive
}
// Argument represents a single command-line argument.
type Argument struct {
// Name is how this variable will be exposed to the underlying command.
Name string `json:"name" yaml:"name" validate:"required,excludesall=!$\\/%^@#?:'\""`
// Description is what this argument is for.
Description string `json:"description" yaml:"description" validate:"required"`
// Default is the default value for this argument if none is provided.
Default any `json:"default,omitempty" yaml:"default,omitempty" validate:"excluded_with=Required"`
// Variadic makes an argument a list of all values from this one on.
Variadic bool `json:"variadic" yaml:"variadic"`
// Required raises an error if an argument is not provided.
Required bool `json:"required" yaml:"required" validate:"excluded_with=Default"`
// Values describes autocompletion and validation for an argument
Values *ValueSource `json:"values,omitempty" yaml:"values" validate:"omitempty"`
Command *Command `json:"-" yaml:"-" validate:"-"`
provided *[]string
}
func (arg *Argument) EnvName() string {
return strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))
}
func (arg *Argument) SetValue(value []string) {
arg.provided = &value
}
func (arg *Argument) IsKnown() bool {
return arg.provided != nil && len(*arg.provided) > 0
}
func (arg *Argument) ToString() string {
val := arg.ToValue()
if arg.Variadic {
val := val.([]string)
return strings.Join(val, " ")
}
return val.(string)
}
func (arg *Argument) ToValue() any {
var value any
if arg.IsKnown() {
if arg.Variadic {
value = *arg.provided
} else {
vals := *arg.provided
value = vals[0]
}
} else {
if arg.Default != nil {
if arg.Variadic {
defaultSlice := anySliceToStringSlice(arg.Default)
// for _, valI := range arg.Default.([]any) {
// valStr := valI.(string)
// defaultSlice = append(defaultSlice, valStr)
// }
value = defaultSlice
} else {
value = arg.Default.(string)
}
} else {
if arg.Variadic {
value = []string{}
} else {
value = ""
}
}
}
return value
}
func (arg *Argument) Validate() error {
if !arg.IsKnown() {
if arg.Required {
return errors.BadArguments{Msg: fmt.Sprintf("Missing argument for %s", strings.ToUpper(arg.Name))}
}
return nil
}
if !arg.Validates() {
return nil
}
validValues, _, err := arg.Resolve(strings.Join(*arg.provided, " "))
if err != nil {
return err
}
if arg.Variadic {
for _, current := range *arg.provided {
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
}
}
} else {
current := arg.ToValue().(string)
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
}
}
return nil
}
// Validates tells if the user-supplied value needs validation.
func (arg *Argument) Validates() bool {
return arg.Values != nil && arg.Values.Validates()
}
// ToDesc prints out the description of an argument for help and docs.
func (arg *Argument) ToDesc() string {
spec := arg.EnvName()
if arg.Variadic {
spec = fmt.Sprintf("%s...", spec)
}
if !arg.Required {
spec = fmt.Sprintf("[%s]", spec)
}
return spec
}
// Resolve returns autocomplete values for an argument.
func (arg *Argument) Resolve(current string) (values []string, flag cobra.ShellCompDirective, err error) {
if arg.Values != nil {
values, flag, err = arg.Values.Resolve(current)
if err != nil {
flag = cobra.ShellCompDirectiveError
return
}
}
return
}