Added simple INI file serializer (utini).
This commit is contained in:
parent
942bf31ab6
commit
2f614c777d
56
utini/file.go
Normal file
56
utini/file.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Comment string
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Sections map[string]*Section
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) CreateIfNotExists(name string) *Section {
|
||||||
|
if f.Sections == nil {
|
||||||
|
f.Sections = map[string]*Section{}
|
||||||
|
}
|
||||||
|
section := f.Sections[name]
|
||||||
|
if section == nil {
|
||||||
|
section = &Section{}
|
||||||
|
f.Sections[name] = section
|
||||||
|
}
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
|
||||||
|
type Property struct {
|
||||||
|
Key, Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
Properties []*Property
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Section) PropertyByKey(key string) *Property {
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
for _, p := range s.Properties {
|
||||||
|
if strings.ToLower(p.Key) == key {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Section) Keys() []string {
|
||||||
|
keys := map[string]struct{}{}
|
||||||
|
for _, property := range s.Properties {
|
||||||
|
keys[property.Key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted []string
|
||||||
|
for key := range keys {
|
||||||
|
sorted = append(sorted, key)
|
||||||
|
}
|
||||||
|
sort.Strings(sorted)
|
||||||
|
return sorted
|
||||||
|
}
|
214
utini/load.go
Normal file
214
utini/load.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
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
|
97
utini/load_test.go
Normal file
97
utini/load_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"opslag.de/schobers/ut/utio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssertLoadMapString(t *testing.T, s string, v interface{}) {
|
||||||
|
file := AssertLoadString(t, s)
|
||||||
|
err := MapFromFile(file, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertLoadString(t *testing.T, s string) *File {
|
||||||
|
lines := utio.Lines()
|
||||||
|
utio.DecodeFromString(s, lines)
|
||||||
|
file, err := load(lines.Lines)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
type testData struct {
|
||||||
|
NoSection string
|
||||||
|
|
||||||
|
Primitives testDataSectionPrimitives
|
||||||
|
OnlySub testDataSectionOnlySub
|
||||||
|
|
||||||
|
NoSection2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDataSectionPrimitives struct {
|
||||||
|
DelimitedString string
|
||||||
|
Integer int
|
||||||
|
SingleQuotes string
|
||||||
|
UnsignedByte uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDataSectionOnlySub struct {
|
||||||
|
Sub testDataSectionSub
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDataSectionSub struct {
|
||||||
|
Test string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDataDefault struct {
|
||||||
|
Integer int `ini:"default=12345678"`
|
||||||
|
String string `ini:"default=default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var testData1IniFile = `no_section = "no section"
|
||||||
|
no_section2 = "no section 2"
|
||||||
|
|
||||||
|
[only_sub.sub]
|
||||||
|
test = "value"
|
||||||
|
|
||||||
|
[primitives]
|
||||||
|
delimited_string = "value"
|
||||||
|
integer = 12345678
|
||||||
|
single_quotes = 'single quote delimited"'
|
||||||
|
unsigned_byte = 101
|
||||||
|
`
|
||||||
|
|
||||||
|
var testData1Struct = testData{
|
||||||
|
NoSection: "no section",
|
||||||
|
NoSection2: "no section 2",
|
||||||
|
Primitives: testDataSectionPrimitives{
|
||||||
|
DelimitedString: "value",
|
||||||
|
Integer: 12345678,
|
||||||
|
SingleQuotes: "single quote delimited\"",
|
||||||
|
UnsignedByte: 101,
|
||||||
|
},
|
||||||
|
OnlySub: testDataSectionOnlySub{
|
||||||
|
Sub: testDataSectionSub{
|
||||||
|
Test: "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapFromFile(t *testing.T) {
|
||||||
|
file := AssertLoadString(t, testData1IniFile)
|
||||||
|
|
||||||
|
data := &testData{}
|
||||||
|
err := MapFromFile(file, data)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, testData1Struct, *data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultValues(t *testing.T) {
|
||||||
|
data := &testDataDefault{}
|
||||||
|
AssertLoadMapString(t, ``, data)
|
||||||
|
|
||||||
|
assert.Equal(t, 12345678, data.Integer)
|
||||||
|
assert.Equal(t, `default`, data.String)
|
||||||
|
}
|
86
utini/names.go
Normal file
86
utini/names.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type characterCasing int
|
||||||
|
|
||||||
|
const (
|
||||||
|
undefinedCase characterCasing = iota
|
||||||
|
lowerCase
|
||||||
|
upperCase
|
||||||
|
)
|
||||||
|
|
||||||
|
func camelCaseToSnakeCase(name string) string {
|
||||||
|
segments := strings.Split(name, ".")
|
||||||
|
if len(segments) > 1 {
|
||||||
|
for i, segment := range segments {
|
||||||
|
segments[i] = camelCaseToSnakeCase(segment)
|
||||||
|
}
|
||||||
|
return strings.Join(segments, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
var words []string
|
||||||
|
runes := []rune(name)
|
||||||
|
if len(runes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
n := len(runes)
|
||||||
|
end := n
|
||||||
|
casing := undefinedCase
|
||||||
|
for i := range runes {
|
||||||
|
j := n - i - 1
|
||||||
|
r := runes[j]
|
||||||
|
|
||||||
|
if unicode.IsLower(r) {
|
||||||
|
if casing == upperCase {
|
||||||
|
words = append(words, strings.ToLower(string(runes[j+1:end])))
|
||||||
|
end = j + 1
|
||||||
|
casing = undefinedCase
|
||||||
|
} else {
|
||||||
|
casing = lowerCase
|
||||||
|
}
|
||||||
|
} else if r == '_' {
|
||||||
|
if end-j > 1 {
|
||||||
|
words = append(words, strings.ToLower(string(runes[j+1:end])))
|
||||||
|
end = j
|
||||||
|
}
|
||||||
|
casing = undefinedCase
|
||||||
|
} else if unicode.IsDigit(r) { // acts like either upper or lowercase depending on which character was following.
|
||||||
|
} else {
|
||||||
|
if casing == lowerCase {
|
||||||
|
words = append(words, strings.ToLower(string(runes[j:end])))
|
||||||
|
end = j
|
||||||
|
casing = undefinedCase
|
||||||
|
} else {
|
||||||
|
casing = upperCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end > 0 {
|
||||||
|
words = append(words, strings.ToLower(string(runes[:end])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse order of words
|
||||||
|
last := len(words) - 1
|
||||||
|
for i := 0; i < len(words)/2; i++ {
|
||||||
|
words[i], words[last-i] = words[last-i], words[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(words, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func snakeCaseToCamelCase(name string) string {
|
||||||
|
words := strings.Split(name, `_`)
|
||||||
|
for i, word := range words {
|
||||||
|
runes := []rune(word)
|
||||||
|
first := strings.IndexFunc(word, unicode.IsLetter)
|
||||||
|
if first > -1 {
|
||||||
|
runes[first] = unicode.ToUpper(runes[first])
|
||||||
|
}
|
||||||
|
words[i] = string(runes)
|
||||||
|
}
|
||||||
|
return strings.Join(words, ``)
|
||||||
|
}
|
173
utini/save.go
Normal file
173
utini/save.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"opslag.de/schobers/ut/utio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringDelimiters = []byte{'"', '\'', '`'}
|
||||||
|
|
||||||
|
func getBoolValue(value reflect.Value, tag string) string {
|
||||||
|
states := statesTagValue(tag)
|
||||||
|
if value.Bool() {
|
||||||
|
return states[1]
|
||||||
|
}
|
||||||
|
return states[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntValue(value reflect.Value, _ string) string {
|
||||||
|
i := value.Int()
|
||||||
|
return strconv.FormatInt(i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringValue(value reflect.Value, _ string) string {
|
||||||
|
s := value.String()
|
||||||
|
for _, delimiter := range stringDelimiters {
|
||||||
|
if strings.Contains(s, string([]byte{delimiter})) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%c%s%c", delimiter, s, delimiter)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUintValue(value reflect.Value, _ string) string {
|
||||||
|
i := value.Uint()
|
||||||
|
return strconv.FormatUint(i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
type getValueFn func(reflect.Value, string) string
|
||||||
|
|
||||||
|
func mapFieldToProperty(file *File, prefix string, field reflect.StructField, value reflect.Value, get getValueFn) error {
|
||||||
|
sectionKey := camelCaseToSnakeCase(prefix)
|
||||||
|
section := file.CreateIfNotExists(sectionKey)
|
||||||
|
key := camelCaseToSnakeCase(field.Name)
|
||||||
|
section.Properties = append(section.Properties, &Property{
|
||||||
|
Key: key,
|
||||||
|
Value: get(value, iniTag(field)),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapStructToFile(file *File, ptr reflect.Value, 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 := mapStructToFile(file, fieldValue.Addr(), name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case kind == reflect.Bool:
|
||||||
|
if err := mapFieldToProperty(file, prefix, field, fieldValue, getBoolValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64:
|
||||||
|
if err := mapFieldToProperty(file, prefix, field, fieldValue, getIntValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64:
|
||||||
|
if err := mapFieldToProperty(file, prefix, field, fieldValue, getUintValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case kind == reflect.String:
|
||||||
|
if err := mapFieldToProperty(file, prefix, field, fieldValue, getStringValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapToFile(v interface{}) (*File, error) {
|
||||||
|
ptr := reflect.ValueOf(v)
|
||||||
|
file := &File{}
|
||||||
|
err := mapStructToFile(file, ptr, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(file *File) ([]string, error) {
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
for key := range file.Sections {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
if key != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("[%s]", key))
|
||||||
|
}
|
||||||
|
section := file.Sections[key]
|
||||||
|
propertyKeys := section.Keys()
|
||||||
|
for _, key := range propertyKeys {
|
||||||
|
for _, property := range section.Properties {
|
||||||
|
if property.Key != key {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%s = %s", property.Key, property.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(w io.Writer, file *File) error {
|
||||||
|
lines, err := save(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
liner := &utio.Liner{Lines: lines}
|
||||||
|
return liner.Encode(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveMap(w io.Writer, v interface{}) error {
|
||||||
|
file, err := MapToFile(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Save(w, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveFile(path string, file *File) error {
|
||||||
|
return utio.WriteFile(path, func(w io.Writer) error {
|
||||||
|
return Save(w, file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveFileMap(path string, v interface{}) error {
|
||||||
|
file, err := MapToFile(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return SaveFile(path, file)
|
||||||
|
}
|
38
utini/save_test.go
Normal file
38
utini/save_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"opslag.de/schobers/ut/utio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssertSaveString(t *testing.T, file *File) string {
|
||||||
|
lines, err := save(file)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
s, _ := utio.EncodeToString(&utio.Liner{Lines: lines})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertSaveMapString(t *testing.T, v interface{}) string {
|
||||||
|
file, err := MapToFile(v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
return AssertSaveString(t, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapToFile(t *testing.T) {
|
||||||
|
file, err := MapToFile(&testData1Struct)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
s := AssertSaveString(t, file)
|
||||||
|
|
||||||
|
assert.Equal(t, testData1IniFile, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveDefaultValues tests that default values are NOT applied during saving.
|
||||||
|
func TestSaveDefaultValues(t *testing.T) {
|
||||||
|
s := AssertSaveMapString(t, &testDataDefault{})
|
||||||
|
|
||||||
|
assert.Equal(t, `integer = 0
|
||||||
|
string = ""
|
||||||
|
`, s)
|
||||||
|
}
|
53
utini/tags.go
Normal file
53
utini/tags.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package utini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultTagValue(tag string) string {
|
||||||
|
value, _ := tagValueByName(tag, `default`)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasTag(tag, name string) bool {
|
||||||
|
nameValuePrefix := fmt.Sprintf("%s=", name)
|
||||||
|
values := strings.Split(tag, ",")
|
||||||
|
for _, value := range values {
|
||||||
|
if value == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(value, nameValuePrefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func iniTag(field reflect.StructField) string {
|
||||||
|
return field.Tag.Get(`ini`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statesTagValue(tag string) [2]string {
|
||||||
|
if statesTag, ok := tagValueByName(tag, "states"); ok {
|
||||||
|
s := strings.Split(statesTag, "|")
|
||||||
|
if len(s) == 2 {
|
||||||
|
return [2]string{s[0], s[1]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [2]string{"false", "true"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagValueByName(tag, name string) (string, bool) {
|
||||||
|
nameValuePrefix := fmt.Sprintf("%s=", name)
|
||||||
|
values := strings.Split(tag, ",")
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if !strings.HasPrefix(value, nameValuePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return value[len(nameValuePrefix):], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user