Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ValidParser #68

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
Adhere recommendations from [docs](https://godoc.org/github.com/valyala/fastjson).
* Cannot parse JSON from `io.Reader`. There is [Scanner](https://godoc.org/github.com/valyala/fastjson#Scanner)
for parsing stream of JSON values from a string.
* `Parse` will interpret `NaN`, `Inf` and `-Inf` as float64 values if present while `Validate` and `ValidParse` will
return an error. These tokens are not strictly valid according to the JSON spec.


## Usage
Expand Down Expand Up @@ -198,6 +200,29 @@ BenchmarkValidate/twitter/stdjson 500 2849439 ns/op 221.63 MB/s
BenchmarkValidate/twitter/fastjson 2000 1036796 ns/op 609.10 MB/s 0 B/op 0 allocs/op
```

Benchmark results for Parse vs ValidParse vs Validate & Parse:

```
BenchmarkParse/small/fastjson-12 10067671 111.0 ns/op 1712.14 MB/s 0 B/op 0 allocs/op
BenchmarkParse/small/fastjson-validate-parser-12 10503465 109.8 ns/op 1729.86 MB/s 0 B/op 0 allocs/op
BenchmarkParse/small/fastjson-validate-and-parse-12 6553486 176.5 ns/op 1076.23 MB/s 0 B/op 0 allocs/op
BenchmarkParse/medium/fastjson-12 1797662 639.2 ns/op 3643.68 MB/s 0 B/op 0 allocs/op
BenchmarkParse/medium/fastjson-validate-parser-12 1635453 795.7 ns/op 2927.14 MB/s 0 B/op 0 allocs/op
BenchmarkParse/medium/fastjson-validate-and-parse-12 945916 1400 ns/op 1663.97 MB/s 0 B/op 0 allocs/op
BenchmarkParse/large/fastjson-12 121093 9001 ns/op 3123.79 MB/s 0 B/op 0 allocs/op
BenchmarkParse/large/fastjson-validate-parser-12 116598 10946 ns/op 2568.69 MB/s 0 B/op 0 allocs/op
BenchmarkParse/large/fastjson-validate-and-parse-12 57669 20630 ns/op 1362.95 MB/s 0 B/op 0 allocs/op
BenchmarkParse/canada/fastjson-12 1072 1205458 ns/op 1867.39 MB/s 1 B/op 0 allocs/op
BenchmarkParse/canada/fastjson-validate-parser-12 1022 1151216 ns/op 1955.38 MB/s 1 B/op 0 allocs/op
BenchmarkParse/canada/fastjson-validate-and-parse-12 678 1781026 ns/op 1263.91 MB/s 2 B/op 0 allocs/op
BenchmarkParse/citm/fastjson-12 2584 476059 ns/op 3628.13 MB/s 0 B/op 0 allocs/op
BenchmarkParse/citm/fastjson-validate-parser-12 2528 481104 ns/op 3590.09 MB/s 0 B/op 0 allocs/op
BenchmarkParse/citm/fastjson-validate-and-parse-12 1443 844153 ns/op 2046.08 MB/s 1 B/op 0 allocs/op
BenchmarkParse/twitter/fastjson-12 6711 164653 ns/op 3835.42 MB/s 756 B/op 0 allocs/op
BenchmarkParse/twitter/fastjson-validate-parser-12 5918 192253 ns/op 3284.80 MB/s 0 B/op 0 allocs/op
BenchmarkParse/twitter/fastjson-validate-and-parse-12 3758 317174 ns/op 1991.06 MB/s 0 B/op 0 allocs/op
```

## FAQ

* Q: _There are a ton of other high-perf packages for JSON parsing in Go. Why creating yet another package?_
Expand Down
40 changes: 40 additions & 0 deletions handy.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,43 @@ func MustParseBytes(b []byte) *Value {
}
return v
}

// ParseValidate parses while validating json string s.
//
// The function is slower than Parse but faster than running both Validate and Parse.
func ValidParse(s string) (*Value, error) {
var p ValidParser
return p.Parse(s)
}

// MustValidParse parses json string s.
//
// The function panics if s cannot be parsed.
// The function is slower than the Parser.Parse for re-used ValidParser.
func MustValidParse(s string) *Value {
v, err := ValidParse(s)
if err != nil {
panic(err)
}
return v
}

// ParseValidateBytes parses b containing json.
//
// The function is slower than ParseBytes but faster than running both ValidateBytes and ParseBytes.
func ValidParseBytes(b []byte) (*Value, error) {
var p ValidParser
return p.ParseBytes(b)
}

// MustValidParseBytes parses b containing json.
//
// The function panics if b cannot be parsed.
// The function is slower than MustParseBytes but faster than running both ValidateBytes and ParseBytes.
func MustValidParseBytes(b []byte) *Value {
v, err := ValidParseBytes(b)
if err != nil {
panic(err)
}
return v
}
44 changes: 44 additions & 0 deletions handy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,50 @@ func TestMustParse(t *testing.T) {
}
}

func TestValidParse(t *testing.T) {
v, err := ValidParse(`{"foo": "bar"}`)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
str := v.String()
if str != `{"foo":"bar"}` {
t.Fatalf("unexpected value parsed: %q; want %q", str, `{"foo":"bar"}`)
}
}

func TestValidParseBytes(t *testing.T) {
v, err := ValidParseBytes([]byte(`{"foo": "bar"}`))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
str := v.String()
if str != `{"foo":"bar"}` {
t.Fatalf("unexpected value parsed: %q; want %q", str, `{"foo":"bar"}`)
}
}

func TestMustValidParse(t *testing.T) {
s := `{"foo":"bar"}`
v := MustValidParse(s)
str := v.String()
if str != s {
t.Fatalf("unexpected value parsed; %q; want %q", str, s)
}

v = MustValidParseBytes([]byte(s))
if str != s {
t.Fatalf("unexpected value parsed; %q; want %q", str, s)
}

if !causesPanic(func() { v = MustValidParse(`[`) }) {
t.Fatalf("expected MustParse to panic")
}

if !causesPanic(func() { v = MustValidParseBytes([]byte(`[`)) }) {
t.Fatalf("expected MustParse to panic")
}
}

func causesPanic(fn func()) (p bool) {
defer func() {
if r := recover(); r != nil {
Expand Down
8 changes: 8 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,14 @@ func TestParserParse(t *testing.T) {
}

})

t.Run("deeply-nested", func(t *testing.T) {
s := strings.Repeat(`[`, 500) + "null" + strings.Repeat(`]`, 500)
_, err := p.Parse(s)
if err == nil {
t.Fatalf("expected nest depth error")
}
})
}

func TestParseBigObject(t *testing.T) {
Expand Down
47 changes: 47 additions & 0 deletions parser_timing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ func benchmarkParse(b *testing.B, s string) {
b.Run("fastjson-get", func(b *testing.B) {
benchmarkFastJSONParseGet(b, s)
})
b.Run("fastjson-valid-parser", func(b *testing.B) {
benchmarkFastJSONParseValidParser(b, s)
})
b.Run("fastjson-validate-and-parse", func(b *testing.B) {
benchmarkFastJSONParseValidateAndParse(b, s)
})
}

func benchmarkFastJSONParse(b *testing.B, s string) {
Expand Down Expand Up @@ -264,7 +270,48 @@ func benchmarkFastJSONParseGet(b *testing.B, s string) {
})
}

func benchmarkFastJSONParseValidateAndParse(b *testing.B, s string) {
b.ReportAllocs()
b.SetBytes(int64(len(s)))
b.RunParallel(func(pb *testing.PB) {
p := benchPool.Get()
for pb.Next() {
err := Validate(s)
if err != nil {
panic(fmt.Errorf("unexpected error: %s", err))
}
v, err := p.Parse(s)
if err != nil {
panic(fmt.Errorf("unexpected error: %s", err))
}
if v.Type() != TypeObject {
panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject))
}
}
benchPool.Put(p)
})
}

func benchmarkFastJSONParseValidParser(b *testing.B, s string) {
b.ReportAllocs()
b.SetBytes(int64(len(s)))
b.RunParallel(func(pb *testing.PB) {
p := benchPoolValidParse.Get()
for pb.Next() {
v, err := p.Parse(s)
if err != nil {
panic(fmt.Errorf("unexpected error: %s", err))
}
if v.Type() != TypeObject {
panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject))
}
}
benchPoolValidParse.Put(p)
})
}

var benchPool ParserPool
var benchPoolValidParse ValidParserPool

func benchmarkStdJSONParseMap(b *testing.B, s string) {
b.ReportAllocs()
Expand Down
24 changes: 24 additions & 0 deletions pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,27 @@ func (ap *ArenaPool) Get() *Arena {
func (ap *ArenaPool) Put(a *Arena) {
ap.pool.Put(a)
}

// ValidParserPool may be used for pooling ValidParsers for similarly typed JSONs.
type ValidParserPool struct {
pool sync.Pool
}

// Get returns a Parser from pp.
//
// The ValidParser must be Put to pp after use.
func (pp *ValidParserPool) Get() *ValidParser {
v := pp.pool.Get()
if v == nil {
return &ValidParser{}
}
return v.(*ValidParser)
}

// Put returns p to pp.
//
// p and objects recursively returned from p cannot be used after p
// is put into pp.
func (pp *ValidParserPool) Put(p *ValidParser) {
pp.pool.Put(p)
}