From 1bcb2c430f87b749ad119ee3b9e7943e6c3ee3a3 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Mon, 3 Jun 2024 12:19:40 -0500 Subject: [PATCH] feat: ffdhall Signed-off-by: jolheiser --- ff_map.go | 64 +++++++++++++++++++++++++++++++++ ffdhall.go | 72 +++++++++++++++++++++++++++++++++++++ ffdhall_test.go | 56 +++++++++++++++++++++++++++++ go.mod | 13 +++++++ go.sum | 53 +++++++++++++++++++++++++++ testdata/bad.dhall | 1 + testdata/basic.dhall | 1 + testdata/empty.dhall | 1 + testdata/value_arrays.dhall | 6 ++++ 9 files changed, 267 insertions(+) create mode 100644 ff_map.go create mode 100644 ffdhall.go create mode 100644 ffdhall_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 testdata/bad.dhall create mode 100644 testdata/basic.dhall create mode 100644 testdata/empty.dhall create mode 100644 testdata/value_arrays.dhall diff --git a/ff_map.go b/ff_map.go new file mode 100644 index 0000000..ee35efa --- /dev/null +++ b/ff_map.go @@ -0,0 +1,64 @@ +// This is taken from https://github.com/peterbourgon/ff/blob/e267c41b1d149b5151fb637d65f53b6e833448fd/internal/traverse_map.go +package ffdhall + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// traverseMap recursively walks the given map, calling set for each value. If the +// value is a slice, set is called for each element of the slice. The keys of +// nested maps are joined with the given delimiter. +func traverseMap(m map[string]any, delimiter string, set func(name, value string) error) error { + return traverse("", m, delimiter, set) +} + +func traverse(key string, val any, delimiter string, set func(name, value string) error) error { + switch v := val.(type) { + case string: + return set(key, v) + case json.Number: + return set(key, v.String()) + case uint64: + return set(key, strconv.FormatUint(v, 10)) + case int: + return set(key, strconv.Itoa(v)) + case int64: + return set(key, strconv.FormatInt(v, 10)) + case float64: + return set(key, strconv.FormatFloat(v, 'g', -1, 64)) + case bool: + return set(key, strconv.FormatBool(v)) + case nil: + return set(key, "") + case []any: + for _, v := range v { + if err := traverse(key, v, delimiter, set); err != nil { + return err + } + } + case map[string]any: + for k, v := range v { + if key != "" { + k = key + delimiter + k + } + if err := traverse(k, v, delimiter, set); err != nil { + return err + } + } + case map[any]any: + for k, v := range v { + ks := fmt.Sprint(k) + if key != "" { + ks = key + delimiter + ks + } + if err := traverse(ks, v, delimiter, set); err != nil { + return err + } + } + default: + return fmt.Errorf("couldn't convert %q (type %T) to string", val, val) + } + return nil +} diff --git a/ffdhall.go b/ffdhall.go new file mode 100644 index 0000000..f8ba655 --- /dev/null +++ b/ffdhall.go @@ -0,0 +1,72 @@ +package ffdhall + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/philandstuff/dhall-golang/v6" +) + +// DhallParser is a helper function that uses a default DhallParseConfig. +func DhallParser(r io.Reader, set func(name, value string) error) error { + return (&DhallParseConfig{}).Parse(r, set) +} + +// DhallParseConfig collects parameters for the Dhall config file parser. +type DhallParseConfig struct { + // Delimiter is used when concatenating nested node keys into a flag name. + // The default delimiter is ".". + Delimiter string +} + +// Parse a Dhall document from the provided io.Reader, using the provided set +// function to set flag values. Flag names are derived from the node names and +// their key/value pairs. +func (pc *DhallParseConfig) Parse(r io.Reader, set func(name, value string) error) error { + if pc.Delimiter == "" { + pc.Delimiter = "." + } + + data, err := io.ReadAll(r) + if err != nil { + return err + } + + // Compatibility with other parsers + if strings.TrimSpace(string(data)) == "" { + return nil + } + + var m interface{} + if err := dhall.Unmarshal(data, &m); err != nil { + return DhallParseError{Inner: err} + } + mm, ok := m.(map[string]interface{}) + if !ok { + return errors.New("could not unmarshal to map[string]interface{}") + } + + if err := traverseMap(mm, pc.Delimiter, set); err != nil { + return DhallParseError{Inner: err} + } + + return nil +} + +// DhallParseError wraps all errors originating from the DhallParser. +type DhallParseError struct { + Inner error +} + +// Error implenents the error interface. +func (e DhallParseError) Error() string { + return fmt.Sprintf("could not parse Dhall config: %v", e.Inner) +} + +// Unwrap implements the errors.Wrapper interface, allowing errors.Is and +// errors.As to work with DhallParseErrors. +func (e DhallParseError) Unwrap() error { + return e.Inner +} diff --git a/ffdhall_test.go b/ffdhall_test.go new file mode 100644 index 0000000..f1dfbec --- /dev/null +++ b/ffdhall_test.go @@ -0,0 +1,56 @@ +package ffdhall + +import ( + "fmt" + "testing" + "time" + + "github.com/peterbourgon/ff/v3" + "github.com/peterbourgon/ff/v3/fftest" +) + +func TestDhallParser(t *testing.T) { + t.Parallel() + + for _, testcase := range []struct { + name string + args []string + file string + want fftest.Vars + }{ + { + name: "empty input", + args: []string{}, + file: "testdata/empty.dhall", + want: fftest.Vars{}, + }, + { + name: "basic KV pairs", + args: []string{}, + file: "testdata/basic.dhall", + want: fftest.Vars{S: "s", I: 10, B: true, D: 5 * time.Second}, + }, + { + name: "value arrays", + args: []string{}, + file: "testdata/value_arrays.dhall", + want: fftest.Vars{S: "bb", I: 12, B: true, D: 5 * time.Second, X: []string{"a", "B", "👍"}}, + }, + { + name: "bad Dhall file", + args: []string{}, + file: "testdata/bad.dhall", + want: fftest.Vars{WantParseErrorString: "no match found"}, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + fs, vars := fftest.Pair() + vars.ParseError = ff.Parse(fs, testcase.args, + ff.WithConfigFile(testcase.file), + ff.WithConfigFileParser(DhallParser), + ) + fmt.Println(vars.ParseError) + fftest.Compare(t, &testcase.want, vars) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b5ee8fe --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module go.jolheiser.com/ffdhall + +go 1.22.3 + +require ( + github.com/peterbourgon/ff/v3 v3.4.0 + github.com/philandstuff/dhall-golang/v6 v6.0.2 +) + +require ( + github.com/fxamacker/cbor/v2 v2.2.1-0.20200511212021-28e39be4a84f // indirect + github.com/x448/float16 v0.8.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f593b5 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.2.1-0.20200511212021-28e39be4a84f h1:lvGFo/tDOSQ4FKu0d2694s8XyOfAL6FLR9DCD5BIUW4= +github.com/fxamacker/cbor/v2 v2.2.1-0.20200511212021-28e39be4a84f/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406 h1:+OUpk+IVvmKU0jivOVFGtOzA6U5AWFs8HE4DRzWLOUE= +github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/philandstuff/dhall-golang/v6 v6.0.2 h1:jv8fi4ZYiFe6uGrprx6dY7L3xPcgmEqWZo3s8ABCzkw= +github.com/philandstuff/dhall-golang/v6 v6.0.2/go.mod h1:XRoxjsqZM2y7KPFhjV7CSVdWpV5CwuTzGjAY/v+1SUU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/testdata/bad.dhall b/testdata/bad.dhall new file mode 100644 index 0000000..98232c6 --- /dev/null +++ b/testdata/bad.dhall @@ -0,0 +1 @@ +{ diff --git a/testdata/basic.dhall b/testdata/basic.dhall new file mode 100644 index 0000000..6d5f38d --- /dev/null +++ b/testdata/basic.dhall @@ -0,0 +1 @@ +{ s = "s", i = 10, b = True, d = "5s" } diff --git a/testdata/empty.dhall b/testdata/empty.dhall new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/testdata/empty.dhall @@ -0,0 +1 @@ + diff --git a/testdata/value_arrays.dhall b/testdata/value_arrays.dhall new file mode 100644 index 0000000..496e203 --- /dev/null +++ b/testdata/value_arrays.dhall @@ -0,0 +1,6 @@ +{ s = [ "a", "bb" ] +, i = [ "10", "11", "12" ] +, b = [ False, True ] +, d = [ "10m", "5s" ] +, x = [ "a", "B", "👍" ] +}