github.com/maresnic/mr-kong@v1.0.0/tag.go (about)

     1  package kong
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"strconv"
     8  	"strings"
     9  	"unicode/utf8"
    10  )
    11  
    12  // Tag represents the parsed state of Kong tags in a struct field tag.
    13  type Tag struct {
    14  	Ignored     bool // Field is ignored by Kong. ie. kong:"-"
    15  	Cmd         bool
    16  	Arg         bool
    17  	Required    bool
    18  	Optional    bool
    19  	Name        string
    20  	Help        string
    21  	Type        string
    22  	TypeName    string
    23  	HasDefault  bool
    24  	Default     string
    25  	Format      string
    26  	PlaceHolder string
    27  	Envs        []string
    28  	Short       rune
    29  	Hidden      bool
    30  	Sep         rune
    31  	MapSep      rune
    32  	Enum        string
    33  	Group       string
    34  	Xor         []string
    35  	Vars        Vars
    36  	Prefix      string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
    37  	EnvPrefix   string
    38  	Embed       bool
    39  	Aliases     []string
    40  	Negatable   bool
    41  	Passthrough bool
    42  
    43  	// Storage for all tag keys for arbitrary lookups.
    44  	items map[string][]string
    45  }
    46  
    47  func (t *Tag) String() string {
    48  	out := []string{}
    49  	for key, list := range t.items {
    50  		for _, value := range list {
    51  			out = append(out, fmt.Sprintf("%s:%q", key, value))
    52  		}
    53  	}
    54  	return strings.Join(out, " ")
    55  }
    56  
    57  type tagChars struct {
    58  	sep, quote, assign rune
    59  	needsUnquote       bool
    60  }
    61  
    62  var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
    63  var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
    64  
    65  //nolint:gocyclo
    66  func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
    67  	d := map[string][]string{}
    68  	key := []rune{}
    69  	value := []rune{}
    70  	quotes := false
    71  	inKey := true
    72  
    73  	add := func() error {
    74  		// Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly)
    75  		// unquotes "kong tags".
    76  		s := string(value)
    77  
    78  		if chr.needsUnquote && s != "" {
    79  			if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil {
    80  				s = unquoted
    81  			} else {
    82  				return fmt.Errorf("unquoting tag value `%s`: %w", s, err)
    83  			}
    84  		}
    85  
    86  		d[string(key)] = append(d[string(key)], s)
    87  		key = []rune{}
    88  		value = []rune{}
    89  		inKey = true
    90  
    91  		return nil
    92  	}
    93  
    94  	runes := []rune(tagString)
    95  	for idx := 0; idx < len(runes); idx++ {
    96  		r := runes[idx]
    97  		next := rune(0)
    98  		eof := false
    99  		if idx < len(runes)-1 {
   100  			next = runes[idx+1]
   101  		} else {
   102  			eof = true
   103  		}
   104  		if !quotes && r == chr.sep {
   105  			if err := add(); err != nil {
   106  				return nil, err
   107  			}
   108  
   109  			continue
   110  		}
   111  		if r == chr.assign && inKey {
   112  			inKey = false
   113  			continue
   114  		}
   115  		if r == '\\' {
   116  			if next == chr.quote {
   117  				idx++
   118  
   119  				// We need to keep the backslashes, otherwise subsequent unquoting cannot work
   120  				if chr.needsUnquote {
   121  					value = append(value, r)
   122  				}
   123  
   124  				r = chr.quote
   125  			}
   126  		} else if r == chr.quote {
   127  			if quotes {
   128  				quotes = false
   129  				if next == chr.sep || eof {
   130  					continue
   131  				}
   132  				return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx)
   133  			}
   134  			quotes = true
   135  			continue
   136  		}
   137  		if inKey {
   138  			key = append(key, r)
   139  		} else {
   140  			value = append(value, r)
   141  		}
   142  	}
   143  	if quotes {
   144  		return nil, fmt.Errorf("%v is not quoted properly", tagString)
   145  	}
   146  
   147  	if err := add(); err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return d, nil
   152  }
   153  
   154  func getTagInfo(ft reflect.StructField) (string, tagChars) {
   155  	s, ok := ft.Tag.Lookup("kong")
   156  	if ok {
   157  		return s, kongChars
   158  	}
   159  
   160  	return string(ft.Tag), bareChars
   161  }
   162  
   163  func newEmptyTag() *Tag {
   164  	return &Tag{items: map[string][]string{}}
   165  }
   166  
   167  func tagSplitFn(r rune) bool {
   168  	return r == ',' || r == ' '
   169  }
   170  
   171  func parseTagString(s string) (*Tag, error) {
   172  	items, err := parseTagItems(s, bareChars)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	t := &Tag{
   177  		items: items,
   178  	}
   179  	err = hydrateTag(t, nil)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("%s: %s", s, err)
   182  	}
   183  	return t, nil
   184  }
   185  
   186  func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
   187  	if ft.Tag.Get("kong") == "-" {
   188  		t := newEmptyTag()
   189  		t.Ignored = true
   190  		return t, nil
   191  	}
   192  	items, err := parseTagItems(getTagInfo(ft))
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	t := &Tag{
   197  		items: items,
   198  	}
   199  	err = hydrateTag(t, ft.Type)
   200  	if err != nil {
   201  		return nil, failField(parent, ft, "%s", err)
   202  	}
   203  	return t, nil
   204  }
   205  
   206  func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
   207  	var typeName string
   208  	var isBool bool
   209  	var isBoolPtr bool
   210  	if typ != nil {
   211  		typeName = typ.Name()
   212  		isBool = typ.Kind() == reflect.Bool
   213  		isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool
   214  	}
   215  	var err error
   216  	t.Cmd = t.Has("cmd")
   217  	t.Arg = t.Has("arg")
   218  	required := t.Has("required")
   219  	optional := t.Has("optional")
   220  	if required && optional {
   221  		return fmt.Errorf("can't specify both required and optional")
   222  	}
   223  	t.Required = required
   224  	t.Optional = optional
   225  	t.HasDefault = t.Has("default")
   226  	t.Default = t.Get("default")
   227  	// Arguments with defaults are always optional.
   228  	if t.Arg && t.HasDefault {
   229  		t.Optional = true
   230  	} else if t.Arg && !optional { // Arguments are required unless explicitly made optional.
   231  		t.Required = true
   232  	}
   233  	t.Name = t.Get("name")
   234  	t.Help = t.Get("help")
   235  	t.Type = t.Get("type")
   236  	t.TypeName = typeName
   237  	for _, env := range t.GetAll("env") {
   238  		t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...)
   239  	}
   240  	t.Short, err = t.GetRune("short")
   241  	if err != nil && t.Get("short") != "" {
   242  		return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
   243  	}
   244  	t.Hidden = t.Has("hidden")
   245  	t.Format = t.Get("format")
   246  	t.Sep, _ = t.GetSep("sep", ',')
   247  	t.MapSep, _ = t.GetSep("mapsep", ';')
   248  	t.Group = t.Get("group")
   249  	for _, xor := range t.GetAll("xor") {
   250  		t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
   251  	}
   252  	t.Prefix = t.Get("prefix")
   253  	t.EnvPrefix = t.Get("envprefix")
   254  	t.Embed = t.Has("embed")
   255  	negatable := t.Has("negatable")
   256  	if negatable && !isBool && !isBoolPtr {
   257  		return fmt.Errorf("negatable can only be set on booleans")
   258  	}
   259  	t.Negatable = negatable
   260  	aliases := t.Get("aliases")
   261  	if len(aliases) > 0 {
   262  		t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
   263  	}
   264  	t.Vars = Vars{}
   265  	for _, set := range t.GetAll("set") {
   266  		parts := strings.SplitN(set, "=", 2)
   267  		if len(parts) == 0 {
   268  			return fmt.Errorf("set should be in the form key=value but got %q", set)
   269  		}
   270  		t.Vars[parts[0]] = parts[1]
   271  	}
   272  	t.PlaceHolder = t.Get("placeholder")
   273  	t.Enum = t.Get("enum")
   274  	scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)
   275  	if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType {
   276  		return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
   277  	}
   278  	passthrough := t.Has("passthrough")
   279  	if passthrough && !t.Arg && !t.Cmd {
   280  		return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
   281  	}
   282  	t.Passthrough = passthrough
   283  	return nil
   284  }
   285  
   286  // Has returns true if the tag contained the given key.
   287  func (t *Tag) Has(k string) bool {
   288  	_, ok := t.items[k]
   289  	return ok
   290  }
   291  
   292  // Get returns the value of the given tag.
   293  //
   294  // Note that this will return the empty string if the tag is missing.
   295  func (t *Tag) Get(k string) string {
   296  	values := t.items[k]
   297  	if len(values) == 0 {
   298  		return ""
   299  	}
   300  	return values[0]
   301  }
   302  
   303  // GetAll returns all encountered values for a tag, in the case of multiple occurrences.
   304  func (t *Tag) GetAll(k string) []string {
   305  	return t.items[k]
   306  }
   307  
   308  // GetBool returns true if the given tag looks like a boolean truth string.
   309  func (t *Tag) GetBool(k string) (bool, error) {
   310  	return strconv.ParseBool(t.Get(k))
   311  }
   312  
   313  // GetFloat parses the given tag as a float64.
   314  func (t *Tag) GetFloat(k string) (float64, error) {
   315  	return strconv.ParseFloat(t.Get(k), 64)
   316  }
   317  
   318  // GetInt parses the given tag as an int64.
   319  func (t *Tag) GetInt(k string) (int64, error) {
   320  	return strconv.ParseInt(t.Get(k), 10, 64)
   321  }
   322  
   323  // GetRune parses the given tag as a rune.
   324  func (t *Tag) GetRune(k string) (rune, error) {
   325  	value := t.Get(k)
   326  	r, size := utf8.DecodeRuneInString(value)
   327  	if r == utf8.RuneError || size < len(value) {
   328  		return 0, errors.New("invalid rune")
   329  	}
   330  	return r, nil
   331  }
   332  
   333  // GetSep parses the given tag as a rune separator, allowing for a default or none.
   334  // The separator is returned, or -1 if "none" is specified. If the tag value is an
   335  // invalid utf8 sequence, the default rune is returned as well as an error. If the
   336  // tag value is more than one rune, the first rune is returned as well as an error.
   337  func (t *Tag) GetSep(k string, dflt rune) (rune, error) {
   338  	tv := t.Get(k)
   339  	if tv == "none" {
   340  		return -1, nil
   341  	} else if tv == "" {
   342  		return dflt, nil
   343  	}
   344  	r, size := utf8.DecodeRuneInString(tv)
   345  	if r == utf8.RuneError {
   346  		return dflt, fmt.Errorf(`%v:"%v" has a rune error`, k, tv)
   347  	} else if size != len(tv) {
   348  		return r, fmt.Errorf(`%v:"%v" is more than a single rune`, k, tv)
   349  	}
   350  	return r, nil
   351  }