package utini import ( "errors" "fmt" "io" "reflect" "regexp" "strconv" "strings" "opslag.de/schobers/ut/utio" ) var errStructPtrExpected = errors.New(`expected pointer to struct`) var keyValuePairRE = regexp.MustCompile(`^\s*(\w+)\s*=\s*(.*)\s*$`) var sectionRE = regexp.MustCompile(`\[((?:\w+\.)*(\w+))\]`) var commentRE = regexp.MustCompile(`^\s*[;#].*$`) func appendToPrefix(prefix, name string) string { if prefix == "" { return name } return fmt.Sprintf(`%s.%s`, prefix, name) } // func fmtErrNoSection(name string) error { return fmt.Errorf(`no section with name %s in file`, name) } func load(lines []string) (*File, error) { file := &File{} var section *Section for _, line := range lines { if commentRE.MatchString(line) { continue } match := sectionRE.FindStringSubmatch(line) if match != nil { section = file.CreateIfNotExists(match[1]) continue } match = keyValuePairRE.FindStringSubmatch(line) if match != nil { if section == nil { section = file.CreateIfNotExists("") } section.Properties = append(section.Properties, &Property{ Key: match[1], Value: match[2], }) } } return file, nil } func Load(r io.Reader) (*File, error) { lines := utio.Lines() if err := lines.Decode(r); err != nil { return nil, err } return load(lines.Lines) } func LoadFile(path string) (*File, error) { var err error var file *File utio.ReadFile(path, func(r io.Reader) error { file, err = Load(r) return err }) return file, err } func loadMap(file *File, err error, v interface{}) error { if err != nil { return err } return MapFromFile(file, v) } func LoadMap(r io.Reader, v interface{}) error { file, err := Load(r) return loadMap(file, err, v) } func LoadMapFile(path string, v interface{}) error { file, err := LoadFile(path) return loadMap(file, err, v) } func mapFileToStruct(ptr reflect.Value, file *File, prefix string) error { if ptr.Type().Kind() != reflect.Ptr { return errStructPtrExpected } value := ptr.Elem() if value.Type().Kind() != reflect.Struct { return errStructPtrExpected } valueTyp := value.Type() n := valueTyp.NumField() for i := 0; i < n; i++ { field := valueTyp.Field(i) name := appendToPrefix(prefix, field.Name) kind := field.Type.Kind() fieldValue := value.FieldByName(field.Name) switch { case kind == reflect.Struct: if err := mapFileToStruct(fieldValue.Addr(), file, name); err != nil { return err } case kind == reflect.Bool: if err := mapPropertyToField(file, prefix, field, fieldValue, setBoolValue); err != nil { return err } case kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64: if err := mapPropertyToField(file, prefix, field, fieldValue, setIntValue); err != nil { return err } case kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64: if err := mapPropertyToField(file, prefix, field, fieldValue, setUintValue); err != nil { return err } case kind == reflect.String: if err := mapPropertyToField(file, prefix, field, fieldValue, setStringValue); err != nil { return err } } } return nil } func MapFromFile(file *File, v interface{}) error { ptr := reflect.ValueOf(v) return mapFileToStruct(ptr, file, "") } func mapPropertyToField(file *File, prefix string, field reflect.StructField, value reflect.Value, set setValueFn) error { property := propertyValueForField(file, prefix, field) if property == "" { return nil } err := set(value, iniTag(field), property) if err != nil { return fmt.Errorf(`failed to map property "%s"; error: %v`, appendToPrefix(prefix, field.Name), err) } return nil } func propertyValueForField(file *File, prefix string, field reflect.StructField) string { sectionKey := camelCaseToSnakeCase(prefix) section := file.Sections[sectionKey] if section == nil { return defaultTagValue(iniTag(field)) } key := camelCaseToSnakeCase(field.Name) property := section.PropertyByKey(key) if property == nil { return defaultTagValue(iniTag(field)) } return property.Value } func setBoolValue(value reflect.Value, tag string, property string) error { states := statesTagValue(tag) p := strings.ToLower(property) if p == states[0] { value.SetBool(false) } else if p == states[1] { value.SetBool(true) } else { return fmt.Errorf(`failed to convert value "%s" to its boolean value, expected either "%s" or "%s"`, property, states[0], states[1]) } return nil } func setIntValue(value reflect.Value, _ string, property string) error { i, err := strconv.ParseInt(property, 10, 64) if err != nil { return fmt.Errorf(`failed to convert value "%s" to an integer`, property) } value.SetInt(i) return nil } func setStringValue(value reflect.Value, _ string, property string) error { n := len(property) if n > 1 { delimiter := property[0] if (delimiter == '"' || delimiter == '\'' || delimiter == '`') && property[n-1] == delimiter { property = property[1 : n-1] } } value.SetString(property) return nil } func setUintValue(value reflect.Value, _ string, property string) error { i, err := strconv.ParseUint(property, 10, 64) if err != nil { return fmt.Errorf(`failed to convert value "%s" to an unsigned integer`, property) } value.SetUint(i) return nil } type setValueFn func(value reflect.Value, tag, property string) error