From 9d7779e8ca5404f26abbd8cce0314d9cee967bba Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Sun, 6 Aug 2017 19:15:07 +1000 Subject: [PATCH] Add a json parser --- .gitignore | 5 ++- json/json.go | 68 +++++++++++++++++++++++++++++++++++ json/json_test.go | 70 +++++++++++++++++++++++++++++++++++++ json/profile/cpuprofile.bat | 3 ++ json/profile/json.go | 55 +++++++++++++++++++++++++++++ json/profile/memprofile.bat | 3 ++ 6 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 json/json.go create mode 100644 json/json_test.go create mode 100644 json/profile/cpuprofile.bat create mode 100644 json/profile/json.go create mode 100644 json/profile/memprofile.bat diff --git a/.gitignore b/.gitignore index 29b636a..557e1d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea -*.iml \ No newline at end of file +*.iml +*.exe +cpu.out +mem.out diff --git a/json/json.go b/json/json.go new file mode 100644 index 0000000..3aa5163 --- /dev/null +++ b/json/json.go @@ -0,0 +1,68 @@ +package json + +import ( + "errors" + + . "github.com/vektah/goparsify" +) + +var ( + value Parser + + array = Map(And(WS, "[", Kleene(&value, And(WS, ",")), "]"), func(n Node) Node { + return n.([]Node)[1].([]Node) + }) + properties = Kleene(And(WS, String('"'), WS, ":", WS, &value), ",") + object = Map(And(WS, "{", WS, properties, WS, "}"), func(n Node) Node { + ret := map[string]interface{}{} + + for _, prop := range n.([]Node)[1].([]Node) { + vals := prop.([]Node) + if len(vals) == 3 { + ret[vals[0].(string)] = vals[2] + } else { + ret[vals[0].(string)] = nil + } + } + + return ret + }) + + _null = Map(And(WS, "null"), func(n Node) Node { + return nil + }) + + _true = Map(And(WS, "true"), func(n Node) Node { + return true + }) + + _false = Map(And(WS, "false"), func(n Node) Node { + return false + }) + + Y = Map(And(&value, WS), func(n Node) Node { + nodes := n.([]Node) + if len(nodes) > 0 { + return nodes[0] + } + return nil + }) +) + +func init() { + value = Any(_null, _true, _false, String('"'), array, object) +} + +func Unmarshal(input string) (interface{}, error) { + result, remaining, err := ParseString(Y, input) + + if err != nil { + return result, err + } + + if remaining != "" { + return result, errors.New("left unparsed: " + remaining) + } + + return result, err +} diff --git a/json/json_test.go b/json/json_test.go new file mode 100644 index 0000000..7f0ab9f --- /dev/null +++ b/json/json_test.go @@ -0,0 +1,70 @@ +package json + +import ( + "testing" + + "github.com/stretchr/testify/require" + . "github.com/vektah/goparsify" +) + +func TestUnmarshal(t *testing.T) { + t.Run("basic types", func(t *testing.T) { + result, err := Unmarshal(`true`) + require.NoError(t, err) + require.Equal(t, true, result) + + result, err = Unmarshal(`false`) + require.NoError(t, err) + require.Equal(t, false, result) + + result, err = Unmarshal(`null`) + require.NoError(t, err) + require.Equal(t, nil, result) + + result, err = Unmarshal(`"true"`) + require.NoError(t, err) + require.Equal(t, "true", result) + }) + + t.Run("array", func(t *testing.T) { + result, err := Unmarshal(`[true, null, false]`) + require.NoError(t, err) + require.Equal(t, []Node{true, nil, false}, result) + }) + + t.Run("object", func(t *testing.T) { + result, err := Unmarshal(`{"true":true, "false":false, "null": null} `) + require.NoError(t, err) + require.Equal(t, map[string]interface{}{"true": true, "false": false, "null": nil}, result) + }) +} + +const benchmarkString = `{"true":true, "false":false, "null": null}` + +//func BenchmarkUnmarshalParsec(b *testing.B) { +// bytes := []byte(benchmarkString) +// +// for i := 0; i < b.N; i++ { +// scanner := parsecJson.NewJSONScanner(bytes) +// _, remaining := parsecJson.Y(scanner) +// +// require.True(b, remaining.Endof()) +// } +//} + +func BenchmarkUnmarshalParsify(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := Unmarshal(benchmarkString) + require.NoError(b, err) + } +} + +// +//func BenchmarkUnmarshalStdlib(b *testing.B) { +// bytes := []byte(benchmarkString) +// var result interface{} +// for i := 0; i < b.N; i++ { +// err := stdlibJson.Unmarshal(bytes, &result) +// require.NoError(b, err) +// } +//} diff --git a/json/profile/cpuprofile.bat b/json/profile/cpuprofile.bat new file mode 100644 index 0000000..2899eb3 --- /dev/null +++ b/json/profile/cpuprofile.bat @@ -0,0 +1,3 @@ +go build +profile.exe -cpuprofile cpu.out +go tool pprof --inuse_objects profile.exe cpu.out diff --git a/json/profile/json.go b/json/profile/json.go new file mode 100644 index 0000000..b94848b --- /dev/null +++ b/json/profile/json.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "log" + "os" + "runtime" + "runtime/pprof" + + "github.com/vektah/goparsify/json" +) + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + + pprof.StartCPUProfile(f) + + defer func() { + pprof.StopCPUProfile() + err := f.Close() + if err != nil { + panic(err) + } + }() + } + if *memprofile != "" { + runtime.MemProfileRate = 1 + } + + for i := 0; i < 10000; i++ { + _, err := json.Unmarshal(`{"true":true, "false":false, "null": null}`) + if err != nil { + panic(err) + } + } + + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + log.Fatal(err) + } + + pprof.WriteHeapProfile(f) + f.Close() + return + } +} diff --git a/json/profile/memprofile.bat b/json/profile/memprofile.bat new file mode 100644 index 0000000..a9c935e --- /dev/null +++ b/json/profile/memprofile.bat @@ -0,0 +1,3 @@ +go build +profile.exe -memprofile mem.out +go tool pprof --inuse_objects profile.exe mem.out