package config import ( "errors" "fmt" "reflect" "regexp" "git.sr.ht/~rjarry/aerc/lib/templates" "github.com/emersion/go-message/mail" "github.com/go-ini/ini" ) func MapToStruct(s *ini.Section, v interface{}, useDefaults bool) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { panic("MapToStruct requires a pointer") } if typ.Kind() != reflect.Struct { panic("MapToStruct requires a pointer to a struct") } for i := 0; i < typ.NumField(); i++ { fieldVal := val.Field(i) fieldType := typ.Field(i) name := fieldType.Tag.Get("ini") if name == "" || name == "-" { continue } key, err := s.GetKey(name) if err != nil { defValue, found := fieldType.Tag.Lookup("default") if useDefaults && found { key, _ = s.NewKey(name, defValue) } else { continue } } err = setField(s, key, reflect.ValueOf(v), fieldVal, fieldType) if err != nil { return fmt.Errorf("[%s].%s: %w", s.Name(), name, err) } } return nil } func setField( s *ini.Section, key *ini.Key, struc reflect.Value, fieldVal reflect.Value, fieldType reflect.StructField, ) error { var methodValue reflect.Value method := getParseMethod(s, key, struc, fieldType) if method.IsValid() { in := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(key)} out := method.Call(in) err, _ := out[1].Interface().(error) if err != nil { return err } methodValue = out[0] } ft := fieldType.Type switch ft.Kind() { case reflect.String: if method.IsValid() { fieldVal.SetString(methodValue.String()) } else { fieldVal.SetString(key.String()) } case reflect.Bool: if method.IsValid() { fieldVal.SetBool(methodValue.Bool()) } else { boolVal, err := key.Bool() if err != nil { return err } fieldVal.SetBool(boolVal) } case reflect.Int32: // impossible to differentiate rune from int32, they are aliases // this is an ugly hack but there is no alternative... if fieldType.Tag.Get("type") == "rune" { if method.IsValid() { fieldVal.Set(methodValue) } else { runes := []rune(key.String()) if len(runes) != 1 { return errors.New("value must be 1 character long") } fieldVal.Set(reflect.ValueOf(runes[0])) } return nil } fallthrough case reflect.Int64: // ParseDuration will not return err for `0`, so check the type name if ft.PkgPath() == "time" && ft.Name() == "Duration" { durationVal, err := key.Duration() if err != nil { return err } fieldVal.Set(reflect.ValueOf(durationVal)) return nil } fallthrough case reflect.Int, reflect.Int8, reflect.Int16: if method.IsValid() { fieldVal.SetInt(methodValue.Int()) } else { intVal, err := key.Int64() if err != nil { return err } fieldVal.SetInt(intVal) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if method.IsValid() { fieldVal.SetUint(methodValue.Uint()) } else { uintVal, err := key.Uint64() if err != nil { return err } fieldVal.SetUint(uintVal) } case reflect.Float32, reflect.Float64: if method.IsValid() { fieldVal.SetFloat(methodValue.Float()) } else { floatVal, err := key.Float64() if err != nil { return err } fieldVal.SetFloat(floatVal) } case reflect.Slice, reflect.Array: switch { case method.IsValid(): fieldVal.Set(methodValue) case ft.Elem().Kind() == reflect.Ptr && typePath(ft.Elem().Elem()) == "net/mail.Address": addrs, err := mail.ParseAddressList(key.String()) if err != nil { return err } fieldVal.Set(reflect.ValueOf(addrs)) case ft.Elem().Kind() == reflect.String: delim := fieldType.Tag.Get("delim") fieldVal.Set(reflect.ValueOf(key.Strings(delim))) default: panic(fmt.Sprintf("unsupported type []%s", typePath(ft.Elem()))) } case reflect.Struct: if method.IsValid() { fieldVal.Set(methodValue) } else { panic(fmt.Sprintf("unsupported type %s", typePath(ft))) } case reflect.Ptr: if method.IsValid() { fieldVal.Set(methodValue) } else { switch typePath(ft.Elem()) { case "net/mail.Address": addr, err := mail.ParseAddress(key.String()) if err != nil { return err } fieldVal.Set(reflect.ValueOf(addr)) case "regexp.Regexp": r, err := regexp.Compile(key.String()) if err != nil { return err } fieldVal.Set(reflect.ValueOf(r)) case "text/template.Template": t, err := templates.ParseTemplate(key.String(), key.String()) if err != nil { return err } fieldVal.Set(reflect.ValueOf(t)) default: panic(fmt.Sprintf("unsupported type %s", typePath(ft))) } } default: panic(fmt.Sprintf("unsupported type %s", typePath(ft))) } return nil } func getParseMethod( section *ini.Section, key *ini.Key, struc reflect.Value, typ reflect.StructField, ) reflect.Value { methodName, found := typ.Tag.Lookup("parse") if !found { return reflect.Value{} } method := struc.MethodByName(methodName) if !method.IsValid() { panic(fmt.Sprintf("(*%s).%s: method not found", struc, methodName)) } if method.Type().NumIn() != 2 || method.Type().In(0) != reflect.TypeOf(section) || method.Type().In(1) != reflect.TypeOf(key) || method.Type().NumOut() != 2 { panic(fmt.Sprintf("(*%s).%s: invalid signature, expected %s", struc.Elem().Type().Name(), methodName, "func(*ini.Section, *ini.Key) (any, error)")) } return method } func typePath(t reflect.Type) string { var prefix string if t.Kind() == reflect.Ptr { t = t.Elem() prefix = "*" } return fmt.Sprintf("%s%s.%s", prefix, t.PkgPath(), t.Name()) }