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
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 | } |