Skip to content

Commit e26c337

Browse files
authored
Improve json to edgeos conversions (#5)
* Tidy go mod * Fix type output in error message * Force consistent map key ordering * Test more cases * Refactor kv writing * Support multiple values for the same key * Support numbers * Support boolean values
1 parent 3b74cdd commit e26c337

File tree

4 files changed

+225
-46
lines changed

4 files changed

+225
-46
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ module github.com/ffddorf/confgen
22

33
go 1.19
44

5+
require github.com/stretchr/testify v1.8.0
6+
57
require (
68
github.com/davecgh/go-spew v1.1.1 // indirect
79
github.com/pmezard/go-difflib v1.0.0 // indirect
8-
github.com/stretchr/objx v0.4.0 // indirect
9-
github.com/stretchr/testify v1.8.0 // indirect
1010
gopkg.in/yaml.v3 v3.0.1 // indirect
1111
)

go.sum

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
44
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
55
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7-
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
87
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
98
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
109
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
1110
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1212
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1313
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1414
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

interop/edgeos/convert.go

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package edgeos
22

33
import (
44
"fmt"
5-
"reflect"
5+
"sort"
66
"strings"
77
)
88

9+
// ForceConsistentMapOrdering is used in tests to ensure consistent output
10+
var ForceConsistentMapOrdering = false
11+
912
type InvalidMapValueTypeError struct {
1013
valueType string
1114
}
@@ -22,57 +25,121 @@ type StringBuilder interface {
2225
const indent = " "
2326

2427
func ConfigFromMap(out StringBuilder, in map[string]interface{}, depth int) error {
25-
indentDepth := strings.Repeat(indent, depth)
26-
for k, v := range in {
27-
if _, err := out.WriteString(indentDepth); err != nil {
28-
return err
29-
}
30-
if _, err := out.WriteString(k); err != nil {
31-
return err
32-
}
33-
if err := out.WriteByte(' '); err != nil {
34-
return err
35-
}
28+
keys := mapKeys(in)
29+
if ForceConsistentMapOrdering {
30+
sort.Strings(keys)
31+
}
32+
33+
indentString := strings.Repeat(indent, depth)
34+
for _, k := range keys {
35+
v := in[k]
3636
switch t := v.(type) {
37-
case string:
38-
if strings.Contains(t, " ") {
39-
if err := out.WriteByte('"'); err != nil {
37+
case []interface{}:
38+
for _, item := range t {
39+
s, err := primitiveToString(item)
40+
if err != nil {
4041
return err
4142
}
42-
if _, err := out.WriteString(t); err != nil {
43+
if err := writeKV(out, k, s, indentString); err != nil {
4344
return err
4445
}
45-
if err := out.WriteByte('"'); err != nil {
46-
return err
47-
}
48-
} else {
49-
if _, err := out.WriteString(t); err != nil {
46+
}
47+
case []string:
48+
for _, item := range t {
49+
if err := writeKV(out, k, item, indentString); err != nil {
5050
return err
5151
}
5252
}
53-
if err := out.WriteByte('\n'); err != nil {
53+
case map[string]interface{}:
54+
if _, err := out.WriteString(indentString); err != nil {
5455
return err
5556
}
56-
case map[string]interface{}:
57-
if _, err := out.WriteString("{\n"); err != nil {
57+
if _, err := out.WriteString(k); err != nil {
58+
return err
59+
}
60+
if _, err := out.WriteString(" {\n"); err != nil {
5861
return err
5962
}
6063

6164
if err := ConfigFromMap(out, t, depth+1); err != nil {
6265
return err
6366
}
6467

65-
if _, err := out.WriteString(indentDepth); err != nil {
68+
if _, err := out.WriteString(indentString); err != nil {
6669
return err
6770
}
6871
if _, err := out.WriteString("}\n"); err != nil {
6972
return err
7073
}
74+
case bool:
75+
if !t {
76+
continue
77+
}
78+
if _, err := out.WriteString(indent); err != nil {
79+
return err
80+
}
81+
if _, err := out.WriteString(k); err != nil {
82+
return err
83+
}
84+
if err := out.WriteByte('\n'); err != nil {
85+
return err
86+
}
7187
default:
72-
return InvalidMapValueTypeError{
73-
valueType: reflect.TypeOf(v).Name(),
88+
s, err := primitiveToString(t)
89+
if err != nil {
90+
return err
91+
}
92+
if err := writeKV(out, k, s, indentString); err != nil {
93+
return err
7494
}
7595
}
7696
}
7797
return nil
7898
}
99+
100+
func writeKV(out StringBuilder, k, v string, indent string) error {
101+
quoted := strings.Contains(v, " ")
102+
if _, err := out.WriteString(indent); err != nil {
103+
return err
104+
}
105+
if _, err := out.WriteString(k); err != nil {
106+
return err
107+
}
108+
if err := out.WriteByte(' '); err != nil {
109+
return err
110+
}
111+
if quoted {
112+
if err := out.WriteByte('"'); err != nil {
113+
return err
114+
}
115+
}
116+
if _, err := out.WriteString(v); err != nil {
117+
return err
118+
}
119+
if quoted {
120+
if err := out.WriteByte('"'); err != nil {
121+
return err
122+
}
123+
}
124+
return out.WriteByte('\n')
125+
}
126+
127+
func primitiveToString(in interface{}) (string, error) {
128+
switch t := in.(type) {
129+
case string:
130+
return t, nil
131+
case uint, int, uint32, int32, uint64, int64, float32, float64:
132+
return fmt.Sprintf("%d", t), nil
133+
}
134+
return "", &InvalidMapValueTypeError{
135+
valueType: fmt.Sprintf("%T", in),
136+
}
137+
}
138+
139+
func mapKeys[T any](in map[string]T) []string {
140+
out := make([]string, 0, len(in))
141+
for key := range in {
142+
out = append(out, key)
143+
}
144+
return out
145+
}

interop/edgeos/convert_test.go

Lines changed: 128 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package edgeos_test
22

33
import (
4-
"fmt"
54
"strings"
65
"testing"
76

@@ -10,28 +9,141 @@ import (
109
"github.com/stretchr/testify/require"
1110
)
1211

12+
func init() {
13+
edgeos.ForceConsistentMapOrdering = true
14+
}
15+
1316
type SM = map[string]interface{}
1417

1518
func TestMapConversion(t *testing.T) {
16-
in := SM{
17-
"smart-queue internal": SM{
18-
"download": SM{
19-
"rate": "39mbit",
19+
testCases := map[string]struct {
20+
in map[string]interface{}
21+
out string
22+
}{
23+
"smart queue": {
24+
in: SM{
25+
"smart-queue internal": SM{
26+
"download": SM{
27+
"rate": "39mbit",
28+
},
29+
"wan-interface": "eth1.2",
30+
},
2031
},
21-
"wan-interface": "eth1.2",
22-
},
23-
}
24-
25-
out := &strings.Builder{}
26-
err := edgeos.ConfigFromMap(out, in, 0)
27-
require.NoError(t, err)
28-
29-
fmt.Println(out.String())
30-
assert.Equal(t, `smart-queue internal {
32+
out: `smart-queue internal {
3133
download {
3234
rate 39mbit
3335
}
3436
wan-interface eth1.2
3537
}
36-
`, out.String())
38+
`,
39+
},
40+
"quoted values": {
41+
in: SM{
42+
"ethernet eth0": SM{
43+
"description": "Some interface doing something",
44+
},
45+
},
46+
out: `ethernet eth0 {
47+
description "Some interface doing something"
48+
}
49+
`,
50+
},
51+
"multivalue": {
52+
in: SM{
53+
"interfaces": SM{
54+
"ethernet eth1": SM{
55+
"address": []interface{}{"10.1.0.1/16", "fde4:4d90:9ebf::1/64"},
56+
},
57+
},
58+
},
59+
out: `interfaces {
60+
ethernet eth1 {
61+
address 10.1.0.1/16
62+
address fde4:4d90:9ebf::1/64
63+
}
64+
}
65+
`,
66+
},
67+
"numbers": {
68+
in: SM{
69+
"protocols": SM{
70+
"bgp 207871": SM{
71+
"maximum-paths": SM{
72+
"ibgp": 2,
73+
},
74+
},
75+
},
76+
},
77+
out: `protocols {
78+
bgp 207871 {
79+
maximum-paths {
80+
ibgp 2
81+
}
82+
}
83+
}
84+
`,
85+
},
86+
"multivalue numbers": {
87+
in: SM{
88+
"snmp": SM{
89+
"community public": SM{
90+
"authorization": "ro",
91+
},
92+
"contact": "support@freifunk-duesseldorf.de",
93+
"listen-address 2001:678:b7c::3": SM{
94+
"port": []interface{}{161, "345"},
95+
},
96+
},
97+
},
98+
out: `snmp {
99+
community public {
100+
authorization ro
101+
}
102+
contact support@freifunk-duesseldorf.de
103+
listen-address 2001:678:b7c::3 {
104+
port 161
105+
port 345
106+
}
107+
}
108+
`,
109+
},
110+
"booleans": {
111+
in: SM{
112+
"ethernet eth3": SM{
113+
"disable": true,
114+
"duplex": "auto",
115+
"speed": "auto",
116+
},
117+
},
118+
out: `ethernet eth3 {
119+
disable
120+
duplex auto
121+
speed auto
122+
}
123+
`,
124+
},
125+
"boolean off": {
126+
in: SM{
127+
"ethernet eth3": SM{
128+
"disable": false,
129+
"duplex": "auto",
130+
"speed": "auto",
131+
},
132+
},
133+
out: `ethernet eth3 {
134+
duplex auto
135+
speed auto
136+
}
137+
`,
138+
},
139+
}
140+
141+
for name, tc := range testCases {
142+
t.Run(name, func(t *testing.T) {
143+
out := &strings.Builder{}
144+
err := edgeos.ConfigFromMap(out, tc.in, 0)
145+
require.NoError(t, err)
146+
assert.Equal(t, tc.out, out.String())
147+
})
148+
}
37149
}

0 commit comments

Comments
 (0)