diff --git a/go.mod b/go.mod index a9620c7..d470492 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,10 @@ module github.com/ffddorf/confgen go 1.19 +require github.com/stretchr/testify v1.8.0 + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.4.0 // indirect - github.com/stretchr/testify v1.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5ebad15..5164829 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/interop/edgeos/convert.go b/interop/edgeos/convert.go index 6615e92..570c043 100644 --- a/interop/edgeos/convert.go +++ b/interop/edgeos/convert.go @@ -2,10 +2,13 @@ package edgeos import ( "fmt" - "reflect" + "sort" "strings" ) +// ForceConsistentMapOrdering is used in tests to ensure consistent output +var ForceConsistentMapOrdering = false + type InvalidMapValueTypeError struct { valueType string } @@ -22,39 +25,39 @@ type StringBuilder interface { const indent = " " func ConfigFromMap(out StringBuilder, in map[string]interface{}, depth int) error { - indentDepth := strings.Repeat(indent, depth) - for k, v := range in { - if _, err := out.WriteString(indentDepth); err != nil { - return err - } - if _, err := out.WriteString(k); err != nil { - return err - } - if err := out.WriteByte(' '); err != nil { - return err - } + keys := mapKeys(in) + if ForceConsistentMapOrdering { + sort.Strings(keys) + } + + indentString := strings.Repeat(indent, depth) + for _, k := range keys { + v := in[k] switch t := v.(type) { - case string: - if strings.Contains(t, " ") { - if err := out.WriteByte('"'); err != nil { + case []interface{}: + for _, item := range t { + s, err := primitiveToString(item) + if err != nil { return err } - if _, err := out.WriteString(t); err != nil { + if err := writeKV(out, k, s, indentString); err != nil { return err } - if err := out.WriteByte('"'); err != nil { - return err - } - } else { - if _, err := out.WriteString(t); err != nil { + } + case []string: + for _, item := range t { + if err := writeKV(out, k, item, indentString); err != nil { return err } } - if err := out.WriteByte('\n'); err != nil { + case map[string]interface{}: + if _, err := out.WriteString(indentString); err != nil { return err } - case map[string]interface{}: - if _, err := out.WriteString("{\n"); err != nil { + if _, err := out.WriteString(k); err != nil { + return err + } + if _, err := out.WriteString(" {\n"); err != nil { return err } @@ -62,17 +65,81 @@ func ConfigFromMap(out StringBuilder, in map[string]interface{}, depth int) erro return err } - if _, err := out.WriteString(indentDepth); err != nil { + if _, err := out.WriteString(indentString); err != nil { return err } if _, err := out.WriteString("}\n"); err != nil { return err } + case bool: + if !t { + continue + } + if _, err := out.WriteString(indent); err != nil { + return err + } + if _, err := out.WriteString(k); err != nil { + return err + } + if err := out.WriteByte('\n'); err != nil { + return err + } default: - return InvalidMapValueTypeError{ - valueType: reflect.TypeOf(v).Name(), + s, err := primitiveToString(t) + if err != nil { + return err + } + if err := writeKV(out, k, s, indentString); err != nil { + return err } } } return nil } + +func writeKV(out StringBuilder, k, v string, indent string) error { + quoted := strings.Contains(v, " ") + if _, err := out.WriteString(indent); err != nil { + return err + } + if _, err := out.WriteString(k); err != nil { + return err + } + if err := out.WriteByte(' '); err != nil { + return err + } + if quoted { + if err := out.WriteByte('"'); err != nil { + return err + } + } + if _, err := out.WriteString(v); err != nil { + return err + } + if quoted { + if err := out.WriteByte('"'); err != nil { + return err + } + } + return out.WriteByte('\n') +} + +func primitiveToString(in interface{}) (string, error) { + switch t := in.(type) { + case string: + return t, nil + case uint, int, uint32, int32, uint64, int64, float32, float64: + return fmt.Sprintf("%d", t), nil + } + return "", &InvalidMapValueTypeError{ + valueType: fmt.Sprintf("%T", in), + } +} + +func mapKeys[T any](in map[string]T) []string { + out := make([]string, 0, len(in)) + for key := range in { + out = append(out, key) + } + return out +} diff --git a/interop/edgeos/convert_test.go b/interop/edgeos/convert_test.go index b998dbc..0278cd2 100644 --- a/interop/edgeos/convert_test.go +++ b/interop/edgeos/convert_test.go @@ -1,7 +1,6 @@ package edgeos_test import ( - "fmt" "strings" "testing" @@ -10,28 +9,141 @@ import ( "github.com/stretchr/testify/require" ) +func init() { + edgeos.ForceConsistentMapOrdering = true +} + type SM = map[string]interface{} func TestMapConversion(t *testing.T) { - in := SM{ - "smart-queue internal": SM{ - "download": SM{ - "rate": "39mbit", + testCases := map[string]struct { + in map[string]interface{} + out string + }{ + "smart queue": { + in: SM{ + "smart-queue internal": SM{ + "download": SM{ + "rate": "39mbit", + }, + "wan-interface": "eth1.2", + }, }, - "wan-interface": "eth1.2", - }, - } - - out := &strings.Builder{} - err := edgeos.ConfigFromMap(out, in, 0) - require.NoError(t, err) - - fmt.Println(out.String()) - assert.Equal(t, `smart-queue internal { + out: `smart-queue internal { download { rate 39mbit } wan-interface eth1.2 } -`, out.String()) +`, + }, + "quoted values": { + in: SM{ + "ethernet eth0": SM{ + "description": "Some interface doing something", + }, + }, + out: `ethernet eth0 { + description "Some interface doing something" +} +`, + }, + "multivalue": { + in: SM{ + "interfaces": SM{ + "ethernet eth1": SM{ + "address": []interface{}{"10.1.0.1/16", "fde4:4d90:9ebf::1/64"}, + }, + }, + }, + out: `interfaces { + ethernet eth1 { + address 10.1.0.1/16 + address fde4:4d90:9ebf::1/64 + } +} +`, + }, + "numbers": { + in: SM{ + "protocols": SM{ + "bgp 207871": SM{ + "maximum-paths": SM{ + "ibgp": 2, + }, + }, + }, + }, + out: `protocols { + bgp 207871 { + maximum-paths { + ibgp 2 + } + } +} +`, + }, + "multivalue numbers": { + in: SM{ + "snmp": SM{ + "community public": SM{ + "authorization": "ro", + }, + "contact": "support@freifunk-duesseldorf.de", + "listen-address 2001:678:b7c::3": SM{ + "port": []interface{}{161, "345"}, + }, + }, + }, + out: `snmp { + community public { + authorization ro + } + contact support@freifunk-duesseldorf.de + listen-address 2001:678:b7c::3 { + port 161 + port 345 + } +} +`, + }, + "booleans": { + in: SM{ + "ethernet eth3": SM{ + "disable": true, + "duplex": "auto", + "speed": "auto", + }, + }, + out: `ethernet eth3 { + disable + duplex auto + speed auto +} +`, + }, + "boolean off": { + in: SM{ + "ethernet eth3": SM{ + "disable": false, + "duplex": "auto", + "speed": "auto", + }, + }, + out: `ethernet eth3 { + duplex auto + speed auto +} +`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + out := &strings.Builder{} + err := edgeos.ConfigFromMap(out, tc.in, 0) + require.NoError(t, err) + assert.Equal(t, tc.out, out.String()) + }) + } }