Codebase list golang-github-spf13-viper / 858ffb6
feat(encoding): add Java properties codec Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com> Mark Sagi-Kazar authored 2 years ago Márk Sági-Kazár committed 2 years ago
3 changed file(s) with 220 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 package javaproperties
1
2 import (
3 "bytes"
4 "sort"
5 "strings"
6
7 "github.com/magiconair/properties"
8 "github.com/spf13/cast"
9 )
10
11 // Codec implements the encoding.Encoder and encoding.Decoder interfaces for Java properties encoding.
12 type Codec struct {
13 KeyDelimiter string
14 }
15
16 func (c Codec) Encode(v map[string]interface{}) ([]byte, error) {
17 p := properties.NewProperties()
18
19 flattened := map[string]interface{}{}
20
21 flattened = flattenAndMergeMap(flattened, v, "", c.keyDelimiter())
22
23 keys := make([]string, 0, len(flattened))
24
25 for key := range flattened {
26 keys = append(keys, key)
27 }
28
29 sort.Strings(keys)
30
31 for _, key := range keys {
32 _, _, err := p.Set(key, cast.ToString(flattened[key]))
33 if err != nil {
34 return nil, err
35 }
36 }
37
38 var buf bytes.Buffer
39
40 _, err := p.WriteComment(&buf, "#", properties.UTF8)
41 if err != nil {
42 return nil, err
43 }
44
45 return buf.Bytes(), nil
46 }
47
48 func (c Codec) Decode(b []byte, v map[string]interface{}) error {
49 p, err := properties.Load(b, properties.UTF8)
50 if err != nil {
51 return err
52 }
53
54 for _, key := range p.Keys() {
55 // ignore existence check: we know it's there
56 value, _ := p.Get(key)
57
58 // recursively build nested maps
59 path := strings.Split(key, c.keyDelimiter())
60 lastKey := strings.ToLower(path[len(path)-1])
61 deepestMap := deepSearch(v, path[0:len(path)-1])
62
63 // set innermost value
64 deepestMap[lastKey] = value
65 }
66
67 return nil
68 }
69
70 func (c Codec) keyDelimiter() string {
71 if c.KeyDelimiter == "" {
72 return "."
73 }
74
75 return c.KeyDelimiter
76 }
0 package javaproperties
1
2 import (
3 "reflect"
4 "testing"
5 )
6
7 // original form of the data
8 const original = `# key-value pair
9 key = value
10 map.key = value
11 `
12
13 // encoded form of the data
14 const encoded = `key = value
15 map.key = value
16 `
17
18 // Viper's internal representation
19 var data = map[string]interface{}{
20 "key": "value",
21 "map": map[string]interface{}{
22 "key": "value",
23 },
24 }
25
26 func TestCodec_Encode(t *testing.T) {
27 codec := Codec{}
28
29 b, err := codec.Encode(data)
30 if err != nil {
31 t.Fatal(err)
32 }
33
34 if encoded != string(b) {
35 t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
36 }
37 }
38
39 func TestCodec_Decode(t *testing.T) {
40 t.Run("OK", func(t *testing.T) {
41 codec := Codec{}
42
43 v := map[string]interface{}{}
44
45 err := codec.Decode([]byte(original), v)
46 if err != nil {
47 t.Fatal(err)
48 }
49
50 if !reflect.DeepEqual(data, v) {
51 t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
52 }
53 })
54
55 t.Run("InvalidData", func(t *testing.T) {
56 t.Skip("TODO: needs invalid data example")
57
58 codec := Codec{}
59
60 v := map[string]interface{}{}
61
62 codec.Decode([]byte(``), v)
63
64 if len(v) > 0 {
65 t.Fatalf("expected map to be empty when data is invalid\nactual: %#v", v)
66 }
67 })
68 }
0 package javaproperties
1
2 import (
3 "strings"
4
5 "github.com/spf13/cast"
6 )
7
8 // THIS CODE IS COPIED HERE: IT SHOULD NOT BE MODIFIED
9 // AT SOME POINT IT WILL BE MOVED TO A COMMON PLACE
10 // deepSearch scans deep maps, following the key indexes listed in the
11 // sequence "path".
12 // The last value is expected to be another map, and is returned.
13 //
14 // In case intermediate keys do not exist, or map to a non-map value,
15 // a new map is created and inserted, and the search continues from there:
16 // the initial map "m" may be modified!
17 func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
18 for _, k := range path {
19 m2, ok := m[k]
20 if !ok {
21 // intermediate key does not exist
22 // => create it and continue from there
23 m3 := make(map[string]interface{})
24 m[k] = m3
25 m = m3
26 continue
27 }
28 m3, ok := m2.(map[string]interface{})
29 if !ok {
30 // intermediate key is a value
31 // => replace with a new map
32 m3 = make(map[string]interface{})
33 m[k] = m3
34 }
35 // continue search from here
36 m = m3
37 }
38 return m
39 }
40
41 // flattenAndMergeMap recursively flattens the given map into a new map
42 // Code is based on the function with the same name in tha main package.
43 // TODO: move it to a common place
44 func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
45 if shadow != nil && prefix != "" && shadow[prefix] != nil {
46 // prefix is shadowed => nothing more to flatten
47 return shadow
48 }
49 if shadow == nil {
50 shadow = make(map[string]interface{})
51 }
52
53 var m2 map[string]interface{}
54 if prefix != "" {
55 prefix += delimiter
56 }
57 for k, val := range m {
58 fullKey := prefix + k
59 switch val.(type) {
60 case map[string]interface{}:
61 m2 = val.(map[string]interface{})
62 case map[interface{}]interface{}:
63 m2 = cast.ToStringMap(val)
64 default:
65 // immediate value
66 shadow[strings.ToLower(fullKey)] = val
67 continue
68 }
69 // recursively merge to shadow map
70 shadow = flattenAndMergeMap(shadow, m2, fullKey, delimiter)
71 }
72 return shadow
73 }