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 }