From 619e1e2a0a03d619bceb6323f8bacf2bf4f0349a Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 15 Apr 2016 16:18:46 -0600 Subject: [PATCH] feat(chart): add values parser This adds code to parse TOML files into Values maps. These can then easily be passed into the template engine. Included in this is support for TOML "tables", subsections of TOML files. We will be using those to pass config data to dependent charts. --- glide.lock | 8 +- glide.yaml | 1 + pkg/chart/testdata/coleridge.toml | 11 +++ pkg/chart/values.go | 67 +++++++++++++++ pkg/chart/values_test.go | 134 ++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 pkg/chart/testdata/coleridge.toml create mode 100644 pkg/chart/values.go create mode 100644 pkg/chart/values_test.go diff --git a/glide.lock b/glide.lock index 795c132ac..71ba5d45c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,10 @@ -hash: 7f9a27ad54a10edaa7c57521246676477d0f84ef4246524bae75f8df9d049983 -updated: 2016-04-14T12:24:49.130995956-06:00 +hash: e7c99013acb06eb359cf20390579af9a4553ef0fbed3f7bbb784b4ab7c8df807 +updated: 2016-04-15T15:15:21.87772545-06:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 +- name: github.com/BurntSushi/toml + version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f - name: github.com/codegangsta/cli version: 71f57d300dd6a780ac1856c005c4b518cfd498ec - name: github.com/golang/protobuf @@ -28,7 +30,7 @@ imports: - http2/hpack - internal/timeseries - name: google.golang.org/grpc - version: 9ac074585f926c8506b6351bfdc396d2b19b1cb1 + version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5 subpackages: - codes - credentials diff --git a/glide.yaml b/glide.yaml index a47f5a981..2b1531d12 100644 --- a/glide.yaml +++ b/glide.yaml @@ -12,3 +12,4 @@ import: - package: gopkg.in/yaml.v2 - package: github.com/Masterminds/semver version: 1.1.0 +- package: github.com/BurntSushi/toml diff --git a/pkg/chart/testdata/coleridge.toml b/pkg/chart/testdata/coleridge.toml new file mode 100644 index 000000000..bd16a8c84 --- /dev/null +++ b/pkg/chart/testdata/coleridge.toml @@ -0,0 +1,11 @@ +poet = "Coleridge" +title = "Rime of the Ancient Mariner" +stanza = ["at", "length", "did", "cross", "an", "Albatross"] + +[mariner] +with = "crossbow" +shot = "ALBATROSS" + +[water.water] +where = "everywhere" +nor = "any drop to drink" diff --git a/pkg/chart/values.go b/pkg/chart/values.go new file mode 100644 index 000000000..8c1c1200d --- /dev/null +++ b/pkg/chart/values.go @@ -0,0 +1,67 @@ +package chart + +import ( + "errors" + "io/ioutil" + "strings" + + "github.com/BurntSushi/toml" +) + +var ErrNoTable = errors.New("no table") + +type Values map[string]interface{} + +// Table gets a table (TOML subsection) from a Values object. +// +// The table is returned as a Values. +// +// Compound table names may be specified with dots: +// +// foo.bar +// +// The above will be evaluated as "The table bar inside the table +// foo". +// +// An ErrNoTable is returned if the table does not exist. +func (v Values) Table(name string) (Values, error) { + names := strings.Split(name, ".") + table := v + var err error + + for _, n := range names { + table, err = tableLookup(table, n) + if err != nil { + return table, err + } + } + return table, err +} + +func tableLookup(v Values, simple string) (Values, error) { + v2, ok := v[simple] + if !ok { + return v, ErrNoTable + } + vv, ok := v2.(map[string]interface{}) + if !ok { + return vv, ErrNoTable + } + return vv, nil +} + +// ReadValues will parse TOML byte data into a Values. +func ReadValues(data []byte) (Values, error) { + out := map[string]interface{}{} + err := toml.Unmarshal(data, out) + return out, err +} + +// ReadValuesFile will parse a TOML file into a Values. +func ReadValuesFile(filename string) (Values, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return map[string]interface{}{}, err + } + return ReadValues(data) +} diff --git a/pkg/chart/values_test.go b/pkg/chart/values_test.go new file mode 100644 index 000000000..ecb6b3df9 --- /dev/null +++ b/pkg/chart/values_test.go @@ -0,0 +1,134 @@ +package chart + +import ( + "bytes" + "fmt" + "testing" + "text/template" +) + +func TestReadValues(t *testing.T) { + doc := `# Test TOML parse +poet = "Coleridge" +title = "Rime of the Ancient Mariner" +stanza = ["at", "length", "did", "cross", "an", "Albatross"] + +[mariner] +with = "crossbow" +shot = "ALBATROSS" + +[water.water] +where = "everywhere" +nor = "any drop to drink" +` + + data, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Error parsing bytes: %s", err) + } + matchValues(t, data) +} + +func TestReadValuesFile(t *testing.T) { + data, err := ReadValuesFile("./testdata/coleridge.toml") + if err != nil { + t.Fatalf("Error reading TOML file: %s", err) + } + matchValues(t, data) +} + +func ExampleValues() { + doc := `title="Moby Dick" +[chapter.one] +title = "Loomings" + +[chapter.two] +title = "The Carpet-Bag" + +[chapter.three] +title = "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + panic(err) + } + ch1, err := d.Table("chapter.one") + if err != nil { + panic("could not find chapter one") + } + fmt.Print(ch1["title"]) + // Output: + // Loomings +} + +func TestTable(t *testing.T) { + doc := `title="Moby Dick" +[chapter.one] +title = "Loomings" + +[chapter.two] +title = "The Carpet-Bag" + +[chapter.three] +title = "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Failed to parse the White Whale: %s", err) + } + + if _, err := d.Table("title"); err == nil { + t.Fatalf("Title is not a table.") + } + + if _, err := d.Table("chapter"); err != nil { + t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) + } + + if v, err := d.Table("chapter.one"); err != nil { + t.Errorf("Failed to get chapter.one: %s", err) + } else if v["title"] != "Loomings" { + t.Errorf("Unexpected title: %s", v["title"]) + } + + if _, err := d.Table("chapter.three"); err != nil { + t.Errorf("Chapter three is missing: %s\n%v", err, d) + } + + if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { + t.Errorf("I think you mean 'Epilogue'") + } +} + +func matchValues(t *testing.T, data map[string]interface{}) { + if data["poet"] != "Coleridge" { + t.Errorf("Unexpected poet: %s", data["poet"]) + } + + if o, err := ttpl("{{len .stanza}}", data); err != nil { + t.Errorf("len stanza: %s", err) + } else if o != "6" { + t.Errorf("Expected 6, got %s", o) + } + + if o, err := ttpl("{{.mariner.shot}}", data); err != nil { + t.Errorf(".mariner.shot: %s", err) + } else if o != "ALBATROSS" { + t.Errorf("Expected that mariner shot ALBATROSS") + } + + if o, err := ttpl("{{.water.water.where}}", data); err != nil { + t.Errorf(".water.water.where: %s", err) + } else if o != "everywhere" { + t.Errorf("Expected water water everywhere") + } +} + +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + if err := tt.Execute(&b, v); err != nil { + return "", err + } + return b.String(), nil +}