diff options
| author | Shulhan <ms@kilabit.info> | 2022-04-18 01:30:40 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-04-18 01:35:59 +0700 |
| commit | f06c0d023a1ddd7b2d384995d975fe303d198ca9 (patch) | |
| tree | 06d3a1d1a9878000012f033a1e1576eb759e6001 | |
| parent | ceba0704c6fcc2c3dd82876f6c45a3b24fe98fd6 (diff) | |
| download | pakakeh.go-f06c0d023a1ddd7b2d384995d975fe303d198ca9.tar.xz | |
lib/ini: implement marshaling and unmarshaling map with struct element
For a field F with type map[K]S `ini:"sec"`, where K is string and S is
a struct or pointer to struct element, marshaling the field F will
result in the following ini format,
[sec "K"]
<S.Field.Tag> = <S.Field.Value>
Each field in struct S unmarshaled normally as "key = value".
This rule is also applied when unmarshalling from ini text into map[K]V.
This implementation allow multiple section with dynamic subsections as
key.
| -rw-r--r-- | lib/ini/doc.go | 41 | ||||
| -rw-r--r-- | lib/ini/ini.go | 39 | ||||
| -rw-r--r-- | lib/ini/ini_example_test.go | 192 | ||||
| -rw-r--r-- | lib/ini/ini_unmarshal.go | 173 | ||||
| -rw-r--r-- | lib/ini/tag_struct_field.go | 10 | ||||
| -rw-r--r-- | lib/ini/tag_struct_field_test.go | 8 |
6 files changed, 336 insertions, 127 deletions
diff --git a/lib/ini/doc.go b/lib/ini/doc.go index 3f55e337..d7cf62e3 100644 --- a/lib/ini/doc.go +++ b/lib/ini/doc.go @@ -2,31 +2,30 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// // Package ini implement reading and writing INI text format as defined by // Git configuration file syntax. // -// Features +// # Features // // * Reading and writing on the same file should not change the content of // file (including comment). // // * Template friendly, through Val(), Vals(), and Subs(). // -// Unsupported features +// # Unsupported features // // Git "include" and "includeIf" directives. // // In Git specification, an empty variable is equal to boolean true. This // cause inconsistency between empty string and boolean true. // -// Syntax +// # Syntax // // The '#' and ';' characters begin comments to the end of line. // // Blank lines are ignored. // -// Section +// # Section // // A section begins with the name of the section in square brackets. // @@ -50,7 +49,7 @@ // section header) are recognized as setting variables, in the form // `name = value`. // -// Subsection +// # Subsection // // To begin a subsection put its name in double quotes, separated by // space from the section name, in the section header, for example @@ -67,7 +66,7 @@ // reading subsection name; for example, `\t` is read as `t` and `\0` is read // as `0`. // -// Variable +// # Variable // // Variable name must start with an alphabetic character. // @@ -80,7 +79,7 @@ // This ini library add extension to allow dot ('.') and underscore ('_') // characters on variable name. // -// Value +// # Value // // Value can be empty or not set. // (EXT) Variable name without value is a short-hand to set the value to the @@ -89,13 +88,12 @@ // [section] // thisisempty # equal to thisisempty= // -// // Internal whitespaces within the value are retained verbatim. // Leading and trailing whitespaces on value without double quote will // be discarded. // -// key = multiple strings # equal to "multiple strings" -// key = " multiple strings " # equal to " multiple strings " +// key = multiple strings # equal to "multiple strings" +// key = " multiple strings " # equal to " multiple strings " // // Value can be continued to the next line by ending it with a backslash '\' // character, the backquote and the end-of-line are stripped. @@ -120,7 +118,7 @@ // Other char escape sequences (including octal escape sequences) are // invalid. // -// Marshaling +// # Marshaling // // The container to be passed when marshaling must be struct type. // Each exported field in the struct with "ini" tags will be marshaled based @@ -156,6 +154,7 @@ // SliceStruct []U `ini:"slice:struct" // Map map[string]int `ini:"amap:" // MapSub map[string]string `ini:"amap:sub" +// MapStruct map[string]U `ini:"mapstruct"` // } // // will be marshaled into @@ -188,12 +187,24 @@ // ... // <T.MapSub.Key[n]> = <T.MapSub.Value[n]> // -// Unmarshaling +// ## On map[string]struct, each key become a subsection. +// [mapstruct "<map.key[0]>"] +// <U.Field[0]> = <U.Value[0]> +// ... +// <U.Field[n]> = <U.Value[n]> +// +// ... +// +// [mapstruct "<map.key[n]>"] +// <U.Field[0]> = <U.Value[0]> +// ... +// <U.Field[n]> = <U.Value[n]> +// +// # Unmarshaling // // The syntax and rules for unmarshaling is equal to the marshaling. // -// References +// # References // // https://git-scm.com/docs/git-config#_configuration_file -// package ini diff --git a/lib/ini/ini.go b/lib/ini/ini.go index fdaf4abc..7094b6ab 100644 --- a/lib/ini/ini.go +++ b/lib/ini/ini.go @@ -191,20 +191,39 @@ func (in *Ini) marshalStruct( } case reflect.Map: - amap := map[string]string{} - keys := make([]string, 0) - iter := fvalue.MapRange() + var ( + amap = map[string]reflect.Value{} + keys = make([]string, 0) + iter = fvalue.MapRange() + + mapKey reflect.Value + mapValue reflect.Value + valueType reflect.Type + key string + ) + // Collect all the map keys and sort it to make the + // output consistent. for iter.Next() { - mk := iter.Key() - mv := iter.Value() - key = strings.ToLower(fmt.Sprintf("%v", mk)) - value = fmt.Sprintf("%v", mv) - amap[key] = value + mapKey = iter.Key() + mapValue = iter.Value() + key = strings.ToLower(fmt.Sprintf("%v", mapKey)) keys = append(keys, key) + amap[key] = mapValue } sort.Strings(keys) for _, key = range keys { - in.Set(sec, sub, key, amap[key]) + mapValue = amap[key] + valueType = reflect.TypeOf(mapValue.Interface()) + for valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + mapValue = mapValue.Elem() + } + if valueType.Kind() == reflect.Struct { + in.marshalStruct(valueType, mapValue, sec, key) + } else { + value = fmt.Sprintf("%v", mapValue) + in.Set(sec, sub, key, value) + } } case reflect.Ptr: @@ -280,7 +299,7 @@ func (in *Ini) Unmarshal(v interface{}) (err error) { return fmt.Errorf("ini: Unmarshal: expecting pointer to struct, got %v", kind) } - tagField := unpackStruct(rtipe, rvalue) + tagField := unpackTagStructField(rtipe, rvalue) in.unmarshal(tagField, rtipe, rvalue) return nil diff --git a/lib/ini/ini_example_test.go b/lib/ini/ini_example_test.go index 0b7f11b5..d3b64143 100644 --- a/lib/ini/ini_example_test.go +++ b/lib/ini/ini_example_test.go @@ -176,9 +176,6 @@ func ExampleMarshal() { PtrTime *time.Time `ini:"section:pointer:time" layout:"2006-01-02 15:04:05"` PtrStruct *U `ini:"pointer:struct"` - MapString map[string]string `ini:"map:string"` - MapInt map[string]int `ini:"map:int"` - String string `ini:"section::string"` SliceString []string `ini:"section:slice:string"` @@ -207,13 +204,6 @@ func ExampleMarshal() { Int: 3, }, - MapString: map[string]string{ - "k": "v", - }, - MapInt: map[string]int{ - "keyInt": 6, - }, - String: "a", SliceString: []string{"c", "d"}, @@ -263,12 +253,6 @@ func ExampleMarshal() { // string = PtrStruct.String // int = 3 // - // [map "string"] - // k = v - // - // [map "int"] - // keyint = 6 - // // [section "slice"] // string = c // string = d @@ -292,6 +276,108 @@ func ExampleMarshal() { // int = 2 } +func ExampleMarshal_map() { + type U struct { + String string `ini:"string"` + Int int `ini:"int"` + } + type ADT struct { + MapString map[string]string `ini:"map:subString"` + MapPtrString map[string]*string `ini:"map:subPtrString"` + + MapInt map[string]int `ini:"map:subInt"` + MapPtrInt map[string]*int `ini:"map:subPtrInt"` + + MapStruct map[string]U `ini:"mapStruct"` + MapPtrStruct map[string]*U `ini:"mapPtrStruct"` + } + + var ( + stringV = "v" + stringV2 = "v2" + intV = 6 + + t = ADT{ + MapString: map[string]string{ + "k": "v", + "k2": "v2", + }, + MapPtrString: map[string]*string{ + "k": &stringV, + "k2": &stringV2, + }, + + MapInt: map[string]int{ + "keyInt": 6, + }, + MapPtrInt: map[string]*int{ + "keyInt": &intV, + }, + + MapStruct: map[string]U{ + "struct-key-1": { + String: "struct-1-string", + Int: 1, + }, + "struct-key-2": { + String: "struct-2-string", + Int: 2, + }, + }, + MapPtrStruct: map[string]*U{ + "ptr-struct-key-1": { + String: "struct-1-string", + Int: 1, + }, + "ptr-struct-key-2": { + String: "struct-2-string", + Int: 2, + }, + }, + } + + iniText []byte + err error + ) + + iniText, err = Marshal(&t) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(iniText)) + //Output: + //[map "subString"] + //k = v + //k2 = v2 + // + //[map "subPtrString"] + //k = v + //k2 = v2 + // + //[map "subInt"] + //keyint = 6 + // + //[map "subPtrInt"] + //keyint = 6 + // + //[mapstruct "struct-key-1"] + //string = struct-1-string + //int = 1 + // + //[mapstruct "struct-key-2"] + //string = struct-2-string + //int = 2 + // + //[mapptrstruct "ptr-struct-key-1"] + //string = struct-1-string + //int = 1 + // + //[mapptrstruct "ptr-struct-key-2"] + //string = struct-2-string + //int = 2 +} + func ExampleUnmarshal() { iniText := ` [section] @@ -319,12 +405,6 @@ int = 1 string = U.string 2 int = 2 -[map "string"] -k = v - -[map "int"] -k = 6 - [section "pointer"] string = b int = 2 @@ -343,9 +423,6 @@ int = 2 PtrTime *time.Time `ini:"section:pointer:time" layout:"2006-01-02 15:04:05"` PtrStruct *U `ini:"pointer:struct"` - MapString map[string]string `ini:"map:string"` - MapInt map[string]int `ini:"map:int"` - String string `ini:"section::string"` SliceString []string `ini:"section:slice:string"` @@ -380,8 +457,6 @@ int = 2 fmt.Printf("SliceUint: %v\n", t.SliceUint) fmt.Printf("SliceBool: %v\n", t.SliceBool) fmt.Printf("SliceStruct: %v\n", t.SliceStruct) - fmt.Printf("MapString: %v\n", t.MapString) - fmt.Printf("MapInt: %v\n", t.MapInt) fmt.Printf("PtrString: %v\n", *t.PtrString) fmt.Printf("PtrInt: %v\n", *t.PtrInt) // Output: @@ -395,12 +470,71 @@ int = 2 // SliceUint: [4 5] // SliceBool: [true false] // SliceStruct: [{U.string 1 1} {U.string 2 2}] - // MapString: map[k:v] - // MapInt: map[k:6] // PtrString: b // PtrInt: 2 } +func ExampleUnmarshal_map() { + type U struct { + String string `ini:"string"` + Int int `ini:"int"` + } + + type ADT struct { + MapString map[string]string `ini:"map:string"` + MapInt map[string]int `ini:"map:int"` + MapStruct map[string]U `ini:"mapstruct"` + MapPtrStruct map[string]*U `ini:"mapptrstruct"` + } + + var ( + iniText = ` +[map "string"] +k = v +k2 = v2 + +[map "int"] +k = 6 +k2 = 7 + +[mapstruct "struct-key-1"] +string = struct-1-string +int = 1 + +[mapstruct "struct-key-2"] +string = struct-2-string +int = 2 + +[mapptrstruct "struct-key-1"] +string = struct-1-string +int = 1 + +[mapptrstruct "struct-key-2"] +string = struct-2-string +int = 2 +` + t = ADT{} + err error + ) + + err = Unmarshal([]byte(iniText), &t) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("MapString: %v\n", t.MapString) + fmt.Printf("MapInt: %v\n", t.MapInt) + fmt.Printf("MapStruct: %v\n", t.MapStruct) + fmt.Printf("MapPtrStruct: struct-key-1: %v\n", t.MapPtrStruct["struct-key-1"]) + fmt.Printf("MapPtrStruct: struct-key-2: %v\n", t.MapPtrStruct["struct-key-2"]) + //Output: + //MapString: map[k:v k2:v2] + //MapInt: map[k:6 k2:7] + //MapStruct: map[struct-key-1:{struct-1-string 1} struct-key-2:{struct-2-string 2}] + //MapPtrStruct: struct-key-1: &{struct-1-string 1} + //MapPtrStruct: struct-key-2: &{struct-2-string 2} +} + func ExampleIni_Prune() { input := []byte(` [section] diff --git a/lib/ini/ini_unmarshal.go b/lib/ini/ini_unmarshal.go index c3fc22d1..bb16893f 100644 --- a/lib/ini/ini_unmarshal.go +++ b/lib/ini/ini_unmarshal.go @@ -11,92 +11,141 @@ import ( "time" ) -// // unmarshal set each section-subsection variables into the struct // fields. -// func (in *Ini) unmarshal(tagField tagStructField, rtype reflect.Type, rval reflect.Value) { - for _, sec := range in.secs { - tag := fmt.Sprintf("%s:%s", sec.name, sec.sub) - sfield, ok := tagField[tag] - if ok { - switch sfield.fkind { - case reflect.Map: - // V map[S]T `ini:"section:sub"` - unmarshalToMap(sec, sfield.ftype, sfield.fval) - - case reflect.Ptr: - for sfield.fkind == reflect.Ptr { - sfield.ftype = sfield.ftype.Elem() - sfield.fkind = sfield.ftype.Kind() - } - - if sfield.fkind == reflect.Struct { - if sfield.fval.IsNil() { - ptrfval := reflect.New(sfield.ftype) - sfield.fval.Set(ptrfval) - sfield.fval = ptrfval.Elem() - } else { - sfield.fval = sfield.fval.Elem() + var ( + sec *Section + sfield *structField + v *variable + tag string + ok bool + ) + for _, sec = range in.secs { + // Search field that tagged with subsection first. + tag = fmt.Sprintf("%s:%s", sec.nameLower, sec.sub) + sfield, ok = tagField[tag] + if !ok { + // Search field that tagged with section name only. + tag = sec.nameLower + sfield, ok = tagField[tag] + if !ok { + // Unmarshal each variable in section-sub into + // field directly. + for _, v = range sec.vars { + tag = fmt.Sprintf("%s:%s:%s", sec.nameLower, sec.sub, v.keyLower) + sfield, ok = tagField[tag] + if !ok { + continue } - unmarshalToStruct(sec, sfield.ftype, sfield.fval) + sfield.set(v.value) } + continue + } + } + switch sfield.fkind { + case reflect.Map: + unmarshalToMap(sec, sfield.ftype, sfield.fval) - case reflect.Slice: - sliceElem := sfield.ftype.Elem() - switch sliceElem.Kind() { - case reflect.Struct: - newStruct := reflect.New(sliceElem) - unmarshalToStruct(sec, sliceElem, newStruct.Elem()) - newSlice := reflect.Append(sfield.fval, newStruct.Elem()) - sfield.fval.Set(newSlice) - - case reflect.Ptr: - // V []*T - for sliceElem.Kind() == reflect.Ptr { - sliceElem = sliceElem.Elem() - } + case reflect.Ptr: + for sfield.fkind == reflect.Ptr { + sfield.ftype = sfield.ftype.Elem() + sfield.fkind = sfield.ftype.Kind() + } - if sliceElem.Kind() == reflect.Struct { - ptrfval := reflect.New(sliceElem) - unmarshalToStruct(sec, sliceElem, ptrfval.Elem()) - newSlice := reflect.Append(sfield.fval, ptrfval) - sfield.fval.Set(newSlice) - } + if sfield.fkind == reflect.Struct { + if sfield.fval.IsNil() { + ptrfval := reflect.New(sfield.ftype) + sfield.fval.Set(ptrfval) + sfield.fval = ptrfval.Elem() + } else { + sfield.fval = sfield.fval.Elem() } - - case reflect.Struct: unmarshalToStruct(sec, sfield.ftype, sfield.fval) } - continue - } - for _, v := range sec.vars { - tag = fmt.Sprintf("%s:%s:%s", sec.name, sec.sub, v.keyLower) - sfield, ok = tagField[tag] - if !ok { - continue + case reflect.Slice: + sliceElem := sfield.ftype.Elem() + switch sliceElem.Kind() { + case reflect.Struct: + newStruct := reflect.New(sliceElem) + unmarshalToStruct(sec, sliceElem, newStruct.Elem()) + newSlice := reflect.Append(sfield.fval, newStruct.Elem()) + sfield.fval.Set(newSlice) + + case reflect.Ptr: + // V []*T + for sliceElem.Kind() == reflect.Ptr { + sliceElem = sliceElem.Elem() + } + + if sliceElem.Kind() == reflect.Struct { + ptrfval := reflect.New(sliceElem) + unmarshalToStruct(sec, sliceElem, ptrfval.Elem()) + newSlice := reflect.Append(sfield.fval, ptrfval) + sfield.fval.Set(newSlice) + } } - sfield.set(v.value) + + case reflect.Struct: + unmarshalToStruct(sec, sfield.ftype, sfield.fval) } + } } +// unmarshalToMap unmarshal the Section into a map. +// +// V map[S]T `ini:"section:sub"` for non-struct value or +// V map[S]T `ini:"section"` for map of struct. func unmarshalToMap(sec *Section, rtype reflect.Type, rval reflect.Value) bool { if rtype.Key().Kind() != reflect.String { return false } - amap := reflect.MakeMap(rtype) - mapType := rtype.Elem() + var ( + elType = rtype.Elem() + elKind = elType.Kind() - for _, v := range sec.vars { + v *variable + amap reflect.Value + astruct reflect.Value + mapValue reflect.Value + isPtr bool + ok bool + ) + + if rval.IsNil() { + amap = reflect.MakeMap(rtype) + rval.Set(amap) + } else { + amap = rval + } + for elKind == reflect.Ptr { + elType = elType.Elem() + elKind = elType.Kind() + isPtr = true + } + if elKind == reflect.Struct { + astruct = reflect.New(elType) + + unmarshalToStruct(sec, elType, astruct.Elem()) + if isPtr { + amap.SetMapIndex(reflect.ValueOf(sec.sub), astruct) + } else { + amap.SetMapIndex(reflect.ValueOf(sec.sub), astruct.Elem()) + } + return true + } + + for _, v = range sec.vars { if len(v.keyLower) == 0 { continue } - rval, ok := unmarshalValue(mapType, v.value) + + mapValue, ok = unmarshalValue(elType, v.value) if ok { - amap.SetMapIndex(reflect.ValueOf(v.keyLower), rval) + amap.SetMapIndex(reflect.ValueOf(v.keyLower), mapValue) } } rval.Set(amap) @@ -104,7 +153,7 @@ func unmarshalToMap(sec *Section, rtype reflect.Type, rval reflect.Value) bool { } func unmarshalToStruct(sec *Section, rtype reflect.Type, rval reflect.Value) { - tagField := unpackStruct(rtype, rval) + tagField := unpackTagStructField(rtype, rval) for _, v := range sec.vars { sfield := tagField.getByKey(v.keyLower) if sfield == nil { @@ -114,10 +163,8 @@ func unmarshalToStruct(sec *Section, rtype reflect.Type, rval reflect.Value) { } } -// // unmarshalValue convert the value from string to primitive type based on its // kind. -// func unmarshalValue(rtype reflect.Type, val string) (rval reflect.Value, ok bool) { switch rtype.Kind() { case reflect.Bool: diff --git a/lib/ini/tag_struct_field.go b/lib/ini/tag_struct_field.go index c014b57c..baeaa043 100644 --- a/lib/ini/tag_struct_field.go +++ b/lib/ini/tag_struct_field.go @@ -14,13 +14,11 @@ import ( type tagStructField map[string]*structField // -// unpackStruct read each tags in the struct field and store its section, +// unpackTagStructField read each ini tag in the struct's field and store its section, // subsection, and/or key along with their reflect type and value into // structField. // -// The returned type is map of field name and the field tag. -// -func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) { +func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStructField) { numField := rtype.NumField() if numField == 0 { return nil @@ -42,7 +40,7 @@ func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) { if len(tag) == 0 { switch fkind { case reflect.Struct: - for k, v := range unpackStruct(ftype, fval) { + for k, v := range unpackTagStructField(ftype, fval) { out[k] = v } @@ -54,7 +52,7 @@ func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) { fval = fval.Elem() kind := ftype.Kind() if kind == reflect.Struct { - for k, v := range unpackStruct(ftype, fval) { + for k, v := range unpackTagStructField(ftype, fval) { out[k] = v } } diff --git a/lib/ini/tag_struct_field_test.go b/lib/ini/tag_struct_field_test.go index d9b90c78..ee3cfc83 100644 --- a/lib/ini/tag_struct_field_test.go +++ b/lib/ini/tag_struct_field_test.go @@ -53,7 +53,7 @@ func TestUnpackStruct(t *testing.T) { rval := reflect.ValueOf(v) rtype = rtype.Elem() rval = rval.Elem() - got := unpackStruct(rtype, rval) + got := unpackTagStructField(rtype, rval) exp := []string{ "::int", @@ -73,7 +73,7 @@ func TestUnpackStruct(t *testing.T) { "slice:OfStruct", } - test.Assert(t, "unpackStruct", exp, got.keys()) + test.Assert(t, "unpackTagStructField", exp, got.keys()) } func TestUnpackStruct_embedded(t *testing.T) { @@ -98,7 +98,7 @@ func TestUnpackStruct_embedded(t *testing.T) { rval := reflect.ValueOf(v) rtype = rtype.Elem() rval = rval.Elem() - got := unpackStruct(rtype, rval) + got := unpackTagStructField(rtype, rval) exp := []string{ "a::x", @@ -106,5 +106,5 @@ func TestUnpackStruct_embedded(t *testing.T) { "b::z", "c::xx", } - test.Assert(t, "unpackStruct embedded", exp, got.keys()) + test.Assert(t, "unpackTagStructField: embedded", exp, got.keys()) } |
