From 44f022544f6a8439f784dde5c65f4478c7500a7c Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Sun, 28 Jul 2019 14:51:14 +0000 Subject: [PATCH] Restructured the BurntSushi testing code, to not let it be a part of the TOML AST. --- ast/burntsushi.go | 84 --------------------- burntsushi/burntsushi.go | 133 ++++++++++++++++++++++++++++++++++ cmd/burntsushi-tester/main.go | 3 +- parse/testfiles_test.go | 7 +- parse/value_string.go | 8 +- 5 files changed, 144 insertions(+), 91 deletions(-) delete mode 100644 ast/burntsushi.go create mode 100644 burntsushi/burntsushi.go diff --git a/ast/burntsushi.go b/ast/burntsushi.go deleted file mode 100644 index 2430eeb..0000000 --- a/ast/burntsushi.go +++ /dev/null @@ -1,84 +0,0 @@ -package ast - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - "time" -) - -// MakeSushi generates a JSON string for an ast Table, which is compatible -// with BurntSushi's TOML testing tool (https://github.com/BurntSushi/toml-test) -func (t Table) MakeSushi() string { - return MakeSushi(NewValue(TypeTable, t)) -} - -// MakeSushi generates a JSON string for an ast Value, which is compatible -// with BurntSushi's TOML testing tool (https://github.com/BurntSushi/toml-test) -func MakeSushi(value *Value) string { - switch value.Type { - case TypeString: - return renderValue("string", value.Data[0].(string)) - case TypeInteger: - return renderValue("integer", fmt.Sprintf("%d", value.Data[0].(int64))) - case TypeFloat: - return renderValue("float", fmt.Sprintf("%v", value.Data[0].(float64))) - case TypeBool: - return renderValue("bool", fmt.Sprintf("%t", value.Data[0].(bool))) - case TypeOffsetDateTime: - return renderValue("datetime", value.Data[0].(time.Time).Format(time.RFC3339Nano)) - case TypeLocalDateTime: - return renderValue("local_datetime", value.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999")) - case TypeLocalDate: - return renderValue("local_date", value.Data[0].(time.Time).Format("2006-01-02")) - case TypeLocalTime: - return renderValue("local_time", value.Data[0].(time.Time).Format("15:04:05.999999999")) - case TypeArrayOfTables: - fallthrough - case TypeArray: - // BurntSushi's tests sees [ {inline: "table"}, {array: "definitions"} ] - // as an array of tables, so here we accomodate for that situation - // by checking for that case and render such inline array definition - // as if it were an [[array.of.tables]]. - arr := value.Data[0].(*Array) - values := make([]string, 0, arr.Length) - for i := arr.First; i != nil; i = i.Next { - values = append(values, MakeSushi(i.Value)) - } - if arr.ItemType == TypeTable { - return fmt.Sprintf("[%s]", strings.Join(values, ", ")) - } - return fmt.Sprintf(`{"type": "array", "value": [%s]}`, strings.Join(values, ", ")) - case TypeImplicitTable: - fallthrough - case TypeTable: - pairs := value.Data[0].(Table) - keys := make([]string, len(pairs)) - i := 0 - for k := range pairs { - keys[i] = k - i++ - } - sort.Strings(keys) - values := make([]string, len(pairs)) - for i, k := range keys { - values[i] = fmt.Sprintf("%q: %s", k, MakeSushi(pairs[k])) - } - return fmt.Sprintf("{%s}", strings.Join(values, ", ")) - default: - panic(fmt.Sprintf("Unhandled data type: %s", value.Type)) - } -} - -func renderValue(t string, v string) string { - return fmt.Sprintf("{%q: %q, %q: %s}", "type", t, "value", toJSON(v)) -} - -func toJSON(s string) string { - j, err := json.Marshal(s) - if err != nil { - panic(fmt.Sprintf("unable to JSON encode %q: %s", s, err)) - } - return string(j) -} diff --git a/burntsushi/burntsushi.go b/burntsushi/burntsushi.go new file mode 100644 index 0000000..1f7ca9d --- /dev/null +++ b/burntsushi/burntsushi.go @@ -0,0 +1,133 @@ +// Package burntsushi translates a TOML AST into a JSON format, for BurntSushi's testing tool (https://github.com/BurntSushi/toml-test) +package burntsushi + +import ( + "bufio" + "fmt" + "os" + "sort" + "strings" + "time" + + "git.makaay.nl/mauricem/go-toml/ast" +) + +// PrintJSON outputs the JSON string for an ast Table to STDOUT. +func PrintJSON(t ast.Table) { + w := bufio.NewWriter(os.Stdout) + writeSushi(w, ast.NewValue(ast.TypeTable, t)) + w.Flush() +} + +// ToJSON generates the JSON string for an ast Table. +func ToJSON(t ast.Table) string { + sb := &strings.Builder{} + w := bufio.NewWriter(sb) + writeSushi(w, ast.NewValue(ast.TypeTable, t)) + w.Flush() + return sb.String() +} + +func writeSushi(w *bufio.Writer, value *ast.Value) { + switch value.Type { + case ast.TypeString: + w.WriteString(`{"type": "string", "value":"`) + for _, c := range value.Data[0].(string) { + switch c { + default: + w.WriteRune(c) + case '"': + w.WriteString(`\"`) + case '\\': + w.WriteString(`\\`) + case '\b': + w.WriteString(`\b`) + case '\f': + w.WriteString(`\f`) + case '\n': + w.WriteString(`\n`) + case '\r': + w.WriteString(`\r`) + case '\t': + w.WriteString(`\t`) + case '\u0000': + w.WriteString(`\u0000`) + } + } + w.WriteString(`"}`) + case ast.TypeInteger: + renderValue(w, "integer", fmt.Sprintf("%d", value.Data[0].(int64))) + case ast.TypeFloat: + renderValue(w, "float", fmt.Sprintf("%v", value.Data[0].(float64))) + case ast.TypeBool: + if value.Data[0].(bool) { + renderValue(w, "bool", "true") + } else { + renderValue(w, "bool", "false") + } + case ast.TypeOffsetDateTime: + renderValue(w, "datetime", value.Data[0].(time.Time).Format(time.RFC3339Nano)) + case ast.TypeLocalDateTime: + renderValue(w, "local_datetime", value.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999")) + case ast.TypeLocalDate: + renderValue(w, "local_date", value.Data[0].(time.Time).Format("2006-01-02")) + case ast.TypeLocalTime: + renderValue(w, "local_time", value.Data[0].(time.Time).Format("15:04:05.999999999")) + case ast.TypeArrayOfTables: + fallthrough + case ast.TypeArray: + // BurntSushi's tests sees [ {inline: "table"}, {array: "definitions"} ] + // as an array of tables, so here we accomodate for that situation + // by checking for that case and render such inline array definition + // as if it were an [[array.of.tables]]. + arr := value.Data[0].(*ast.Array) + + if arr.ItemType == ast.TypeTable { + w.WriteByte('[') + } else { + w.WriteString(`{"type": "array", "value": [`) + } + for i := arr.First; i != nil; i = i.Next { + if i != arr.First { + w.WriteString(", ") + } + writeSushi(w, i.Value) + } + if arr.ItemType == ast.TypeTable { + w.WriteByte(']') + } else { + w.WriteString(`]}`) + } + case ast.TypeImplicitTable: + fallthrough + case ast.TypeTable: + pairs := value.Data[0].(ast.Table) + keys := make([]string, len(pairs)) + i := 0 + for k := range pairs { + keys[i] = k + i++ + } + sort.Strings(keys) + + w.WriteByte('{') + for i, k := range keys { + if i > 0 { + w.WriteByte(',') + } + w.WriteString(fmt.Sprintf("%q: ", k)) + writeSushi(w, pairs[k]) + } + w.WriteByte('}') + default: + panic(fmt.Sprintf("Unhandled data type: %s", value.Type)) + } +} + +func renderValue(w *bufio.Writer, t string, v string) { + w.WriteString(`{"type":"`) + w.WriteString(t) + w.WriteString(`", "value":"`) + w.WriteString(v) + w.WriteString(`"}`) +} diff --git a/cmd/burntsushi-tester/main.go b/cmd/burntsushi-tester/main.go index 0161db2..1d7e778 100644 --- a/cmd/burntsushi-tester/main.go +++ b/cmd/burntsushi-tester/main.go @@ -12,6 +12,7 @@ import ( "path" "time" + "git.makaay.nl/mauricem/go-toml/burntsushi" "git.makaay.nl/mauricem/go-toml/parse" "github.com/pkg/profile" ) @@ -53,6 +54,6 @@ func main() { if err != nil { log.Fatalf("Error decoding TOML: %s", err) } else { - fmt.Println(toml.MakeSushi()) + burntsushi.PrintJSON(toml) } } diff --git a/parse/testfiles_test.go b/parse/testfiles_test.go index 5f561c5..30e3b7b 100644 --- a/parse/testfiles_test.go +++ b/parse/testfiles_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "git.makaay.nl/mauricem/go-toml/burntsushi" + "git.makaay.nl/mauricem/go-toml/parse" ) @@ -64,8 +66,9 @@ func Test_Valid(t *testing.T) { continue } var actual interface{} - if err := json.Unmarshal([]byte(tomlTable.MakeSushi()), &actual); err != nil { - t.Errorf("[%s] Could not convert parser output to BurntSushi format: %s", name, err) + sushi := burntsushi.ToJSON(tomlTable) + if err := json.Unmarshal([]byte(sushi), &actual); err != nil { + t.Errorf("[%s] Could not JSON decode parser output %q to BurntSushi format: %s", name, sushi, err) fail++ continue } diff --git a/parse/value_string.go b/parse/value_string.go index 436838e..930682b 100644 --- a/parse/value_string.go +++ b/parse/value_string.go @@ -103,7 +103,7 @@ func (t *parser) parseBasicString(name string, p *parse.API) (string, bool) { sb := &strings.Builder{} for { switch { - case p.PeekWithResult(controlCharacter): + case p.Peek(controlCharacter): p.SetError("invalid character in %s: %q (must be escaped)", name, p.Result.Bytes[0]) return sb.String(), false case p.Accept(validEscape): @@ -146,7 +146,7 @@ func (t *parser) parseLiteralString(name string, p *parse.API) (string, bool) { return sb.String(), true case p.Skip(a.Tab): sb.WriteString("\t") - case p.PeekWithResult(controlCharacter): + case p.Peek(controlCharacter): p.SetError("invalid character in %s: %q (no control chars allowed, except for tab)", name, p.Result.Bytes[0]) return sb.String(), false case p.Peek(a.InvalidRune): @@ -194,7 +194,7 @@ func (t *parser) parseMultiLineBasicString(p *parse.API) (string, bool) { switch { case p.Skip(newline): sb.WriteString("\n") - case p.PeekWithResult(controlCharacter): + case p.Peek(controlCharacter): p.SetError("invalid character in multi-line basic string: %q (must be escaped)", p.Result.Bytes[0]) return sb.String(), false case p.Accept(validEscape): @@ -278,7 +278,7 @@ func (t *parser) parseMultiLineLiteralString(p *parse.API) (string, bool) { sb.WriteString("\t") case p.Skip(newline): sb.WriteString("\n") - case p.PeekWithResult(controlCharacter): + case p.Peek(controlCharacter): p.SetError("invalid character in literal string: %q (no control chars allowed, except for tab and newline)", p.Result.Bytes[0]) return sb.String(), false case p.Accept(a.ValidRune):