package opts import ( "fmt" "reflect" "strconv" "strings" ) type Kind = reflect.Kind const ( Bool = reflect.Bool Int = reflect.Int String = reflect.String ) type Param struct { Name string Kind Kind Default string Required bool Positive bool index []int } type Opts map[string]string func Parse(s string) (string, Opts) { v := strings.Split(s, ",") m := map[string]string{} for _, t := range v[1:] { kv := strings.SplitN(t, "=", 2) if len(kv) < 2 { m[kv[0]] = "" } else { m[kv[0]] = kv[1] } } return v[0], m } func (opts Opts) Bool(key string) bool { _, ok := opts[key] return ok } func extract(i interface{}) (reflect.Value, error) { v := reflect.ValueOf(i) var err error switch { case v.Kind() != reflect.Ptr: err = fmt.Errorf("non-pointer %s", v.Type().String()) case v.IsNil(): err = fmt.Errorf("nil %s", v.Type().String()) case v.Elem().Kind() != reflect.Struct: err = fmt.Errorf("non-struct %s", v.Elem().Type().String()) } if err != nil { return reflect.Value{}, err } return v.Elem(), nil } func (opts Opts) Configure(i interface{}) error { v, err := extract(i) if err != nil { return fmt.Errorf("opts: configure failed: %w", err) } return opts.configure(v) } func (p *Param) parseTags(tags string) { for _, s := range strings.Split(tags, ",") { switch { case s == "required": p.Required = true case s == "positive": p.Positive = true case strings.HasPrefix(s, "default:"): p.Default = s[8:] } } } func (opts Opts) setValue(v reflect.Value, p Param) error { s, ok := opts[p.Name] if p.Kind == Bool { if s != "" { return fmt.Errorf("%s: boolean option does not need a value", p.Name) } if ok { v.SetBool(true) } return nil } if s == "" { if p.Required { return fmt.Errorf("%s: expect value", p.Name) } s = p.Default } if s == "" { return nil } switch p.Kind { case String: v.SetString(s) case Int: n, err := strconv.Atoi(s) if err != nil { return fmt.Errorf("%s: %w", p.Name, err) } if n <= 0 { return fmt.Errorf("%s: expect positive", p.Name) } v.SetInt(int64(n)) default: return fmt.Errorf("%s: unsupported type '%s'", p.Name, v.Type().String()) } return nil } func Parametrize(t reflect.Type) []Param { if t.Kind() != reflect.Struct { return nil } return parametrize(reflect.New(t).Elem()) } func parametrize(v reflect.Value) (param []Param) { var visit func(index []int, v reflect.Value) visit = func(index []int, v reflect.Value) { for n := 0; n < v.NumField(); n++ { f := v.Type().Field(n) x := append(append([]int{}, index...), f.Index...) if f.Anonymous { visit(x, v.Field(n)) continue } if !v.Field(n).CanSet() { continue } p := Param{ Name: strings.ToLower(f.Name), Kind: f.Type.Kind(), index: x, } p.parseTags(f.Tag.Get("opts")) param = append(param, p) } } visit(nil, v) return } func (opts Opts) configure(v reflect.Value) error { param := parametrize(v) m := map[string]bool{} for s := range opts { m[s] = true } for _, p := range param { f := v.FieldByIndex(p.index) if err := opts.setValue(f, p); err != nil { return err } delete(m, p.Name) } if len(m) > 0 { var ss []string for s := range m { ss = append(ss, s) } return fmt.Errorf("unknown options: %s", strings.Join(ss, ",")) } return nil }