gojay

high performance JSON encoder/decoder with stream API for Golang
git clone git://git.lair.cx/gojay
Log | Files | Refs | README | LICENSE

commit d6ba85a4c67da14b61d4e7e80620a13f6b9c3e40
Author: francoispqt <francois@parquet.ninja>
Date:   Wed, 25 Apr 2018 22:53:22 +0800

initial commit

Diffstat:
A.gitignore | 5+++++
A.travis.yml | 12++++++++++++
AGopkg.lock | 9+++++++++
AGopkg.toml | 23+++++++++++++++++++++++
ALICENSE | 22++++++++++++++++++++++
AMakefile | 12++++++++++++
AREADME.md | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/benchmarks_large.go | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/benchmarks_medium.go | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/benchmarks_small.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/decoder/.gitignore | 2++
Abenchmarks/decoder/Makefile | 32++++++++++++++++++++++++++++++++
Abenchmarks/decoder/decoder.go | 1+
Abenchmarks/decoder/decoder_bench_large_test.go | 42++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/decoder/decoder_bench_medium_test.go | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/decoder/decoder_bench_small_test.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/decoder/decoder_large_test.go | 25+++++++++++++++++++++++++
Abenchmarks/decoder/decoder_medium_test.go | 18++++++++++++++++++
Abenchmarks/decoder/decoder_small_test.go | 29+++++++++++++++++++++++++++++
Abenchmarks/encoder/.gitignore | 2++
Abenchmarks/encoder/Makefile | 28++++++++++++++++++++++++++++
Abenchmarks/encoder/encoder_bench_large_test.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/encoder/encoder_bench_medium_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/encoder/encoder_bench_small_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Abenchmarks/encoder/encoder_large_test.go | 1+
Abenchmarks/encoder/encoder_medium_test.go | 1+
Abenchmarks/encoder/encoder_small_test.go | 1+
Adecode.go | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_array.go | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_array_test.go | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_bool.go | 41+++++++++++++++++++++++++++++++++++++++++
Adecode_bool_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Adecode_number.go | 592+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_number_test.go | 310+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_object.go | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_object_test.go | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_pool.go | 48++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_stream.go | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_stream_test.go | 375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_string.go | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_string_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Adecode_test.go | 498+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode.go | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_array.go | 30++++++++++++++++++++++++++++++
Aencode_array_test.go | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_bool.go | 30++++++++++++++++++++++++++++++
Aencode_builder.go | 37+++++++++++++++++++++++++++++++++++++
Aencode_interface.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_number.go | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_number_test.go | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_object.go | 40++++++++++++++++++++++++++++++++++++++++
Aencode_object_test.go | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_pool.go | 21+++++++++++++++++++++
Aencode_string.go | 39+++++++++++++++++++++++++++++++++++++++
Aencode_string_test.go | 27+++++++++++++++++++++++++++
Aencode_test.go | 1+
Aerrors.go | 35+++++++++++++++++++++++++++++++++++
Agojay.go | 7+++++++
58 files changed, 5718 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +*.out +*.log +*.test +.vscode +\ No newline at end of file diff --git a/.travis.yml b/.travis.yml @@ -0,0 +1,11 @@ +language: go + +go: + - "1.10.x" + - master + +script: + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) +\ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock @@ -0,0 +1,9 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "d28f6ccb578626a5a92d7cb277cbdf125ab5af3080e1a165b59b8b2068881897" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml @@ -0,0 +1,23 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +ignored = ["github.com/francoispqt/benchmarks*","github.com/stretchr/testify*","github.com/stretchr/testify","github.com/json-iterator/go","github.com/buger/jsonparser"] diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 gojay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +\ No newline at end of file diff --git a/Makefile b/Makefile @@ -0,0 +1,11 @@ +.PHONY: test +test: + go test -run=^Test -v + +.PHONY: cover +cover: + go test -coverprofile=coverage.out + +.PHONY: coverhtml +coverhtml: + go tool cover -html=coverage.out +\ No newline at end of file diff --git a/README.md b/README.md @@ -0,0 +1,298 @@ +[![Go doc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square +)](https://godoc.org/github.com/francoispqt/gojay) +![MIT License](https://img.shields.io/badge/license-mit-blue.svg?style=flat-square) + +# GoJay +GoJay is a performant JSON encoder/decoder for Golang (currently the most performant). + +It has a simple API and doesn't use reflection. It relies on small interfaces to decode/encode structures and slices. + +Gojay also comes with powerful stream decoding features. + +# Get started + +```bash +go get github.com/francoispqt/gojay +``` + +## Decoding + +Example of basic stucture decoding: +```go +import "github.com/francoispqt/gojay" + +type user struct { + id int + name string + email string +} +// implement UnmarshalerObject +func (u *user) UnmarshalObject(dec *gojay.Decoder, key string) { + switch k { + case "id": + return dec.AddInt(&u.id) + case "name": + return dec.AddString(&u.name) + case "email": + return dec.AddString(&u.email) + } +} +func (u *user) NKeys() int { + return 3 +} + +func main() { + u := &user{} + d := []byte(`{"id":1,"name":"gojay","email":"gojay@email.com"}`) + err := gojay.UnmarshalObject(d, user) + if err != nil { + log.Fatal(err) + } +} +``` + +### Structs +#### UnmarshalerObject Interface + +To unmarshal a JSON object to a structure, the structure must implement the UnmarshalerObject interface: +```go +type UnmarshalerObject interface { + UnmarshalObject(*Decoder, string) error + NKeys() int +} +``` +UnmarshalObject method takes two arguments, the first one is a pointer to the Decoder (*gojay.Decoder) and the second one is the string value of the current key being parsed. If the JSON data is not an object, the UnmarshalObject method will never be called. + +NKeys method must return the number of keys to Unmarshal in the JSON object. + +Example of implementation: +```go +type user struct { + id int + name string + email string +} +// implement UnmarshalerObject +func (u *user) UnmarshalObject(dec *gojay.Decoder, key string) { + switch k { + case "id": + return dec.AddInt(&u.id) + case "name": + return dec.AddString(&u.name) + case "email": + return dec.AddString(&u.email) + } +} +func (u *user) NKeys() int { + return 3 +} +``` + + +### Arrays, Slices and Channels + +To unmarshal a JSON object to a slice an array or a channel, it must implement the UnmarshalerArray interface: +```go +type UnmarshalerArray interface { + UnmarshalArray(*Decoder) error +} +``` +UnmarshalArray method takes one argument, a pointer to the Decoder (*gojay.Decoder). If the JSON data is not an array, the Unmarshal method will never be called. + +Example of implementation with a slice: +```go +type testSlice []string + +func (t *testStringArr) UnmarshalArray(dec *gojay.Decoder) error { + str := "" + if err := dec.AddString(&str); err != nil { + return err + } + *t = append(*t, str) + return nil +} +``` + +Example of implementation with a channel: +```go +type ChannelString chan string + +func (c *ChannelArray) UnmarshalArray(dec *gojay.Decoder) error { + str := "" + if err := dec.AddString(&str); err != nil { + return err + } + *c <- str + return nil +} +``` + +### Stream Decoding +GoJay ships with a powerful stream decoder. + +It allows to read continuously from an io.Reader stream and do JIT decoding writing unmarshalled JSON to a channel to allow async consuming. + +When using the Stream API, the Decoder implements context.Context to provide graceful cancellation. + +Example: +```go +type ChannelStream chan *TestObj + +func (c *ChannelStream) UnmarshalStream(dec *gojay.StreamDecoder) error { + obj := &TestObj{} + if err := dec.AddObject(obj); err != nil { + return err + } + *c <- obj + return nil +} + +func main() { + // create our channel which will receive our objects + streamChan := ChannelStream(make(chan *TestObj)) + // get a reader implementing io.Reader + reader := getAnIOReaderStream() + dec := gojay.Stream.NewDecoder(reader) + // start decoding (will block the goroutine until something is written to the ReadWriter) + go dec.DecodeStream(&streamChan) + for { + select { + case v := <-streamChan: + // do something with my TestObj + case <-dec.Done(): + os.Exit("finished reading stream") + } + } +} +``` + +### Other types + +## Encoding + +Example of basic structure encoding: +```go +import "github.com/francoispqt/gojay" + +type user struct { + id int + name string + email string +} +// implement UnmarshalerObject +func (u *user) MarshalObject(dec *gojay.Decoder, key string) { + dec.AddIntKey("id", u.id) + dec.AddStringKey("name", u.name) + dec.AddStringKey("email", u.email) +} +func (u *user) IsNil() bool { + return u == nil +} + +func main() { + u := &user{1, "gojay", "gojay@email.com"} + b, _ := gojay.MarshalObject(user) + fmt.Println(string(b)) // {"id":1,"name":"gojay","email":"gojay@email.com"} +} +``` + +### Structs +### Arrays and Slices +### Other types + +# Benchmarks + +Benchmarks encode and decode three different data based on size (small, medium, large). + +To run benchmark for decoder: +```bash +cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/decoder && make bench +``` + +To run benchmark for encoder: +```bash +cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench +``` + +# Benchmark Results +## Decode + +<img src="https://images2.imgbox.com/78/01/49OExcPh_o.png" width="500px"> + +### Small Payload +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/decoder/decoder_bench_small_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_small.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| Std Library | 4661 | 496 | 12 | +| JsonParser | 1313 | 0 | 0 | +| JsonIter | 899 | 192 | 5 | +| GoJay | 662 | 112 | 1 | + +### Medium Payload +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/decoder/decoder_bench_medium_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_medium.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| Std Library | 30148 | 2152 | 496 | +| JsonParser | 7793 | 0 | 0 | +| JsonIter | 5967 | 496 | 44 | +| GoJay | 3914 | 128 | 12 | + +### Large Payload +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/decoder/decoder_bench_large_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_large.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| JsonParser | 66813 | 0 | 0 | +| JsonIter | 87994 | 6738 | 329 | +| GoJay | 43402 | 1408 | 76 | + +## Encode + +<img src="https://images2.imgbox.com/e9/cc/pnM8c7Gf_o.png" width="500px"> + +### Small Struct +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/encoder/encoder_bench_small_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_small.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| Std Library | 1280 | 464 | 3 | +| JsonIter | 866 | 272 | 3 | +| GoJay | 484 | 320 | 2 | +### Medium Struct +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/encoder/encoder_bench_medium_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_medium.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| Std Library | 3325 | 1496 | 18 | +| JsonIter | 1939 | 648 | 16 | +| GoJay | 1196 | 936 | 16 | + +### Large Struct +[benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/encoder/encoder_bench_large_test.go) + +[benchmark data is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/benchmarks_large.go) + +| | ns/op | bytes/op | allocs/op | +|-------------|-------|--------------|-----------| +| Std Library | 51317 | 28704 | 326 | +| JsonIter | 35247 | 14608 | 320 | +| GoJay | 27847 | 27888 | 326 | + +# Contributing + +Contributions are welcome :) + +If you encounter issues please report it in Github and/or send an email at [francois@parquet.ninja](mailto:francois@parquet.ninja) + diff --git a/benchmarks/benchmarks_large.go b/benchmarks/benchmarks_large.go @@ -0,0 +1,170 @@ +package benchmarks + +import ( + "strconv" + + "github.com/francoispqt/gojay" +) + +type DSUser struct { + Username string +} + +func (m *DSUser) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "username": + return dec.AddString(&m.Username) + } + return nil +} +func (m *DSUser) NKeys() int { + return 1 +} +func (m *DSUser) IsNil() bool { + return m == nil +} +func (m *DSUser) MarshalObject(enc *gojay.Encoder) { + enc.AddStringKey("username", m.Username) +} + +type DSTopic struct { + Id int + Slug string +} + +func (m *DSTopic) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "id": + return dec.AddInt(&m.Id) + case "slug": + return dec.AddString(&m.Slug) + } + return nil +} +func (m *DSTopic) NKeys() int { + return 2 +} +func (m *DSTopic) IsNil() bool { + return m == nil +} +func (m *DSTopic) MarshalObject(enc *gojay.Encoder) { + enc.AddIntKey("id", m.Id) + enc.AddStringKey("slug", m.Slug) +} + +type DSTopics []*DSTopic + +func (t *DSTopics) UnmarshalArray(dec *gojay.Decoder) error { + dsTopic := &DSTopic{} + *t = append(*t, dsTopic) + return dec.AddObject(dsTopic) +} + +func (m *DSTopics) MarshalArray(enc *gojay.Encoder) { + for _, e := range *m { + enc.AddObject(e) + } +} + +type DSTopicsList struct { + Topics DSTopics + MoreTopicsUrl string +} + +func (m *DSTopicsList) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "topics": + m.Topics = DSTopics{} + return dec.AddArray(&m.Topics) + case "more_topics_url": + return dec.AddString(&m.MoreTopicsUrl) + } + return nil +} +func (m *DSTopicsList) NKeys() int { + return 2 +} + +func (m *DSTopicsList) IsNil() bool { + return m == nil +} + +func (m *DSTopicsList) MarshalObject(enc *gojay.Encoder) { + enc.AddArrayKey("users", &m.Topics) + enc.AddStringKey("more_topics_url", m.MoreTopicsUrl) +} + +type DSUsers []*DSUser + +func (t *DSUsers) UnmarshalArray(dec *gojay.Decoder) error { + dsUser := DSUser{} + *t = append(*t, &dsUser) + return dec.AddObject(&dsUser) +} + +func (m *DSUsers) MarshalArray(enc *gojay.Encoder) { + for _, e := range *m { + enc.AddObject(e) + } +} + +type LargePayload struct { + Users DSUsers + Topics *DSTopicsList +} + +func (m *LargePayload) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "users": + return dec.AddArray(&m.Users) + case "topics": + m.Topics = &DSTopicsList{} + return dec.AddObject(m.Topics) + } + return nil +} + +func (m *LargePayload) NKeys() int { + return 2 +} + +func (m *LargePayload) MarshalObject(enc *gojay.Encoder) { + enc.AddArrayKey("users", &m.Users) + enc.AddObjectKey("topics", m.Topics) +} + +func (m *LargePayload) IsNil() bool { + return m == nil +} + +var LargeFixture = []byte(` + {"users":[{"id":-1,"username":"system","avatar_template":"/user_avatar/discourse.metabase.com/system/{size}/6_1.png"},{"id":89,"username":"zergot","avatar_template":"https://avatars.discourse.org/v2/letter/z/0ea827/{size}.png"},{"id":1,"username":"sameer","avatar_template":"https://avatars.discourse.org/v2/letter/s/bbce88/{size}.png"},{"id":84,"username":"HenryMirror","avatar_template":"https://avatars.discourse.org/v2/letter/h/ecd19e/{size}.png"},{"id":73,"username":"fimp","avatar_template":"https://avatars.discourse.org/v2/letter/f/ee59a6/{size}.png"},{"id":14,"username":"agilliland","avatar_template":"/user_avatar/discourse.metabase.com/agilliland/{size}/26_1.png"},{"id":87,"username":"amir","avatar_template":"https://avatars.discourse.org/v2/letter/a/c37758/{size}.png"},{"id":82,"username":"waseem","avatar_template":"https://avatars.discourse.org/v2/letter/w/9dc877/{size}.png"},{"id":78,"username":"tovenaar","avatar_template":"https://avatars.discourse.org/v2/letter/t/9de0a6/{size}.png"},{"id":74,"username":"Ben","avatar_template":"https://avatars.discourse.org/v2/letter/b/df788c/{size}.png"},{"id":71,"username":"MarkLaFay","avatar_template":"https://avatars.discourse.org/v2/letter/m/3bc359/{size}.png"},{"id":72,"username":"camsaul","avatar_template":"/user_avatar/discourse.metabase.com/camsaul/{size}/70_1.png"},{"id":53,"username":"mhjb","avatar_template":"/user_avatar/discourse.metabase.com/mhjb/{size}/54_1.png"},{"id":58,"username":"jbwiv","avatar_template":"https://avatars.discourse.org/v2/letter/j/6bbea6/{size}.png"},{"id":70,"username":"Maggs","avatar_template":"https://avatars.discourse.org/v2/letter/m/bbce88/{size}.png"},{"id":69,"username":"andrefaria","avatar_template":"/user_avatar/discourse.metabase.com/andrefaria/{size}/65_1.png"},{"id":60,"username":"bencarter78","avatar_template":"/user_avatar/discourse.metabase.com/bencarter78/{size}/59_1.png"},{"id":55,"username":"vikram","avatar_template":"https://avatars.discourse.org/v2/letter/v/e47774/{size}.png"},{"id":68,"username":"edchan77","avatar_template":"/user_avatar/discourse.metabase.com/edchan77/{size}/66_1.png"},{"id":9,"username":"karthikd","avatar_template":"https://avatars.discourse.org/v2/letter/k/cab0a1/{size}.png"},{"id":23,"username":"arthurz","avatar_template":"/user_avatar/discourse.metabase.com/arthurz/{size}/32_1.png"},{"id":3,"username":"tom","avatar_template":"/user_avatar/discourse.metabase.com/tom/{size}/21_1.png"},{"id":50,"username":"LeoNogueira","avatar_template":"/user_avatar/discourse.metabase.com/leonogueira/{size}/52_1.png"},{"id":66,"username":"ss06vi","avatar_template":"https://avatars.discourse.org/v2/letter/s/3ab097/{size}.png"},{"id":34,"username":"mattcollins","avatar_template":"/user_avatar/discourse.metabase.com/mattcollins/{size}/41_1.png"},{"id":51,"username":"krmmalik","avatar_template":"/user_avatar/discourse.metabase.com/krmmalik/{size}/53_1.png"},{"id":46,"username":"odysseas","avatar_template":"https://avatars.discourse.org/v2/letter/o/5f8ce5/{size}.png"},{"id":5,"username":"jonthewayne","avatar_template":"/user_avatar/discourse.metabase.com/jonthewayne/{size}/18_1.png"},{"id":11,"username":"anandiyer","avatar_template":"/user_avatar/discourse.metabase.com/anandiyer/{size}/23_1.png"},{"id":25,"username":"alnorth","avatar_template":"/user_avatar/discourse.metabase.com/alnorth/{size}/34_1.png"},{"id":52,"username":"j_at_svg","avatar_template":"https://avatars.discourse.org/v2/letter/j/96bed5/{size}.png"},{"id":42,"username":"styts","avatar_template":"/user_avatar/discourse.metabase.com/styts/{size}/47_1.png"}],"topics":{"can_create_topic":false,"more_topics_url":"/c/uncategorized/l/latest?page=1","draft":null,"draft_key":"new_topic","draft_sequence":null,"per_page":30,"topics":[{"id":8,"title":"Welcome to Metabase's Discussion Forum","fancy_title":"Welcome to Metabase&rsquo;s Discussion Forum","slug":"welcome-to-metabases-discussion-forum","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/images/welcome/discourse-edit-post-animated.gif","created_at":"2015-10-17T00:14:49.526Z","last_posted_at":"2015-10-17T00:14:49.557Z","bumped":true,"bumped_at":"2015-10-21T02:32:22.486Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"Welcome to Metabase&#39;s discussion forum. This is a place to get help on installation, setting up as well as sharing tips and tricks.","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":197,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"system","category_id":1,"pinned_globally":true,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":-1}]},{"id":169,"title":"Formatting Dates","fancy_title":"Formatting Dates","slug":"formatting-dates","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-01-14T06:30:45.311Z","last_posted_at":"2016-01-14T06:30:45.397Z","bumped":true,"bumped_at":"2016-01-14T06:30:45.397Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":11,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"zergot","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":89}]},{"id":168,"title":"Setting for google api key","fancy_title":"Setting for google api key","slug":"setting-for-google-api-key","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2016-01-13T17:14:31.799Z","last_posted_at":"2016-01-14T06:24:03.421Z","bumped":true,"bumped_at":"2016-01-14T06:24:03.421Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":16,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"zergot","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":89}]},{"id":167,"title":"Cannot see non-US timezones on the admin","fancy_title":"Cannot see non-US timezones on the admin","slug":"cannot-see-non-us-timezones-on-the-admin","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-01-13T17:07:36.764Z","last_posted_at":"2016-01-13T17:07:36.831Z","bumped":true,"bumped_at":"2016-01-13T17:07:36.831Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":11,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"zergot","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":89}]},{"id":164,"title":"External (Metabase level) linkages in data schema","fancy_title":"External (Metabase level) linkages in data schema","slug":"external-metabase-level-linkages-in-data-schema","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2016-01-11T13:51:02.286Z","last_posted_at":"2016-01-12T11:06:37.259Z","bumped":true,"bumped_at":"2016-01-12T11:06:37.259Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":32,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"zergot","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":89},{"extras":null,"description":"Frequent Poster","user_id":1}]},{"id":155,"title":"Query working on \"Questions\" but not in \"Pulses\"","fancy_title":"Query working on &ldquo;Questions&rdquo; but not in &ldquo;Pulses&rdquo;","slug":"query-working-on-questions-but-not-in-pulses","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2016-01-01T14:06:10.083Z","last_posted_at":"2016-01-08T22:37:51.772Z","bumped":true,"bumped_at":"2016-01-08T22:37:51.772Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":72,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"agilliland","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":84},{"extras":null,"description":"Frequent Poster","user_id":73},{"extras":"latest","description":"Most Recent Poster","user_id":14}]},{"id":161,"title":"Pulses posted to Slack don't show question output","fancy_title":"Pulses posted to Slack don&rsquo;t show question output","slug":"pulses-posted-to-slack-dont-show-question-output","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/original/1X/9d2806517bf3598b10be135b2c58923b47ba23e7.png","created_at":"2016-01-08T22:09:58.205Z","last_posted_at":"2016-01-08T22:28:44.685Z","bumped":true,"bumped_at":"2016-01-08T22:28:44.685Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":34,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":87},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":152,"title":"Should we build Kafka connecter or Kafka plugin","fancy_title":"Should we build Kafka connecter or Kafka plugin","slug":"should-we-build-kafka-connecter-or-kafka-plugin","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2015-12-28T20:37:23.501Z","last_posted_at":"2015-12-31T18:16:45.477Z","bumped":true,"bumped_at":"2015-12-31T18:16:45.477Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":84,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":82},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1}]},{"id":147,"title":"Change X and Y on graph","fancy_title":"Change X and Y on graph","slug":"change-x-and-y-on-graph","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-12-21T17:52:46.581Z","last_posted_at":"2015-12-21T17:52:46.684Z","bumped":true,"bumped_at":"2015-12-21T18:19:13.003Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":68,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"tovenaar","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":78}]},{"id":142,"title":"Issues sending mail via office365 relay","fancy_title":"Issues sending mail via office365 relay","slug":"issues-sending-mail-via-office365-relay","posts_count":5,"reply_count":2,"highest_post_number":5,"image_url":null,"created_at":"2015-12-16T10:38:47.315Z","last_posted_at":"2015-12-21T09:26:27.167Z","bumped":true,"bumped_at":"2015-12-21T09:26:27.167Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":122,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Ben","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":74},{"extras":null,"description":"Frequent Poster","user_id":1}]},{"id":137,"title":"I see triplicates of my mongoDB collections","fancy_title":"I see triplicates of my mongoDB collections","slug":"i-see-triplicates-of-my-mongodb-collections","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2015-12-14T13:33:03.426Z","last_posted_at":"2015-12-17T18:40:05.487Z","bumped":true,"bumped_at":"2015-12-17T18:40:05.487Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":97,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"MarkLaFay","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":71},{"extras":null,"description":"Frequent Poster","user_id":14}]},{"id":140,"title":"Google Analytics plugin","fancy_title":"Google Analytics plugin","slug":"google-analytics-plugin","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-12-15T13:00:55.644Z","last_posted_at":"2015-12-15T13:00:55.705Z","bumped":true,"bumped_at":"2015-12-15T13:00:55.705Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":105,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"fimp","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":73}]},{"id":138,"title":"With-mongo-connection failed: bad connection details:","fancy_title":"With-mongo-connection failed: bad connection details:","slug":"with-mongo-connection-failed-bad-connection-details","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-12-14T17:28:11.041Z","last_posted_at":"2015-12-14T17:28:11.111Z","bumped":true,"bumped_at":"2015-12-14T17:28:11.111Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":56,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"MarkLaFay","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":71}]},{"id":133,"title":"\"We couldn't understand your question.\" when I query mongoDB","fancy_title":"&ldquo;We couldn&rsquo;t understand your question.&rdquo; when I query mongoDB","slug":"we-couldnt-understand-your-question-when-i-query-mongodb","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2015-12-11T17:38:30.576Z","last_posted_at":"2015-12-14T13:31:26.395Z","bumped":true,"bumped_at":"2015-12-14T13:31:26.395Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":107,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"MarkLaFay","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":71},{"extras":null,"description":"Frequent Poster","user_id":72}]},{"id":129,"title":"My bar charts are all thin","fancy_title":"My bar charts are all thin","slug":"my-bar-charts-are-all-thin","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":"/uploads/default/original/1X/41bcf3b2a00dc7cfaff01cb3165d35d32a85bf1d.png","created_at":"2015-12-09T22:09:56.394Z","last_posted_at":"2015-12-11T19:00:45.289Z","bumped":true,"bumped_at":"2015-12-11T19:00:45.289Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":116,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"mhjb","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":53},{"extras":null,"description":"Frequent Poster","user_id":1}]},{"id":106,"title":"What is the expected return order of columns for graphing results when using raw SQL?","fancy_title":"What is the expected return order of columns for graphing results when using raw SQL?","slug":"what-is-the-expected-return-order-of-columns-for-graphing-results-when-using-raw-sql","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2015-11-24T19:07:14.561Z","last_posted_at":"2015-12-11T17:04:14.149Z","bumped":true,"bumped_at":"2015-12-11T17:04:14.149Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":153,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"jbwiv","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":58},{"extras":null,"description":"Frequent Poster","user_id":14}]},{"id":131,"title":"Set site url from admin panel","fancy_title":"Set site url from admin panel","slug":"set-site-url-from-admin-panel","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-12-10T06:22:46.042Z","last_posted_at":"2015-12-10T19:12:57.449Z","bumped":true,"bumped_at":"2015-12-10T19:12:57.449Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":77,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":70},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":127,"title":"Internationalization (i18n)","fancy_title":"Internationalization (i18n)","slug":"internationalization-i18n","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-12-08T16:55:37.397Z","last_posted_at":"2015-12-09T16:49:55.816Z","bumped":true,"bumped_at":"2015-12-09T16:49:55.816Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":85,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"agilliland","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":69},{"extras":"latest","description":"Most Recent Poster","user_id":14}]},{"id":109,"title":"Returning raw data with no filters always returns We couldn't understand your question","fancy_title":"Returning raw data with no filters always returns We couldn&rsquo;t understand your question","slug":"returning-raw-data-with-no-filters-always-returns-we-couldnt-understand-your-question","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2015-11-25T21:35:01.315Z","last_posted_at":"2015-12-09T10:26:12.255Z","bumped":true,"bumped_at":"2015-12-09T10:26:12.255Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":133,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"bencarter78","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":60},{"extras":null,"description":"Frequent Poster","user_id":14}]},{"id":103,"title":"Support for Cassandra?","fancy_title":"Support for Cassandra?","slug":"support-for-cassandra","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2015-11-20T06:45:31.741Z","last_posted_at":"2015-12-09T03:18:51.274Z","bumped":true,"bumped_at":"2015-12-09T03:18:51.274Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":169,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"vikram","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":55},{"extras":null,"description":"Frequent Poster","user_id":1}]},{"id":128,"title":"Mongo query with Date breaks [solved: Mongo 3.0 required]","fancy_title":"Mongo query with Date breaks [solved: Mongo 3.0 required]","slug":"mongo-query-with-date-breaks-solved-mongo-3-0-required","posts_count":5,"reply_count":0,"highest_post_number":5,"image_url":null,"created_at":"2015-12-08T18:30:56.562Z","last_posted_at":"2015-12-08T21:03:02.421Z","bumped":true,"bumped_at":"2015-12-08T21:03:02.421Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":102,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"edchan77","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":68},{"extras":null,"description":"Frequent Poster","user_id":1}]},{"id":23,"title":"Can this connect to MS SQL Server?","fancy_title":"Can this connect to MS SQL Server?","slug":"can-this-connect-to-ms-sql-server","posts_count":7,"reply_count":1,"highest_post_number":7,"image_url":null,"created_at":"2015-10-21T18:52:37.987Z","last_posted_at":"2015-12-07T17:41:51.609Z","bumped":true,"bumped_at":"2015-12-07T17:41:51.609Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":367,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":9},{"extras":null,"description":"Frequent Poster","user_id":23},{"extras":null,"description":"Frequent Poster","user_id":3},{"extras":null,"description":"Frequent Poster","user_id":50},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":121,"title":"Cannot restart metabase in docker","fancy_title":"Cannot restart metabase in docker","slug":"cannot-restart-metabase-in-docker","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2015-12-04T21:28:58.137Z","last_posted_at":"2015-12-04T23:02:00.488Z","bumped":true,"bumped_at":"2015-12-04T23:02:00.488Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":96,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":66},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1}]},{"id":85,"title":"Edit Max Rows Count","fancy_title":"Edit Max Rows Count","slug":"edit-max-rows-count","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":null,"created_at":"2015-11-11T23:46:52.917Z","last_posted_at":"2015-11-24T01:01:14.569Z","bumped":true,"bumped_at":"2015-11-24T01:01:14.569Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":169,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":34},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1}]},{"id":96,"title":"Creating charts by querying more than one table at a time","fancy_title":"Creating charts by querying more than one table at a time","slug":"creating-charts-by-querying-more-than-one-table-at-a-time","posts_count":6,"reply_count":4,"highest_post_number":6,"image_url":null,"created_at":"2015-11-17T11:20:18.442Z","last_posted_at":"2015-11-21T02:12:25.995Z","bumped":true,"bumped_at":"2015-11-21T02:12:25.995Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":217,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":51},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1}]},{"id":90,"title":"Trying to add RDS postgresql as the database fails silently","fancy_title":"Trying to add RDS postgresql as the database fails silently","slug":"trying-to-add-rds-postgresql-as-the-database-fails-silently","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":null,"created_at":"2015-11-14T23:45:02.967Z","last_posted_at":"2015-11-21T01:08:45.915Z","bumped":true,"bumped_at":"2015-11-21T01:08:45.915Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":162,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":46},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1}]},{"id":17,"title":"Deploy to Heroku isn't working","fancy_title":"Deploy to Heroku isn&rsquo;t working","slug":"deploy-to-heroku-isnt-working","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":null,"created_at":"2015-10-21T16:42:03.096Z","last_posted_at":"2015-11-20T18:34:14.044Z","bumped":true,"bumped_at":"2015-11-20T18:34:14.044Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":332,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"agilliland","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":5},{"extras":null,"description":"Frequent Poster","user_id":3},{"extras":null,"description":"Frequent Poster","user_id":11},{"extras":null,"description":"Frequent Poster","user_id":25},{"extras":"latest","description":"Most Recent Poster","user_id":14}]},{"id":100,"title":"Can I use DATEPART() in SQL queries?","fancy_title":"Can I use DATEPART() in SQL queries?","slug":"can-i-use-datepart-in-sql-queries","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-11-17T23:15:58.033Z","last_posted_at":"2015-11-18T00:19:48.763Z","bumped":true,"bumped_at":"2015-11-18T00:19:48.763Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":112,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":53},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":98,"title":"Feature Request: LDAP Authentication","fancy_title":"Feature Request: LDAP Authentication","slug":"feature-request-ldap-authentication","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-11-17T17:22:44.484Z","last_posted_at":"2015-11-17T17:22:44.577Z","bumped":true,"bumped_at":"2015-11-17T17:22:44.577Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":97,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"j_at_svg","category_id":1,"pinned_globally":false,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":52}]},{"id":87,"title":"Migrating from internal H2 to Postgres","fancy_title":"Migrating from internal H2 to Postgres","slug":"migrating-from-internal-h2-to-postgres","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-11-12T14:36:06.745Z","last_posted_at":"2015-11-12T18:05:10.796Z","bumped":true,"bumped_at":"2015-11-12T18:05:10.796Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":111,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sameer","category_id":1,"pinned_globally":false,"posters":[{"extras":null,"description":"Original Poster","user_id":42},{"extras":"latest","description":"Most Recent Poster","user_id":1}]}]}} +`) + +func NewLargePayload() *LargePayload { + dsUsers := DSUsers{} + dsTopics := DSTopics{} + for i := 0; i < 100; i++ { + str := "test" + strconv.Itoa(i) + dsUsers = append( + dsUsers, + &DSUser{ + Username: str, + }, + ) + dsTopics = append( + dsTopics, + &DSTopic{ + Id: i, + Slug: str, + }, + ) + } + return &LargePayload{ + Users: dsUsers, + Topics: &DSTopicsList{ + Topics: dsTopics, + MoreTopicsUrl: "http://test.com", + }, + } +} diff --git a/benchmarks/benchmarks_medium.go b/benchmarks/benchmarks_medium.go @@ -0,0 +1,312 @@ +package benchmarks + +import "github.com/francoispqt/gojay" + +// Reponse from Clearbit API. Size: 2.4kb +var MediumFixture = []byte(`{ + "person": { + "id": "d50887ca-a6ce-4e59-b89f-14f0b5d03b03", + "name": { + "fullName": "Leonid Bugaev", + "givenName": "Leonid", + "familyName": "Bugaev" + }, + "email": "leonsbox@gmail.com", + "gender": "male", + "location": "Saint Petersburg, Saint Petersburg, RU", + "geo": { + "city": "Saint Petersburg", + "state": "Saint Petersburg", + "country": "Russia", + "lat": 59.9342802, + "lng": 30.3350986 + }, + "bio": "Senior engineer at Granify.com", + "site": "http://flickfaver.com", + "avatar": "https://d1ts43dypk8bqh.cloudfront.net/v1/avatars/d50887ca-a6ce-4e59-b89f-14f0b5d03b03", + "employment": { + "name": "www.latera.ru", + "title": "Software Engineer", + "domain": "gmail.com" + }, + "facebook": { + "handle": "leonid.bugaev" + }, + "github": { + "handle": "buger", + "id": 14009, + "avatar": "https://avatars.githubusercontent.com/u/14009?v=3", + "company": "Granify", + "blog": "http://leonsbox.com", + "followers": 95, + "following": 10 + }, + "twitter": { + "handle": "flickfaver", + "id": 77004410, + "bio": null, + "followers": 2, + "following": 1, + "statuses": 5, + "favorites": 0, + "location": "", + "site": "http://flickfaver.com", + "avatar": null + }, + "linkedin": { + "handle": "in/leonidbugaev" + }, + "googleplus": { + "handle": null + }, + "angellist": { + "handle": "leonid-bugaev", + "id": 61541, + "bio": "Senior engineer at Granify.com", + "blog": "http://buger.github.com", + "site": "http://buger.github.com", + "followers": 41, + "avatar": "https://d1qb2nb5cznatu.cloudfront.net/users/61541-medium_jpg?1405474390" + }, + "klout": { + "handle": null, + "score": null + }, + "foursquare": { + "handle": null + }, + "aboutme": { + "handle": "leonid.bugaev", + "bio": null, + "avatar": null + }, + "gravatar": { + "handle": "buger", + "urls": [ + ], + "avatar": "http://1.gravatar.com/avatar/f7c8edd577d13b8930d5522f28123510", + "avatars": [ + { + "url": "http://1.gravatar.com/avatar/f7c8edd577d13b8930d5522f28123510", + "type": "thumbnail" + } + ] + }, + "fuzzy": false + }, + "company": null + }`) + +type CBAvatar struct { + Url string +} + +func (m *CBAvatar) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "avatars": + return dec.AddString(&m.Url) + } + return nil +} +func (m *CBAvatar) NKeys() int { + return 1 +} + +func (m *CBAvatar) MarshalObject(enc *gojay.Encoder) { + enc.AddStringKey("url", m.Url) +} + +func (m *CBAvatar) IsNil() bool { + return m == nil +} + +type Avatars []*CBAvatar + +func (t *Avatars) UnmarshalArray(dec *gojay.Decoder) error { + avatar := CBAvatar{} + *t = append(*t, &avatar) + return dec.AddObject(&avatar) +} + +func (m *Avatars) MarshalArray(enc *gojay.Encoder) { + for _, e := range *m { + enc.AddObject(e) + } +} + +type CBGravatar struct { + Avatars Avatars +} + +func (m *CBGravatar) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "avatars": + return dec.AddArray(&m.Avatars) + } + return nil +} +func (m *CBGravatar) NKeys() int { + return 1 +} + +func (m *CBGravatar) MarshalObject(enc *gojay.Encoder) { + enc.AddArrayKey("avatars", &m.Avatars) +} + +func (m *CBGravatar) IsNil() bool { + return m == nil +} + +type CBGithub struct { + Followers int +} + +func (m *CBGithub) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "followers": + return dec.AddInt(&m.Followers) + } + return nil +} + +func (m *CBGithub) NKeys() int { + return 1 +} + +func (m *CBGithub) MarshalObject(enc *gojay.Encoder) { + enc.AddIntKey("followers", m.Followers) +} + +func (m *CBGithub) IsNil() bool { + return m == nil +} + +type CBName struct { + FullName string `json:"fullName"` +} + +func (m *CBName) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "fullName": + return dec.AddString(&m.FullName) + } + return nil +} + +func (m *CBName) NKeys() int { + return 1 +} + +func (m *CBName) MarshalObject(enc *gojay.Encoder) { + enc.AddStringKey("fullName", m.FullName) +} + +func (m *CBName) IsNil() bool { + return m == nil +} + +type CBPerson struct { + Name *CBName `json:"name"` + Github *CBGithub `json:"github"` + Gravatar *CBGravatar +} + +func (m *CBPerson) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "name": + m.Name = &CBName{} + return dec.AddObject(m.Name) + case "github": + m.Github = &CBGithub{} + return dec.AddObject(m.Github) + case "gravatar": + m.Gravatar = &CBGravatar{} + return dec.AddObject(m.Gravatar) + } + return nil +} + +func (m *CBPerson) NKeys() int { + return 3 +} + +func (m *CBPerson) MarshalObject(enc *gojay.Encoder) { + enc.AddObjectKey("name", m.Name) + enc.AddObjectKey("github", m.Github) + enc.AddObjectKey("gravatar", m.Gravatar) +} + +func (m *CBPerson) IsNil() bool { + return m == nil +} + +type MediumPayload struct { + Person *CBPerson `json:"person"` + Company string `json:"company"` +} + +func (m *MediumPayload) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "person": + m.Person = &CBPerson{} + return dec.AddObject(m.Person) + case "company": + dec.AddString(&m.Company) + } + return nil +} + +func (m *MediumPayload) NKeys() int { + return 2 +} + +func (m *MediumPayload) MarshalObject(enc *gojay.Encoder) { + enc.AddObjectKey("person", m.Person) + // enc.AddStringKey("company", m.Company) +} + +func (m *MediumPayload) IsNil() bool { + return m == nil +} + +func NewMediumPayload() *MediumPayload { + return &MediumPayload{ + Company: "test", + Person: &CBPerson{ + Name: &CBName{ + FullName: "test", + }, + Github: &CBGithub{ + Followers: 100, + }, + Gravatar: &CBGravatar{ + Avatars: Avatars{ + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + &CBAvatar{ + Url: "http://test.com", + }, + }, + }, + }, + } +} diff --git a/benchmarks/benchmarks_small.go b/benchmarks/benchmarks_small.go @@ -0,0 +1,75 @@ +package benchmarks + +import "github.com/francoispqt/gojay" + +var SmallFixture = []byte(`{"st": 1,"sid": 486,"tt": "active","gr": 0,"uuid": "de305d54-75b4-431b-adb2-eb6b9e546014","ip": "127.0.0.1","ua": "user_agent","tz": -6,"v": 1}`) + +type SmallPayload struct { + St int + Sid int + Tt string + Gr int + Uuid string + Ip string + Ua string + Tz int + V int +} + +func (t *SmallPayload) MarshalObject(enc *gojay.Encoder) { + enc.AddIntKey("st", t.St) + enc.AddIntKey("sid", t.Sid) + enc.AddStringKey("tt", t.Tt) + enc.AddIntKey("gr", t.Gr) + enc.AddStringKey("uuid", t.Uuid) + enc.AddStringKey("ip", t.Ip) + enc.AddStringKey("ua", t.Ua) + enc.AddIntKey("tz", t.Tz) + enc.AddIntKey("v", t.V) +} + +func (t *SmallPayload) IsNil() bool { + return t == nil +} + +func (t *SmallPayload) UnmarshalObject(dec *gojay.Decoder, key string) error { + switch key { + case "st": + return dec.AddInt(&t.St) + case "sid": + return dec.AddInt(&t.Sid) + case "gr": + return dec.AddInt(&t.Gr) + case "tz": + return dec.AddInt(&t.Tz) + case "v": + return dec.AddInt(&t.V) + case "tt": + return dec.AddString(&t.Tt) + case "uuid": + return dec.AddString(&t.Uuid) + case "ip": + return dec.AddString(&t.Ip) + case "ua": + return dec.AddString(&t.Ua) + } + return nil +} + +func (t *SmallPayload) NKeys() int { + return 9 +} + +func NewSmallPayload() *SmallPayload { + return &SmallPayload{ + St: 1, + Sid: 2, + Tt: "TestString", + Gr: 4, + Uuid: "8f9a65eb-4807-4d57-b6e0-bda5d62f1429", + Ip: "127.0.0.1", + Ua: "Mozilla", + Tz: 8, + V: 6, + } +} diff --git a/benchmarks/decoder/.gitignore b/benchmarks/decoder/.gitignore @@ -0,0 +1 @@ +vendor/* +\ No newline at end of file diff --git a/benchmarks/decoder/Makefile b/benchmarks/decoder/Makefile @@ -0,0 +1,31 @@ +.PHONY: test +test: + go test -run=^Test -v + +.PHONY: bench +bench: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/decoder -bench ^Benchmark + +.PHONY: benchtrace +benchtrace: + go test -c & GODEBUG=allocfreetrace=1 ./decoder.test -test.run=none -test.bench=^Benchmark -test.benchtime=10ms 2>trace.log + +.PHONY: benchcpu +benchcpu: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/decoder -bench ^Benchmark -cpuprofile cpu.out + +.PHONY: testtrace +testtrace: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/decoder -bench ^Benchmark -trace trace.out + +.PHONY: benchgojay +benchgojay: + go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms -cpuprofile cpu.out + +.PHONY: benchjsoniter +benchjsoniter: + go test -benchmem -run=^BenchmarkJsonIter -bench=^BenchmarkJsonIter -benchtime=30ms + +.PHONY: benchjsonparser +benchjsonparser: + go test -benchmem -run=^BenchmarkJsonParser -bench=^BenchmarkJsonParser -benchtime=30ms +\ No newline at end of file diff --git a/benchmarks/decoder/decoder.go b/benchmarks/decoder/decoder.go @@ -0,0 +1 @@ +package benchmarks diff --git a/benchmarks/decoder/decoder_bench_large_test.go b/benchmarks/decoder/decoder_bench_large_test.go @@ -0,0 +1,42 @@ +package benchmarks + +import ( + "testing" + + "github.com/buger/jsonparser" + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkJsonParserDecodeObjLarge(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonparser.ArrayEach(benchmarks.LargeFixture, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { + jsonparser.Get(value, "username") + nothing() + }, "users") + + jsonparser.ArrayEach(benchmarks.LargeFixture, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { + jsonparser.GetInt(value, "id") + jsonparser.Get(value, "slug") + nothing() + }, "topics", "topics") + } +} + +func BenchmarkJsonIterDecodeObjLarge(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + result := benchmarks.LargePayload{} + jsoniter.Unmarshal(benchmarks.LargeFixture, &result) + } +} + +func BenchmarkGoJayDecodeObjLarge(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + result := benchmarks.LargePayload{} + gojay.UnmarshalObject(benchmarks.LargeFixture, &result) + } +} diff --git a/benchmarks/decoder/decoder_bench_medium_test.go b/benchmarks/decoder/decoder_bench_medium_test.go @@ -0,0 +1,60 @@ +package benchmarks + +import ( + "encoding/json" + "testing" + + "github.com/buger/jsonparser" + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkJsonIterDecodeObjMedium(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + result := benchmarks.MediumPayload{} + jsoniter.Unmarshal(benchmarks.MediumFixture, &result) + } +} + +/* + github.com/buger/jsonparser +*/ +func BenchmarkJSONParserDecodeObjMedium(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonparser.Get(benchmarks.MediumFixture, "person", "name", "fullName") + jsonparser.GetInt(benchmarks.MediumFixture, "person", "github", "followers") + jsonparser.Get(benchmarks.MediumFixture, "company") + + jsonparser.ArrayEach(benchmarks.MediumFixture, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { + jsonparser.Get(value, "url") + nothing() + }, "person", "gravatar", "avatars") + } +} + +func BenchmarkEncodingJsonStructMedium(b *testing.B) { + for i := 0; i < b.N; i++ { + var data = benchmarks.MediumPayload{} + json.Unmarshal(benchmarks.MediumFixture, &data) + + nothing(data.Person.Name.FullName, data.Person.Github.Followers, data.Company) + + for _, el := range data.Person.Gravatar.Avatars { + nothing(el.Url) + } + } +} + +func BenchmarkGoJayDecodeObjMedium(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + result := benchmarks.MediumPayload{} + err := gojay.UnmarshalObject(benchmarks.MediumFixture, &result) + if err != nil { + b.Error(err) + } + } +} diff --git a/benchmarks/decoder/decoder_bench_small_test.go b/benchmarks/decoder/decoder_bench_small_test.go @@ -0,0 +1,52 @@ +package benchmarks + +import ( + "encoding/json" + _ "fmt" + "testing" + + "github.com/buger/jsonparser" + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkJSONDecodeObjSmall(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + result := benchmarks.SmallPayload{} + json.Unmarshal(benchmarks.SmallFixture, &result) + } +} + +func nothing(_ ...interface{}) {} +func BenchmarkJSONParserSmall(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonparser.GetInt(benchmarks.SmallFixture, "tz") + jsonparser.GetInt(benchmarks.SmallFixture, "v") + jsonparser.GetInt(benchmarks.SmallFixture, "sid") + jsonparser.GetInt(benchmarks.SmallFixture, "st") + jsonparser.GetInt(benchmarks.SmallFixture, "gr") + jsonparser.Get(benchmarks.SmallFixture, "uuid") + jsonparser.Get(benchmarks.SmallFixture, "ua") + + nothing() + } +} + +func BenchmarkJsonIterDecodeObjSmall(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + result := benchmarks.SmallPayload{} + jsoniter.Unmarshal(benchmarks.SmallFixture, &result) + } +} + +func BenchmarkGoJayDecodeObjSmall(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + result := benchmarks.SmallPayload{} + gojay.UnmarshalObject(benchmarks.SmallFixture, &result) + } +} diff --git a/benchmarks/decoder/decoder_large_test.go b/benchmarks/decoder/decoder_large_test.go @@ -0,0 +1,25 @@ +package benchmarks + +import ( + "log" + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + "github.com/stretchr/testify/assert" +) + +func TestGoJayDecodeObjLarge(t *testing.T) { + result := benchmarks.LargePayload{} + err := gojay.UnmarshalObject(benchmarks.LargeFixture, &result) + assert.Nil(t, err, "err should be nil") + assert.Len(t, result.Users, 32, "Len of users should be 32") + for _, u := range result.Users { + log.Print(u) + assert.True(t, len(u.Username) > 0, "User should have username") + } + assert.Len(t, result.Topics.Topics, 30, "Len of topics should be 30") + for _, top := range result.Topics.Topics { + assert.True(t, top.Id > 0, "Topic should have Id") + } +} diff --git a/benchmarks/decoder/decoder_medium_test.go b/benchmarks/decoder/decoder_medium_test.go @@ -0,0 +1,18 @@ +package benchmarks + +import ( + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + "github.com/stretchr/testify/assert" +) + +func TestGoJayDecodeObjMedium(t *testing.T) { + result := benchmarks.MediumPayload{} + err := gojay.Unmarshal(benchmarks.MediumFixture, &result) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "Leonid Bugaev", result.Person.Name.FullName, "result.Person.Name.FullName should be Leonid Bugaev") + assert.Equal(t, 95, result.Person.Github.Followers, "result.Person.Github.Followers should be 95") + assert.Len(t, result.Person.Gravatar.Avatars, 1, "result.Person.Gravatar.Avatars should have 1 item") +} diff --git a/benchmarks/decoder/decoder_small_test.go b/benchmarks/decoder/decoder_small_test.go @@ -0,0 +1,29 @@ +package benchmarks + +import ( + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + "github.com/stretchr/testify/assert" +) + +func TestGoJayDecodeObjSmall(t *testing.T) { + result := benchmarks.SmallPayload{} + err := gojay.Unmarshal(benchmarks.SmallFixture, &result) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, result.St, 1, "result.St should be 1") + assert.Equal(t, result.Sid, 486, "result.Sid should be 486") + assert.Equal(t, result.Tt, "active", "result.Sid should be 'active'") + assert.Equal(t, result.Gr, 0, "result.Gr should be 0") + assert.Equal( + t, + result.Uuid, + "de305d54-75b4-431b-adb2-eb6b9e546014", + "result.Gr should be 'de305d54-75b4-431b-adb2-eb6b9e546014'", + ) + assert.Equal(t, result.Ip, "127.0.0.1", "result.Ip should be '127.0.0.1'") + assert.Equal(t, result.Ua, "user_agent", "result.Ua should be 'user_agent'") + assert.Equal(t, result.Tz, -6, "result.Tz should be 6") + assert.Equal(t, result.V, 1, "result.V should be 1") +} diff --git a/benchmarks/encoder/.gitignore b/benchmarks/encoder/.gitignore @@ -0,0 +1 @@ +vendor/* +\ No newline at end of file diff --git a/benchmarks/encoder/Makefile b/benchmarks/encoder/Makefile @@ -0,0 +1,27 @@ +.PHONY: bench +bench: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/encoder -bench ^Benchmark + +.PHONY: benchtrace +benchtrace: + go test -c & GODEBUG=allocfreetrace=1 ./encoder.test -test.run=none -test.bench=^Benchmark -test.benchtime=10ms 2>trace.log + +.PHONY: benchcpu +benchcpu: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/encoder -bench ^Benchmark -cpuprofile cpu.out + +.PHONY: testtrace +testtrace: + go test -benchmem -run=^$ github.com/francoispqt/gojay/benchmarks/encoder -bench ^Benchmark -trace trace.out + +.PHONY: benchgojay +benchgojay: + go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms -cpuprofile cpu.out + +.PHONY: benchjsoniter +benchjsoniter: + go test -benchmem -run=^BenchmarkJsonIter -bench=^BenchmarkJsonIter -benchtime=30ms + +.PHONY: benchjsonparser +benchjsonparser: + go test -benchmem -run=^BenchmarkJsonParser -bench=^BenchmarkJsonParser -benchtime=30ms +\ No newline at end of file diff --git a/benchmarks/encoder/encoder_bench_large_test.go b/benchmarks/encoder/encoder_bench_large_test.go @@ -0,0 +1,46 @@ +package benchmarks + +import ( + "encoding/json" + "log" + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkEncodingJsonEncodeLargeStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewLargePayload()); err != nil { + b.Fatal(err) + } + } +} +func BenchmarkJsonIterEncodeLargeStruct(b *testing.B) { + var json = jsoniter.ConfigCompatibleWithStandardLibrary + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewLargePayload()); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkGoJayEncodeLargeStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := gojay.MarshalObject(benchmarks.NewLargePayload()); err != nil { + b.Fatal(err) + } + } +} + +func TestGoJayEncodeLargeStruct(t *testing.T) { + if output, err := gojay.MarshalObject(benchmarks.NewLargePayload()); err != nil { + t.Fatal(err) + } else { + log.Print(string(output)) + } +} diff --git a/benchmarks/encoder/encoder_bench_medium_test.go b/benchmarks/encoder/encoder_bench_medium_test.go @@ -0,0 +1,47 @@ +package benchmarks + +import ( + "encoding/json" + "log" + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkEncodingJsonEncodeMediumStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewMediumPayload()); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkJsonIterEncodeMediumStruct(b *testing.B) { + var json = jsoniter.ConfigCompatibleWithStandardLibrary + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewMediumPayload()); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkGoJayEncodeMediumStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := gojay.MarshalObject(benchmarks.NewMediumPayload()); err != nil { + b.Fatal(err) + } + } +} + +func TestGoJayEncodeMediumStruct(t *testing.T) { + if output, err := gojay.MarshalObject(benchmarks.NewMediumPayload()); err != nil { + t.Fatal(err) + } else { + log.Print(string(output)) + } +} diff --git a/benchmarks/encoder/encoder_bench_small_test.go b/benchmarks/encoder/encoder_bench_small_test.go @@ -0,0 +1,47 @@ +package benchmarks + +import ( + "encoding/json" + "log" + "testing" + + "github.com/francoispqt/gojay" + "github.com/francoispqt/gojay/benchmarks" + jsoniter "github.com/json-iterator/go" +) + +func BenchmarkEncodingJsonEncodeSmallStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewSmallPayload()); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkJsonIterEncodeSmallStruct(b *testing.B) { + var json = jsoniter.ConfigCompatibleWithStandardLibrary + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(benchmarks.NewSmallPayload()); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkGoJayEncodeSmallStruct(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := gojay.MarshalObject(benchmarks.NewSmallPayload()); err != nil { + b.Fatal(err) + } + } +} + +func TestGoJayEncodeSmallStruct(t *testing.T) { + if output, err := gojay.MarshalObject(benchmarks.NewSmallPayload()); err != nil { + t.Fatal(err) + } else { + log.Print(output) + } +} diff --git a/benchmarks/encoder/encoder_large_test.go b/benchmarks/encoder/encoder_large_test.go @@ -0,0 +1 @@ +package benchmarks diff --git a/benchmarks/encoder/encoder_medium_test.go b/benchmarks/encoder/encoder_medium_test.go @@ -0,0 +1 @@ +package benchmarks diff --git a/benchmarks/encoder/encoder_small_test.go b/benchmarks/encoder/encoder_small_test.go @@ -0,0 +1 @@ +package benchmarks diff --git a/decode.go b/decode.go @@ -0,0 +1,298 @@ +package gojay + +import ( + "fmt" + "io" + "reflect" +) + +// UnmarshalArray parses the JSON-encoded data and stores the result in the value pointed to by v. +// +// v must implement UnmarshalerArray. +// +// If a JSON value is not appropriate for a given target type, or if a JSON number +// overflows the target type, UnmarshalArray skips that field and completes the unmarshaling as best it can. +func UnmarshalArray(data []byte, v UnmarshalerArray) error { + dec := newDecoder(nil, 0) + dec.data = data + dec.length = len(data) + _, err := dec.DecodeArray(v) + dec.addToPool() + if dec.err != nil { + return dec.err + } + return err +} + +// UnmarshalObject parses the JSON-encoded data and stores the result in the value pointed to by v. +// +// v must implement UnmarshalerObject. +// +// If a JSON value is not appropriate for a given target type, or if a JSON number +// overflows the target type, UnmarshalObject skips that field and completes the unmarshaling as best it can. +func UnmarshalObject(data []byte, v UnmarshalerObject) error { + dec := newDecoder(nil, 0) + dec.data = data + dec.length = len(data) + _, err := dec.DecodeObject(v) + dec.addToPool() + return err +} + +// Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. +// If v is nil, not a pointer, or not an implementation of UnmarshalerObject or UnmarshalerArray +// Unmarshal returns an InvalidUnmarshalError. +// +// Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps, slices, and pointers as necessary, with the following additional rules: +// To unmarshal JSON into a pointer, Unmarshal first handles the case of the JSON being the JSON literal null. +// In that case, Unmarshal sets the pointer to nil. +// Otherwise, Unmarshal unmarshals the JSON into the value pointed at by the pointer. +// If the pointer is nil, Unmarshal allocates a new value for it to point to. +// +// To Unmarshal JSON into a struct, Unmarshal requires the struct to implement UnmarshalerObject. +// +// To unmarshal a JSON array into a slice, Unmarshal requires the slice to implement UnmarshalerArray. +// +// Unmarshal JSON does not allow yet to unmarshall an interface value +// If a JSON value is not appropriate for a given target type, or if a JSON number +// overflows the target type, Unmarshal skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns an UnmarshalTypeError describing the earliest such error. In any case, it's not guaranteed that all the remaining fields following the problematic one will be unmarshaled into the target object. +func Unmarshal(data []byte, v interface{}) error { + var err error + var dec *Decoder + switch vt := v.(type) { + case *string: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeString(vt) + case *int: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeInt(vt) + case *int32: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeInt32(vt) + case *uint32: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeUint32(vt) + case *int64: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeInt64(vt) + case *uint64: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeUint64(vt) + case *float64: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeFloat64(vt) + case *bool: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + err = dec.DecodeBool(vt) + case UnmarshalerObject: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + _, err = dec.DecodeObject(vt) + case UnmarshalerArray: + dec = newDecoder(nil, 0) + dec.length = len(data) + dec.data = data + _, err = dec.DecodeArray(vt) + default: + return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) + } + defer dec.addToPool() + if err != nil { + return err + } + return dec.err +} + +// UnmarshalerObject is the interface to implement for a struct to be +// decoded +type UnmarshalerObject interface { + UnmarshalObject(*Decoder, string) error + NKeys() int +} + +// UnmarshalerArray is the interface to implement for a slice or an array to be +// decoded +type UnmarshalerArray interface { + UnmarshalArray(*Decoder) error +} + +// UnmarshalerStream is the interface to implement for a slice, an array or a slice +// to decode a line delimited JSON to. +type UnmarshalerStream interface { + UnmarshalStream(*StreamDecoder) error +} + +// A Decoder reads and decodes JSON values from an input stream. +type Decoder struct { + data []byte + cursor int + length int + keysDone int + called byte + child byte + err error + r io.Reader +} + +// Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) Decode(v interface{}) error { + switch vt := v.(type) { + case *string: + return dec.DecodeString(vt) + case *int: + return dec.DecodeInt(vt) + case *int32: + return dec.DecodeInt32(vt) + case *uint32: + return dec.DecodeUint32(vt) + case *int64: + return dec.DecodeInt64(vt) + case *uint64: + return dec.DecodeUint64(vt) + case *float64: + return dec.DecodeFloat64(vt) + case *bool: + return dec.DecodeBool(vt) + case UnmarshalerObject: + _, err := dec.DecodeObject(vt) + return err + case UnmarshalerArray: + _, err := dec.DecodeArray(vt) + return err + default: + return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) + } +} + +// ADD VALUES FUNCTIONS + +// AddInt decodes the next key to an *int. +// If next key value overflows int, an InvalidTypeError error will be returned. +func (dec *Decoder) AddInt(v *int) error { + err := dec.DecodeInt(v) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + +// AddFloat decodes the next key to a *float64. +// If next key value overflows float64, an InvalidTypeError error will be returned. +func (dec *Decoder) AddFloat(v *float64) error { + err := dec.DecodeFloat64(v) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + +// AddBool decodes the next key to a *bool. +// If next key is neither null nor a JSON boolean, an InvalidTypeError will be returned. +// If next key is null, bool will be false. +func (dec *Decoder) AddBool(v *bool) error { + err := dec.DecodeBool(v) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + +// AddString decodes the next key to a *string. +// If next key is not a JSON string nor null, InvalidTypeError will be returned. +func (dec *Decoder) AddString(v *string) error { + err := dec.DecodeString(v) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + +// AddObject decodes the next key to a UnmarshalerObject. +func (dec *Decoder) AddObject(value UnmarshalerObject) error { + initialKeysDone := dec.keysDone + initialChild := dec.child + dec.keysDone = 0 + dec.called = 0 + dec.child |= 1 + newCursor, err := dec.DecodeObject(value) + if err != nil { + return err + } + dec.cursor = newCursor + dec.keysDone = initialKeysDone + dec.child = initialChild + dec.called |= 1 + return nil +} + +// AddArray decodes the next key to a UnmarshalerArray. +func (dec *Decoder) AddArray(value UnmarshalerArray) error { + newCursor, err := dec.DecodeArray(value) + if err != nil { + return err + } + dec.cursor = newCursor + dec.called |= 1 + return nil +} + +// Non exported + +func isDigit(b byte) bool { + switch b { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true + default: + return false + } +} + +func (dec *Decoder) read() bool { + if dec.r != nil { + // idea is to append data from reader at the end + n, err := dec.r.Read(dec.data[dec.length:]) + if err != nil || n == 0 { + return false + } + dec.length = dec.length + n + return true + } + return false +} + +func (dec *Decoder) nextChar() byte { + for dec.cursor < dec.length || dec.read() { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + dec.cursor = dec.cursor + 1 + continue + } + d := dec.data[dec.cursor] + return d + } + return 0 +} diff --git a/decode_array.go b/decode_array.go @@ -0,0 +1,106 @@ +package gojay + +import ( + "fmt" +) + +// DecodeArray reads the next JSON-encoded value from its input and stores it in the value pointed to by v. +// +// v must implement UnmarshalerArray. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeArray(arr UnmarshalerArray) (int, error) { + // not an array not an error, but do not know what to do + // do not check syntax + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + case '[': + n := 0 + dec.cursor = dec.cursor + 1 + // array is open, char is not space start readings + for dec.nextChar() != 0 { + // closing array + if dec.data[dec.cursor] == ']' { + dec.cursor = dec.cursor + 1 + return dec.cursor, nil + } + // calling unmarshall function for each element of the slice + err := arr.UnmarshalArray(dec) + if err != nil { + return 0, err + } + n++ + } + return dec.cursor, nil + case 'n': + // is null + dec.cursor = dec.cursor + 4 + return dec.cursor, nil + case '{', '"', 'f', 't', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + // can't unmarshall to struct + // we skip array and set Error + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to array, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return 0, err + } + return dec.cursor, nil + default: + return 0, InvalidJSONError("Invalid JSON") + } + } + return 0, InvalidJSONError("Invalid JSON") +} + +func (dec *Decoder) skipArray() (int, error) { + var arraysOpen = 1 + var arraysClosed = 0 + // var stringOpen byte = 0 + for j := dec.cursor; j < dec.length; j++ { + switch dec.data[j] { + case ']': + arraysClosed++ + // everything is closed return + if arraysOpen == arraysClosed { + // add char to object data + return j + 1, nil + } + case '[': + arraysOpen++ + case '"': + j++ + for ; j < dec.length; j++ { + if dec.data[j] != '"' { + continue + } + if dec.data[j-1] != '\\' { + break + } + // loop backward and count how many anti slash found + // to see if string is effectively escaped + ct := 1 + for i := j; i > 0; i-- { + if dec.data[i] != '\\' { + break + } + ct++ + } + // is even number of slashes, quote is not escaped + if ct&1 == 0 { + break + } + } + default: + continue + } + } + return 0, nil +} diff --git a/decode_array_test.go b/decode_array_test.go @@ -0,0 +1,171 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type testSliceStrings []string + +func (t *testSliceStrings) UnmarshalArray(dec *Decoder) error { + str := "" + if err := dec.AddString(&str); err != nil { + return err + } + *t = append(*t, str) + return nil +} + +type testSliceInts []*int + +func (t *testSliceInts) UnmarshalArray(dec *Decoder) error { + i := 0 + ptr := &i + *t = append(*t, ptr) + return dec.AddInt(ptr) +} + +type testSliceObj []*TestObj + +func (t *testSliceObj) UnmarshalArray(dec *Decoder) error { + obj := &TestObj{} + *t = append(*t, obj) + return dec.AddObject(obj) +} + +type testChannelArray chan *TestObj + +func (c *testChannelArray) UnmarshalArray(dec *Decoder) error { + obj := &TestObj{} + if err := dec.AddObject(obj); err != nil { + return err + } + *c <- obj + return nil +} + +func TestDecoderSliceOfStringsBasic(t *testing.T) { + json := []byte(`["string","string1"]`) + testArr := testSliceStrings{} + err := Unmarshal(json, &testArr) + assert.Nil(t, err, "Err must be nil") + assert.Len(t, testArr, 2, "testArr should be of len 2") + assert.Equal(t, "string", testArr[0], "testArr[0] should be 'string'") + assert.Equal(t, "string1", testArr[1], "testArr[1] should be 'string1'") +} + +func TestDecoderSliceNull(t *testing.T) { + json := []byte(`null`) + v := &testSliceStrings{} + err := Unmarshal(json, v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, len(*v), 0, "v must be of len 0") +} + +func TestDecoderSliceArrayOfIntsBasic(t *testing.T) { + json := []byte(`[ + 1, + 2 + ]`) + testArr := testSliceInts{} + err := UnmarshalArray(json, &testArr) + assert.Nil(t, err, "Err must be nil") + assert.Len(t, testArr, 2, "testArr should be of len 2") + assert.Equal(t, 1, *testArr[0], "testArr[0] should be 1") + assert.Equal(t, 2, *testArr[1], "testArr[1] should be 2") +} + +func TestDecoderSliceArrayOfIntsBigInts(t *testing.T) { + json := []byte(`[ + 789034384533530523, + 545344023293232032 + ]`) + testArr := testSliceInts{} + err := UnmarshalArray(json, &testArr) + assert.Nil(t, err, "Err must be nil") + assert.Len(t, testArr, 2, "testArr should be of len 2") + assert.Equal(t, 789034384533530523, *testArr[0], "testArr[0] should be 789034384533530523") + assert.Equal(t, 545344023293232032, *testArr[1], "testArr[1] should be 545344023293232032") +} + +func TestDecoderSliceOfObjectsBasic(t *testing.T) { + json := []byte(`[ + { + "test": 245, + "test2": -246, + "test3": "string" + }, + { + "test": 247, + "test2": 248, + "test3": "string" + }, + { + "test": 777, + "test2": 456, + "test3": "string" + } + ]`) + testArr := testSliceObj{} + err := Unmarshal(json, &testArr) + assert.Nil(t, err, "Err must be nil") + assert.Len(t, testArr, 3, "testArr should be of len 2") + assert.Equal(t, 245, testArr[0].test, "testArr[0] should be 245") + assert.Equal(t, -246, testArr[0].test2, "testArr[0] should be 246") + assert.Equal(t, "string", testArr[0].test3, "testArr[0].test3 should be 'string'") + assert.Equal(t, 247, testArr[1].test, "testArr[1] should be 247") + assert.Equal(t, 248, testArr[1].test2, "testArr[1] should be 248") + assert.Equal(t, "string", testArr[1].test3, "testArr[1].test3 should be 'string'") + assert.Equal(t, 777, testArr[2].test, "testArr[2] should be 777") + assert.Equal(t, 456, testArr[2].test2, "testArr[2] should be 456") + assert.Equal(t, "string", testArr[2].test3, "testArr[2].test3 should be 'string'") +} + +func TestDecodeSliceInvalidType(t *testing.T) { + result := testSliceObj{} + err := UnmarshalArray([]byte(`{}`), &result) + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err should be of type InvalidTypeError") + assert.Equal(t, "Cannot unmarshall to array, wrong char '{' found at pos 0", err.Error(), "err should not be nil") +} + +func TestDecoderChannelOfObjectsBasic(t *testing.T) { + json := []byte(`[ + { + "test": 245, + "test2": -246, + "test3": "string" + }, + { + "test": 247, + "test2": 248, + "test3": "string" + }, + { + "test": 777, + "test2": 456, + "test3": "string" + } + ]`) + testChan := testChannelArray(make(chan *TestObj, 3)) + err := UnmarshalArray(json, &testChan) + assert.Nil(t, err, "Err must be nil") + ct := 0 + l := len(testChan) + for _ = range testChan { + ct++ + if ct == l { + break + } + } + assert.Equal(t, ct, 3) +} + +func TestDecoderSliceInvalidJSON(t *testing.T) { + json := []byte(`hello`) + testArr := testSliceInts{} + err := UnmarshalArray(json, &testArr) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_bool.go b/decode_bool.go @@ -0,0 +1,41 @@ +package gojay + +import "fmt" + +// DecodeBool reads the next JSON-encoded value from its input and stores it in the boolean pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeBool(v *bool) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + case 't': + dec.cursor = dec.cursor + 4 + *v = true + return nil + case 'f': + dec.cursor = dec.cursor + 5 + *v = false + return nil + case 'n': + dec.cursor = dec.cursor + 4 + *v = false + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to bool, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return nil +} diff --git a/decode_bool_test.go b/decode_bool_test.go @@ -0,0 +1,47 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecoderBoolTrue(t *testing.T) { + json := []byte(`true`) + var v bool + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, true, v, "v must be equal to true") +} + +func TestDecoderBoolFalse(t *testing.T) { + json := []byte(`false`) + var v bool + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, false, v, "v must be equal to false") +} + +func TestDecoderBoolInvalidType(t *testing.T) { + json := []byte(`"string"`) + var v bool + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil") + assert.Equal(t, false, v, "v must be equal to false as it is zero val") +} + +func TestDecoderBoolNonBooleanJSONFalse(t *testing.T) { + json := []byte(`null`) + var v bool + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, false, v, "v must be equal to true") +} + +func TestDecoderBoolInvalidJSON(t *testing.T) { + json := []byte(`hello`) + var v bool + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_number.go b/decode_number.go @@ -0,0 +1,592 @@ +package gojay + +import ( + "fmt" +) + +var digits []int8 + +const maxUint32 = uint32(0xffffffff) +const maxUint64 = uint64(0xffffffffffffffff) +const maxInt32 = int32(0x7fffffff) +const maxInt64 = int64(0x7fffffffffffffff) +const maxInt64toMultiply = int64(0x7fffffffffffffff) / 10 +const maxInt32toMultiply = int32(0x7fffffff) / 10 +const maxUint32toMultiply = uint32(0xffffffff) / 10 +const maxUint64toMultiply = uint64(0xffffffffffffffff) / 10 +const maxUint32Length = 10 +const maxUint64Length = 20 +const maxInt32Length = 10 +const maxInt64Length = 19 +const invalidNumber = int8(-1) + +var pow10uint64 = [20]uint64{ + 0, + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 10000000000000, + 100000000000000, + 1000000000000000, + 10000000000000000, + 100000000000000000, + 1000000000000000000, +} + +func init() { + digits = make([]int8, 256) + for i := 0; i < len(digits); i++ { + digits[i] = invalidNumber + } + for i := int8('0'); i <= int8('9'); i++ { + digits[i] = i - int8('0') + } +} + +// DecodeInt reads the next JSON-encoded value from its input and stores it in the int pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeInt(v *int) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getInt64(c) + if err != nil { + return err + } + *v = int(val) + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getInt64(dec.data[dec.cursor]) + if err != nil { + return err + } + *v = -int(val) + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to int, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing int") +} + +// DecodeInt32 reads the next JSON-encoded value from its input and stores it in the int32 pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeInt32(v *int32) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getInt32(c) + if err != nil { + return err + } + *v = val + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getInt32(dec.data[dec.cursor]) + if err != nil { + return err + } + *v = -val + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to int, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing int") +} + +// DecodeUint32 reads the next JSON-encoded value from its input and stores it in the uint32 pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeUint32(v *uint32) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getUint32(c) + if err != nil { + return err + } + *v = val + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getUint32(dec.data[dec.cursor]) + if err != nil { + return err + } + // unsigned int so we don't bother with the sign + *v = val + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to int, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing int") +} + +// DecodeInt64 reads the next JSON-encoded value from its input and stores it in the int64 pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeInt64(v *int64) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getInt64(c) + if err != nil { + return err + } + *v = val + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getInt64(dec.data[dec.cursor]) + if err != nil { + return err + } + *v = -val + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to int, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing int") +} + +// DecodeUint64 reads the next JSON-encoded value from its input and stores it in the uint64 pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeUint64(v *uint64) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getUint64(c) + if err != nil { + return err + } + *v = val + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getUint64(dec.data[dec.cursor]) + if err != nil { + return err + } + // unsigned int so we don't bother with the sign + *v = val + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to int, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing int") +} + +// DecodeFloat64 reads the next JSON-encoded value from its input and stores it in the float64 pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeFloat64(v *float64) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch c := dec.data[dec.cursor]; c { + case ' ', '\n', '\t', '\r', ',': + continue + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + val, err := dec.getFloat(c) + if err != nil { + return err + } + *v = val + return nil + case '-': + dec.cursor = dec.cursor + 1 + val, err := dec.getFloat(c) + if err != nil { + return err + } + *v = -val + return nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to float, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing float") +} + +func (dec *Decoder) skipNumber() (int, error) { + end := dec.cursor + 1 + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + 1 + continue + case '.': + end = j + 1 + continue + case ',', '}', ']': + return end, nil + case ' ', '\n', '\t', '\r': + continue + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return end, InvalidJSONError("Invalid JSON while parsing number") + } + return end, nil +} + +func (dec *Decoder) getInt64(b byte) (int64, error) { + var end = dec.cursor + var start = dec.cursor + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + continue + case ' ', '\n', '\t', '\r': + continue + case '.', ',', '}', ']': + dec.cursor = j + return dec.atoi64(start, end), nil + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return 0, InvalidJSONError("Invalid JSON while parsing number") + } + return dec.atoi64(start, end), nil +} + +func (dec *Decoder) getUint64(b byte) (uint64, error) { + var end = dec.cursor + var start = dec.cursor + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + continue + case ' ', '\n', '\t', '\r': + continue + case '.', ',', '}', ']': + dec.cursor = j + return dec.atoui64(start, end), nil + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return 0, InvalidJSONError("Invalid JSON while parsing number") + } + return dec.atoui64(start, end), nil +} + +func (dec *Decoder) getInt32(b byte) (int32, error) { + var end = dec.cursor + var start = dec.cursor + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + continue + case ' ', '\n', '\t', '\r': + continue + case '.', ',', '}', ']': + dec.cursor = j + return dec.atoi32(start, end), nil + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return 0, InvalidJSONError("Invalid JSON while parsing number") + } + return dec.atoi32(start, end), nil +} + +func (dec *Decoder) getUint32(b byte) (uint32, error) { + var end = dec.cursor + var start = dec.cursor + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + continue + case ' ', '\n', '\t', '\r': + continue + case '.', ',', '}', ']': + dec.cursor = j + return dec.atoui32(start, end), nil + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return 0, InvalidJSONError("Invalid JSON while parsing number") + } + return dec.atoui32(start, end), nil +} + +func (dec *Decoder) getFloat(b byte) (float64, error) { + var end = dec.cursor + var start = dec.cursor + // look for following numbers + for j := dec.cursor + 1; j < dec.length || dec.read(); j++ { + switch dec.data[j] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + end = j + continue + case '.': + // we get part before decimal as integer + beforeDecimal := dec.atoi64(start, end) + // then we get part after decimal as integer + start = j + 1 + // get number after the decimal point + // multiple the before decimal point portion by 10 using bitwise + for i := j + 1; i < dec.length || dec.read(); i++ { + c := dec.data[i] + if isDigit(c) { + end = i + beforeDecimal = (beforeDecimal << 3) + (beforeDecimal << 1) + continue + } + dec.cursor = i + break + } + // then we add both integers + // then we divide the number by the power found + afterDecimal := dec.atoi64(start, end) + pow := pow10uint64[end-start+2] + return float64(beforeDecimal+afterDecimal) / float64(pow), nil + case ' ', '\n', '\t', '\r': + continue + case ',', '}', ']': // does not have decimal + dec.cursor = j + return float64(dec.atoi64(start, end)), nil + } + // invalid json we expect numbers, dot (single one), comma, or spaces + return 0, InvalidJSONError("Invalid JSON while parsing number") + } + return float64(dec.atoi64(start, end)), nil +} + +func (dec *Decoder) atoi64(start, end int) int64 { + var ll = end + 1 - start + var val = int64(digits[dec.data[start]]) + end = end + 1 + if ll < maxInt64Length { + for i := start + 1; i < end; i++ { + intv := int64(digits[dec.data[i]]) + val = (val << 3) + (val << 1) + intv + } + return val + } else if ll == maxInt64Length { + for i := start + 1; i < end; i++ { + intv := int64(digits[dec.data[i]]) + if val > maxInt64toMultiply { + dec.err = InvalidTypeError("Overflows int64") + return 0 + } + val = (val << 3) + (val << 1) + if maxInt64-val < intv { + dec.err = InvalidTypeError("Overflows int64") + return 0 + } + val += intv + } + } else { + dec.err = InvalidTypeError("Overflows int64") + return 0 + } + return val +} + +func (dec *Decoder) atoui64(start, end int) uint64 { + var ll = end + 1 - start + var val = uint64(digits[dec.data[start]]) + end = end + 1 + if ll < maxUint64Length { + for i := start + 1; i < end; i++ { + uintv := uint64(digits[dec.data[i]]) + val = (val << 3) + (val << 1) + uintv + } + } else if ll == maxUint64Length { + for i := start + 1; i < end; i++ { + uintv := uint64(digits[dec.data[i]]) + if val > maxUint64toMultiply { + dec.err = InvalidTypeError("Overflows uint64") + return 0 + } + val = (val << 3) + (val << 1) + if maxUint64-val < uintv { + dec.err = InvalidTypeError("Overflows uint64") + return 0 + } + val += uintv + } + } else { + dec.err = InvalidTypeError("Overflows uint64") + return 0 + } + return val +} + +func (dec *Decoder) atoi32(start, end int) int32 { + var ll = end + 1 - start + var val = int32(digits[dec.data[start]]) + end = end + 1 + // overflowing + if ll < maxInt32Length { + for i := start + 1; i < end; i++ { + intv := int32(digits[dec.data[i]]) + val = (val << 3) + (val << 1) + intv + } + } else if ll == maxInt32Length { + for i := start + 1; i < end; i++ { + intv := int32(digits[dec.data[i]]) + if val > maxInt32toMultiply { + dec.err = InvalidTypeError("Overflows int321") + return 0 + } + val = (val << 3) + (val << 1) + if maxInt32-val < intv { + dec.err = InvalidTypeError("Overflows int322") + return 0 + } + val += intv + } + } else { + dec.err = InvalidTypeError("Overflows int32") + return 0 + } + return val +} + +func (dec *Decoder) atoui32(start, end int) uint32 { + var ll = end + 1 - start + var val uint32 + val = uint32(digits[dec.data[start]]) + end = end + 1 + if ll < maxUint32Length { + for i := start + 1; i < end; i++ { + uintv := uint32(digits[dec.data[i]]) + val = (val << 3) + (val << 1) + uintv + } + } else if ll == maxUint32Length { + for i := start + 1; i < end; i++ { + uintv := uint32(digits[dec.data[i]]) + if val > maxUint32toMultiply { + dec.err = InvalidTypeError("Overflows uint32") + return 0 + } + val = (val << 3) + (val << 1) + if maxUint32-val < uintv { + dec.err = InvalidTypeError("Overflows int32") + return 0 + } + val += uintv + } + } else if ll > maxUint32Length { + dec.err = InvalidTypeError("Overflows uint32") + val = 0 + } + return val +} diff --git a/decode_number_test.go b/decode_number_test.go @@ -0,0 +1,310 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecoderIntBasic(t *testing.T) { + json := []byte(`124`) + var v int + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 124, v, "v must be equal to 124") +} +func TestDecoderIntNegative(t *testing.T) { + json := []byte(`-124`) + var v int + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, -124, v, "v must be equal to -124") +} +func TestDecoderIntNull(t *testing.T) { + json := []byte(`null`) + var v int + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int(0), v, "v must be equal to 0") +} +func TestDecoderIntInvalidType(t *testing.T) { + json := []byte(`"string"`) + var v int + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeErrorr") +} +func TestDecoderIntInvalidJSON(t *testing.T) { + json := []byte(`123n`) + var v int + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") +} +func TestDecoderIntBig(t *testing.T) { + json := []byte(`9223372036854775807`) + var v int + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 9223372036854775807, v, "v must be equal to 9223372036854775807") +} +func TestDecoderIntOverfow(t *testing.T) { + json := []byte(`9223372036854775808`) + var v int + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as int is overflowing") + assert.Equal(t, 0, v, "v must be equal to 0") +} +func TestDecoderIntOverfow2(t *testing.T) { + json := []byte(`92233720368547758089`) + var v int + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as int is overflowing") + assert.Equal(t, 0, v, "v must be equal to 0") +} + +func TestDecoderInt32Basic(t *testing.T) { + json := []byte(`124`) + var v int32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int32(124), v, "v must be equal to 124") +} +func TestDecoderInt32Negative(t *testing.T) { + json := []byte(`-124`) + var v int32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int32(-124), v, "v must be equal to -124") +} +func TestDecoderInt32Null(t *testing.T) { + json := []byte(`null`) + var v int32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int32(0), v, "v must be equal to 0") +} +func TestDecoderInt32InvalidType(t *testing.T) { + json := []byte(`"string"`) + var v int32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeErrorr") +} +func TestDecoderInt32InvalidJSON(t *testing.T) { + json := []byte(`123n`) + var v int32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") +} +func TestDecoderInt32Big(t *testing.T) { + json := []byte(`2147483647`) + var v int32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "err must not be nil as int32 does not overflow") + assert.Equal(t, int32(2147483647), v, "int32 must be equal to 2147483647") +} +func TestDecoderInt32Overflow(t *testing.T) { + json := []byte(`2147483648`) + var v int32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} +func TestDecoderInt32Overflow2(t *testing.T) { + json := []byte(`21474836483`) + var v int32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} + +func TestDecoderUint32Basic(t *testing.T) { + json := []byte(`124`) + var v uint32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint32(124), v, "v must be equal to 124") +} +func TestDecoderUint32Null(t *testing.T) { + json := []byte(`null`) + var v uint32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint32(0), v, "v must be equal to 0") +} +func TestDecoderUint32InvalidType(t *testing.T) { + json := []byte(`"string"`) + var v uint32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeErrorr") +} +func TestDecoderUint32InvalidJSON(t *testing.T) { + json := []byte(`123n`) + var v uint32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") +} +func TestDecoderUint32Big(t *testing.T) { + json := []byte(`4294967295`) + var v uint32 + err := Unmarshal(json, &v) + assert.Nil(t, err, "err must not be nil as uint32 does not overflow") + assert.Equal(t, uint32(4294967295), v, "err must be of type InvalidTypeError") +} +func TestDecoderUint32Overflow(t *testing.T) { + json := []byte(`4294967298`) + var v uint32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as uint32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} + +func TestDecoderUint32Overflow2(t *testing.T) { + json := []byte(`42949672983`) + var v uint32 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as uint32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} + +func TestDecoderInt64Basic(t *testing.T) { + json := []byte(`124`) + var v int64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int64(124), v, "v must be equal to 124") +} +func TestDecoderInt64Negative(t *testing.T) { + json := []byte(`-124`) + var v int64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int64(-124), v, "v must be equal to -124") +} +func TestDecoderInt64Null(t *testing.T) { + json := []byte(`null`) + var v int64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int64(0), v, "v must be equal to 0") +} +func TestDecoderInt64InvalidType(t *testing.T) { + json := []byte(`"string"`) + var v int64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeErrorr") +} +func TestDecoderInt64InvalidJSON(t *testing.T) { + json := []byte(`123n`) + var v int64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") +} +func TestDecoderInt64Big(t *testing.T) { + json := []byte(`9223372036854775807`) + var v int64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "err must not be nil as int64 does not overflow") + assert.Equal(t, int64(9223372036854775807), v, "err must be of type InvalidTypeError") +} +func TestDecoderInt64Overflow(t *testing.T) { + json := []byte(`9223372036854775808`) + var v int64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int64 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} +func TestDecoderInt64Overflow2(t *testing.T) { + json := []byte(`92233720368547758082`) + var v int64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int64 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} +func TestDecoderUint64Basic(t *testing.T) { + json := []byte(`124`) + var v uint64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint64(124), v, "v must be equal to 124") +} +func TestDecoderUint64Null(t *testing.T) { + json := []byte(`null`) + var v uint64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint64(0), v, "v must be equal to 0") +} +func TestDecoderUint64InvalidType(t *testing.T) { + json := []byte(`"string"`) + var v uint64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeErrorr") +} +func TestDecoderUint64InvalidJSON(t *testing.T) { + json := []byte(`123n`) + var v uint64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") +} +func TestDecoderUint64Big(t *testing.T) { + json := []byte(`18446744073709551615`) + var v uint64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "err must not be nil as uint64 does not overflow") + assert.Equal(t, uint64(18446744073709551615), v, "err must be of type InvalidTypeError") +} +func TestDecoderUint64Overflow(t *testing.T) { + json := []byte(`18446744073709551616`) + var v uint64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} +func TestDecoderUint64Overflow2(t *testing.T) { + json := []byte(`184467440737095516161`) + var v uint64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil as int32 overflows") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") +} + +func TestDecoderFloatBasic(t *testing.T) { + json := []byte(`100.11`) + var v float64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 100.11, v, "v must be equal to 100.11") +} + +func TestDecoderFloatBig(t *testing.T) { + json := []byte(`89899843.3493493`) + var v float64 + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 89899843.3493493, v, "v must be equal to 8989984340.3493493") +} + +func TestDecoderFloatInvalidType(t *testing.T) { + json := []byte(`"string"`) + var v float64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidTypeError(""), err, "err must be of type *strconv.NumError") +} + +func TestDecoderFloatInvalidJSON(t *testing.T) { + json := []byte(`hello`) + var v float64 + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_object.go b/decode_object.go @@ -0,0 +1,187 @@ +package gojay + +import ( + "fmt" + "unsafe" +) + +// DecodeObject reads the next JSON-encoded value from its input and stores it in the value pointed to by v. +// +// v must implement UnmarshalerObject. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeObject(j UnmarshalerObject) (int, error) { + keys := j.NKeys() + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + case '{': + dec.cursor = dec.cursor + 1 + for (dec.cursor < dec.length || dec.read()) && dec.keysDone < keys { + k, done, err := dec.nextKey() + if err != nil { + return 0, err + } else if done { + return dec.cursor, nil + } + err = j.UnmarshalObject(dec, k) + if err != nil { + return 0, err + } else if dec.called&1 == 0 { + err := dec.skipData() + if err != nil { + return 0, err + } + } else { + dec.keysDone++ + } + dec.called &= 0 + } + // will get to that point when keysDone is not lower than keys anymore + // in that case, we make sure cursor goes to the end of object, but we skip + // unmarshalling + if dec.child&1 != 0 { + end, err := dec.skipObject() + dec.cursor = end + return dec.cursor, err + } + return dec.cursor, nil + case 'n': + // is null + dec.cursor = dec.cursor + 4 + return dec.cursor, nil + default: + // can't unmarshall to struct + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to struct, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return 0, err + } + return dec.cursor, nil + } + } + return 0, InvalidJSONError("Invalid JSON while paring object") +} + +func (dec *Decoder) skipObject() (int, error) { + var objectsOpen = 1 + var objectsClosed = 0 + // var stringOpen byte = 0 + for j := dec.cursor; j < dec.length; j++ { + switch dec.data[j] { + case '}': + objectsClosed++ + // everything is closed return + if objectsOpen == objectsClosed { + // add char to object data + return j + 1, nil + } + case '{': + objectsOpen++ + case '"': + j++ + for ; j < dec.length; j++ { + if dec.data[j] != '"' { + continue + } + if dec.data[j-1] != '\\' { + break + } + // loop backward and count how many anti slash found + // to see if string is effectively escaped + ct := 1 + for i := j; i > 0; i-- { + if dec.data[i] != '\\' { + break + } + ct++ + } + // is pair number of slashes, quote is not escaped + if ct&1 == 0 { + break + } + } + default: + continue + } + } + return 0, nil +} + +func (dec *Decoder) nextKey() (string, bool, error) { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + case '"': + dec.cursor = dec.cursor + 1 + start, end, err := dec.getString() + if err != nil { + return "", false, err + } + var found byte + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + if dec.data[dec.cursor] == ':' { + found |= 1 + break + } + } + if found&1 != 0 { + dec.cursor++ + d := dec.data[start : end-1] + return *(*string)(unsafe.Pointer(&d)), false, nil + } + return "", false, InvalidJSONError("Invalid JSON while parsing object key") + case '}': + dec.cursor = dec.cursor + 1 + return "", true, nil + } + } + return "", false, InvalidJSONError("Invalid JSON while parsing object key") +} + +func (dec *Decoder) skipData() error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + // is null + case 'n', 't': + dec.cursor = dec.cursor + 4 + return nil + // is false + case 'f': + dec.cursor = dec.cursor + 5 + return nil + // is an object + case '{': + dec.cursor = dec.cursor + 1 + end, err := dec.skipObject() + dec.cursor = end + return err + // is string + case '"': + dec.cursor = dec.cursor + 1 + err := dec.skipString() + return err + // is array + case '[': + dec.cursor = dec.cursor + 1 + end, err := dec.skipArray() + dec.cursor = end + return err + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': + end, err := dec.skipNumber() + dec.cursor = end + return err + } + return InvalidJSONError("Invalid JSON") + } + return InvalidJSONError("Invalid JSON") +} diff --git a/decode_object_test.go b/decode_object_test.go @@ -0,0 +1,264 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestObj struct { + test int + test2 int + test3 string + test4 string + test5 float64 + testArr testSliceObj + testSubObj *TestSubObj + testSubObj2 *TestSubObj +} + +type TestSubObj struct { + test3 int + test4 int + test5 string + testSubSubObj *TestSubObj + testSubSubObj2 *TestSubObj +} + +func (t *TestSubObj) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "test": + return dec.AddInt(&t.test3) + case "test2": + return dec.AddInt(&t.test4) + case "test3": + return dec.AddString(&t.test5) + case "testSubSubObj": + t.testSubSubObj = &TestSubObj{} + return dec.AddObject(t.testSubSubObj) + case "testSubSubObj2": + t.testSubSubObj2 = &TestSubObj{} + return dec.AddObject(t.testSubSubObj2) + } + return nil +} + +func (t *TestSubObj) NKeys() int { + return 1000 +} + +func (t *TestObj) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "test": + return dec.AddInt(&t.test) + case "test2": + return dec.AddInt(&t.test2) + case "test3": + return dec.AddString(&t.test3) + case "test4": + return dec.AddString(&t.test4) + case "test5": + return dec.AddFloat(&t.test5) + case "testSubObj": + t.testSubObj = &TestSubObj{} + return dec.AddObject(t.testSubObj) + case "testSubObj2": + t.testSubObj2 = &TestSubObj{} + return dec.AddObject(t.testSubObj2) + case "testArr": + return dec.AddArray(&t.testArr) + } + return nil +} + +func (t *TestObj) NKeys() int { + return 8 +} + +func TestDecoderObject(t *testing.T) { + json := []byte(`{ + "test": 245, + "test2": 246, + "test3": "string", + "test4": "complex string with spaces and some slashes\"", + "test5": -1.15657654376543, + "testNull": null, + "testArr": [ + { + "test": 245, + "test2": 246 + }, + { + "test": 245, + "test2": 246 + } + ], + "testSubObj": { + "test": 121, + "test2": 122, + "testNull": null, + "testSubSubObj": { + "test": 150, + "testNull": null + }, + "testSubSubObj2": { + "test": 150 + }, + "test3": "string" + "testNull": null, + }, + "testSubObj2": { + "test": 122, + "test3": "string" + "testSubSubObj": { + "test": 151 + }, + "test2": 123 + } + }`) + v := &TestObj{} + err := Unmarshal(json, v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 245, v.test, "v.test must be equal to 245") + assert.Equal(t, 246, v.test2, "v.test2 must be equal to 246") + assert.Equal(t, "string", v.test3, "v.test3 must be equal to 'string'") + assert.Equal(t, "complex string with spaces and some slashes\"", v.test4, "v.test4 must be equal to 'string'") + assert.Equal(t, -1.15657654376543, v.test5, "v.test5 must be equal to 1.15") + assert.Len(t, v.testArr, 2, "v.testArr must be of len 2") + + assert.Equal(t, 121, v.testSubObj.test3, "v.testSubObj.test3 must be equal to 121") + assert.Equal(t, 122, v.testSubObj.test4, "v.testSubObj.test4 must be equal to 122") + assert.Equal(t, "string", v.testSubObj.test5, "v.testSubObj.test5 must be equal to 'string'") + assert.Equal(t, 150, v.testSubObj.testSubSubObj.test3, "v.testSubObj.testSubSubObj.test3 must be equal to 150") + assert.Equal(t, 150, v.testSubObj.testSubSubObj2.test3, "v.testSubObj.testSubSubObj2.test3 must be equal to 150") + + assert.Equal(t, 122, v.testSubObj2.test3, "v.testSubObj2.test3 must be equal to 121") + assert.Equal(t, 123, v.testSubObj2.test4, "v.testSubObj2.test4 must be equal to 122") + assert.Equal(t, "string", v.testSubObj2.test5, "v.testSubObj2.test5 must be equal to 'string'") + assert.Equal(t, 151, v.testSubObj2.testSubSubObj.test3, "v.testSubObj2.testSubSubObj.test must be equal to 150") +} + +func TestDecodeObjectNull(t *testing.T) { + json := []byte(`null`) + v := &TestObj{} + err := Unmarshal(json, v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, v.test, 0, "v.test must be 0 val") +} + +var jsonComplex = []byte(`{ + "test": "{\"test\":\"1\",\"test1\":2}", + "test2\\n": "\\\\\\\\\\\n", + "testArrSkip": ["testString with escaped \" quotes"], + "testSkipString": "skip \\ string with \\n escaped char \" ", + "testSkipNumber": 123.23, + "testBool": true, + "testSub": { + "test": "{\"test\":\"1\",\"test1\":2}", + "test2\\n": "[1,2,3]", + "test3": 1, + "testObjSkip": { + "test": "test string with escaped \" quotes" + }, + "testStrSkip" : "test" + }, + "testBoolSkip": false, + "testObjInvalidType": "somestring", + "testArrSkip2": [[],["someString"]], + "test3": 1 +}`) + +type jsonObjectComplex struct { + Test string + Test2 string + Test3 int + Test4 bool + testSub *jsonObjectComplex + testObjInvalidType *jsonObjectComplex +} + +func (j *jsonObjectComplex) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "test": + return dec.AddString(&j.Test) + case `test2\n`: + return dec.AddString(&j.Test2) + case "test3": + return dec.AddInt(&j.Test3) + case "testBool": + return dec.AddBool(&j.Test4) + case "testSub": + j.testSub = &jsonObjectComplex{} + return dec.AddObject(j.testSub) + case "testObjInvalidType": + j.testObjInvalidType = &jsonObjectComplex{} + return dec.AddObject(j.testObjInvalidType) + } + return nil +} + +func (j *jsonObjectComplex) NKeys() int { + return 6 +} + +func TestDecodeObjComplex(t *testing.T) { + result := jsonObjectComplex{} + err := UnmarshalObject(jsonComplex, &result) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, `{"test":"1","test1":2}`, result.Test, "result.Test is not expected value") + assert.Equal(t, `\\\\\\n`, result.Test2, "result.Test2 is not expected value") + assert.Equal(t, 1, result.Test3, "result.test3 is not expected value") + assert.Equal(t, `{"test":"1","test1":2}`, result.testSub.Test, "result.testSub.test is not expected value") + assert.Equal(t, `[1,2,3]`, result.testSub.Test2, "result.testSub.test2 is not expected value") + assert.Equal(t, 1, result.testSub.Test3, "result.testSub.test3 is not expected value") + assert.Equal(t, true, result.Test4, "result.Test4 is not expected value, should be true") +} + +type jsonDecodePartial struct { + Test string + Test2 string +} + +func (j *jsonDecodePartial) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "test": + return dec.AddString(&j.Test) + case `test2`: + return dec.AddString(&j.Test2) + } + return nil +} + +func (j *jsonDecodePartial) NKeys() int { + return 2 +} + +func TestDecodeObjectPartial(t *testing.T) { + result := jsonDecodePartial{} + dec := NewDecoder(nil) + dec.data = []byte(`{ + "test": "test", + "test2": "test", + "testArrSkip": ["test"], + "testSkipString": "test", + "testSkipNumber": 123.23 + }`) + dec.length = len(dec.data) + _, err := dec.DecodeObject(&result) + assert.Nil(t, err, "err should be nil") + assert.NotEqual(t, len(dec.data), dec.cursor) +} + +func TestDecoderObjectInvalidJSON(t *testing.T) { + result := jsonDecodePartial{} + dec := NewDecoder(nil) + dec.data = []byte(`{ + "test2": "test", + "testArrSkip": ["test"], + "testSkipString": "testInvalidJSON\\\\ + }`) + dec.length = len(dec.data) + _, err := dec.DecodeObject(&result) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_pool.go b/decode_pool.go @@ -0,0 +1,48 @@ +package gojay + +import "io" + +var decPool = make(chan *Decoder, 16) + +// NewDecoder returns a new decoder or borrows one from the pool +// it takes an io.Reader implementation as data input +func NewDecoder(r io.Reader) *Decoder { + return newDecoder(r, 512) +} + +func newDecoder(r io.Reader, bufSize int) *Decoder { + select { + case dec := <-decPool: + dec.called = 0 + dec.keysDone = 0 + dec.cursor = 0 + dec.err = nil + dec.r = r + dec.length = 0 + if bufSize > 0 { + dec.data = make([]byte, bufSize) + dec.length = 0 + } + return dec + default: + dec := &Decoder{ + called: 0, + cursor: 0, + keysDone: 0, + err: nil, + r: r, + } + if bufSize > 0 { + dec.data = make([]byte, bufSize) + dec.length = 0 + } + return dec + } +} + +func (dec *Decoder) addToPool() { + select { + case decPool <- dec: + default: + } +} diff --git a/decode_stream.go b/decode_stream.go @@ -0,0 +1,111 @@ +package gojay + +import ( + "io" + "time" +) + +// Stream is a struct holding the Stream api +var Stream = stream{} + +type stream struct{} + +// A StreamDecoder reads and decodes JSON values from an input stream. +// +// It implements conext.Context and provide a channel to notify interruption. +type StreamDecoder struct { + *Decoder + done chan struct{} + deadline *time.Time +} + +// NewDecoder returns a new decoder or borrows one from the pool. +// It takes an io.Reader implementation as data input. +// It initiates the done channel returned by Done(). +func (s stream) NewDecoder(r io.Reader) *StreamDecoder { + dec := newDecoder(r, 512) + streamDec := &StreamDecoder{ + Decoder: dec, + done: make(chan struct{}, 1), + } + return streamDec +} + +// DecodeStream reads the next line delimited JSON-encoded value from its input and stores it in the value pointed to by c. +// +// c must implement UnmarshalerStream. Ideally c is a channel. See example for implementation. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *StreamDecoder) DecodeStream(c UnmarshalerStream) error { + if dec.r == nil { + dec.err = NoReaderError("No reader given to decode stream") + close(dec.done) + return dec.err + } + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + default: + // char is not space start reading + for dec.nextChar() != 0 { + // calling unmarshal stream + err := c.UnmarshalStream(dec) + if err != nil { + dec.err = err + close(dec.done) + return err + } + // garbage collects buffer + // we don't want the buffer to grow extensively + dec.data = dec.data[dec.cursor:] + dec.length = dec.length - dec.cursor + dec.cursor = 0 + } + // close the done channel to signal the end of the job + close(dec.done) + return nil + } + } + return InvalidJSONError("Invalid JSON while parsing line delimited JSON") +} + +// context.Context implementation + +// Done returns a channel that's closed when work is done. +// It implements context.Context +func (dec *StreamDecoder) Done() <-chan struct{} { + return dec.done +} + +// Deadline returns the time when work done on behalf of this context +// should be canceled. Deadline returns ok==false when no deadline is +// set. Successive calls to Deadline return the same results. +func (dec *StreamDecoder) Deadline() (time.Time, bool) { + if dec.deadline != nil { + return *dec.deadline, true + } + return time.Time{}, false +} + +// SetDeadline sets the deadline +func (dec *StreamDecoder) SetDeadline(t time.Time) { + dec.deadline = &t +} + +// Err returns nil if Done is not yet closed. +// If Done is closed, Err returns a non-nil error explaining why. +// It implements context.Context +func (dec *StreamDecoder) Err() error { + select { + case <-dec.done: + return dec.err + default: + return nil + } +} + +// Value implements context.Context +func (dec *StreamDecoder) Value(key interface{}) interface{} { + return nil +} diff --git a/decode_stream_test.go b/decode_stream_test.go @@ -0,0 +1,375 @@ +package gojay + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// Basic Behaviour Tests +// +func TestDecoderImplementsContext(t *testing.T) { + var dec interface{} = &StreamDecoder{} + _ = dec.(context.Context) +} + +func TestDecodeStreamNoReader(t *testing.T) { + dec := Stream.NewDecoder(nil) + dec.done = make(chan struct{}, 1) + testChan := ChannelStreamObjects(make(chan *TestObj)) + go dec.DecodeStream(&testChan) + + select { + case <-dec.Done(): + assert.NotNil(t, dec.Err(), "dec.Err() should not be nil") + assert.Equal(t, "No reader given to decode stream", dec.Err().Error(), "dec.Err().Error() should not be 'No reader given to decode stream'") + case <-testChan: + assert.True(t, false, "should not be called as decoder should not return error right away") + } +} + +// Table Tests + +// Objects + +type StreamTestObject struct { + name string + streamReader *StreamReader + expectations func(error, []*TestObj, *testing.T) +} + +func TestStreamDecodingObjectsParallel(t *testing.T) { + var tests = []StreamTestObject{ + { + name: "Stream objects", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + {"test":246,"test2":-246,"test3":"string"} + {"test":247,"test2":248,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + `, + }, + expectations: func(err error, result []*TestObj, t *testing.T) { + assert.Nil(t, err, "err should be nil") + + assert.Equal(t, 246, result[0].test, "v[0].test should be equal to 246") + assert.Equal(t, -246, result[0].test2, "v[0].test2 should be equal to -247") + assert.Equal(t, "string", result[0].test3, "v[0].test3 should be equal to \"string\"") + + assert.Equal(t, 247, result[1].test, "result[1].test should be equal to 246") + assert.Equal(t, 248, result[1].test2, "result[1].test2 should be equal to 248") + assert.Equal(t, "string", result[1].test3, "result[1].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[2].test, "result[2].test should be equal to 777") + assert.Equal(t, 456, result[2].test2, "result[2].test2 should be equal to 456") + assert.Equal(t, "string", result[2].test3, "result[2].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[3].test, "result[3].test should be equal to 777") + assert.Equal(t, 456, result[3].test2, "result[3].test2 should be equal to 456") + assert.Equal(t, "string", result[3].test3, "result[3].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[4].test, "result[4].test should be equal to 777") + assert.Equal(t, 456, result[4].test2, "result[4].test2 should be equal to 456") + assert.Equal(t, "string", result[4].test3, "result[4].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[5].test, "result[5].test should be equal to 777") + assert.Equal(t, 456, result[5].test2, "result[5].test2 should be equal to 456") + assert.Equal(t, "string", result[5].test3, "result[5].test3 should be equal to \"string\"") + }, + }, + { + name: "Stream test objects with null values", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + {"test":246,"test2":-246,"test3":"string"} + {"test":247,"test2":248,"test3":"string"} + null + {"test":777,"test2":456,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + {"test":777,"test2":456,"test3":"string"} + `, + }, + expectations: func(err error, result []*TestObj, t *testing.T) { + assert.Nil(t, err, "err should be nil") + + assert.Equal(t, 246, result[0].test, "v[0].test should be equal to 246") + assert.Equal(t, -246, result[0].test2, "v[0].test2 should be equal to -247") + assert.Equal(t, "string", result[0].test3, "v[0].test3 should be equal to \"string\"") + + assert.Equal(t, 247, result[1].test, "result[1].test should be equal to 246") + assert.Equal(t, 248, result[1].test2, "result[1].test2 should be equal to 248") + assert.Equal(t, "string", result[1].test3, "result[1].test3 should be equal to \"string\"") + + assert.Equal(t, 0, result[2].test, "result[2].test should be equal to 0 as input is null") + assert.Equal(t, 0, result[2].test2, "result[2].test2 should be equal to 0 as input is null") + assert.Equal(t, "", result[2].test3, "result[2].test3 should be equal to \"\" as input is null") + + assert.Equal(t, 777, result[3].test, "result[3].test should be equal to 777") + assert.Equal(t, 456, result[3].test2, "result[3].test2 should be equal to 456") + assert.Equal(t, "string", result[3].test3, "result[3].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[4].test, "result[4].test should be equal to 777") + assert.Equal(t, 456, result[4].test2, "result[4].test2 should be equal to 456") + assert.Equal(t, "string", result[4].test3, "result[4].test3 should be equal to \"string\"") + + assert.Equal(t, 777, result[5].test, "result[5].test should be equal to 777") + assert.Equal(t, 456, result[5].test2, "result[5].test2 should be equal to 456") + assert.Equal(t, "string", result[5].test3, "result[5].test3 should be equal to \"string\"") + }, + }, + { + name: "Stream test starting with null values", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + null + {"test":246,"test2":-246,"test3":"string"} + {"test":247,"test2":248,"test3":"string"} + `, + }, + expectations: func(err error, result []*TestObj, t *testing.T) { + assert.Nil(t, err, "err should be nil") + + assert.Equal(t, 0, result[0].test, "result[0].test should be equal to 0 as input is null") + assert.Equal(t, 0, result[0].test2, "result[0].test2 should be equal to 0 as input is null") + assert.Equal(t, "", result[0].test3, "result[0].test3 should be equal to \"\" as input is null") + + assert.Equal(t, 246, result[1].test, "v[1].test should be equal to 246") + assert.Equal(t, -246, result[1].test2, "v[1].test2 should be equal to -247") + assert.Equal(t, "string", result[1].test3, "v[1].test3 should be equal to \"string\"") + + assert.Equal(t, 247, result[2].test, "result[2].test should be equal to 246") + assert.Equal(t, 248, result[2].test2, "result[2].test2 should be equal to 248") + assert.Equal(t, "string", result[2].test3, "result[2].test3 should be equal to \"string\"") + }, + }, + { + name: "Stream test invalid JSON", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + invalid json + {"test":246,"test2":-246,"test3":"string"} + {"test":247,"test2":248,"test3":"string"} + `, + }, + expectations: func(err error, result []*TestObj, t *testing.T) { + assert.NotNil(t, err, "err is not nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err is of type InvalidJSONError") + assert.Equal(t, "Invalid JSON", err.Error(), "err message is Invalid JSON") + }, + }, + } + for _, testCase := range tests { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + runStreamTestCaseObjects(t, testCase) + }) + } +} + +func runStreamTestCaseObjects(t *testing.T, testCase StreamTestObject) { + // create our channel which will receive our objects + testChan := ChannelStreamObjects(make(chan *TestObj)) + dec := Stream.NewDecoder(testCase.streamReader) + // start decoding (will block the goroutine until something is written to the ReadWriter) + go dec.DecodeStream(&testChan) + // start writing to the ReadWriter + go testCase.streamReader.Write() + // prepare our result + result := []*TestObj{} +loop: + for { + select { + case v := <-testChan: + result = append(result, v) + case <-dec.Done(): + break loop + } + } + testCase.expectations(dec.Err(), result, t) +} + +type ChannelStreamObjects chan *TestObj + +func (c *ChannelStreamObjects) UnmarshalStream(dec *StreamDecoder) error { + obj := &TestObj{} + if err := dec.AddObject(obj); err != nil { + return err + } + *c <- obj + return nil +} + +// Strings +type StreamTestString struct { + name string + streamReader *StreamReader + expectations func(error, []*string, *testing.T) +} + +func TestStreamDecodingStringsParallel(t *testing.T) { + var tests = []StreamTestString{ + { + name: "Stream strings basic", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + "hello" + "world" + "!" + `, + }, + expectations: func(err error, result []*string, t *testing.T) { + assert.Nil(t, err, "err should be nil") + + assert.Equal(t, "hello", *result[0], "v[0] should be equal to 'hello'") + assert.Equal(t, "world", *result[1], "v[1] should be equal to 'world'") + assert.Equal(t, "!", *result[2], "v[2] should be equal to '!'") + }, + }, + { + name: "Stream strings with null", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + "hello" + null + "!" + `, + }, + expectations: func(err error, result []*string, t *testing.T) { + assert.Nil(t, err, "err should be nil") + + assert.Equal(t, "hello", *result[0], "v[0] should be equal to 'hello'") + assert.Equal(t, "", *result[1], "v[1] should be equal to ''") + assert.Equal(t, "!", *result[2], "v[2] should be equal to '!'") + }, + }, + { + name: "Stream strings invalid JSON", + streamReader: &StreamReader{ + readChan: make(chan string), + done: make(chan struct{}), + data: ` + "hello" + world + "!" + `, + }, + expectations: func(err error, result []*string, t *testing.T) { + assert.NotNil(t, err, "err should not be nil") + + assert.IsType(t, InvalidJSONError(""), err, "err is of type InvalidJSONError") + assert.Equal(t, "Invalid JSON", err.Error(), "err message is Invalid JSON") + }, + }, + } + for _, testCase := range tests { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + runStreamTestCaseStrings(t, testCase) + }) + } +} + +func runStreamTestCaseStrings(t *testing.T, testCase StreamTestString) { + // create our channel which will receive our objects + testChan := ChannelStreamStrings(make(chan *string)) + dec := Stream.NewDecoder(testCase.streamReader) + // start decoding (will block the goroutine until something is written to the ReadWriter) + go dec.DecodeStream(&testChan) + // start writing to the ReadWriter + go testCase.streamReader.Write() + // prepare our result + result := []*string{} +loop: + for { + select { + case v := <-testChan: + result = append(result, v) + case <-dec.Done(): + break loop + } + } + testCase.expectations(dec.Err(), result, t) +} + +type ChannelStreamStrings chan *string + +func (c *ChannelStreamStrings) UnmarshalStream(dec *StreamDecoder) error { + str := "" + if err := dec.AddString(&str); err != nil { + return err + } + *c <- &str + return nil +} + +// StreamReader mocks a stream reading chunks of data +type StreamReader struct { + writeCounter int + readChan chan string + done chan struct{} + data string +} + +func (r *StreamReader) Write() { + l := len(r.data) + t := 4 + chunkSize := l / t + carry := 0 + lastWrite := 0 + for r.writeCounter < t { + time.Sleep(time.Duration(r.writeCounter*100) * time.Millisecond) + currentChunkStart := (chunkSize) * r.writeCounter + lastWrite := currentChunkStart + chunkSize + r.readChan <- r.data[currentChunkStart:lastWrite] + carry = l - lastWrite + r.writeCounter++ + } + if carry > 0 { + r.readChan <- r.data[lastWrite:] + } + r.done <- struct{}{} +} + +func (r *StreamReader) Read(b []byte) (int, error) { + select { + case v := <-r.readChan: + n := copy(b, v) + return n, nil + case <-r.done: + return 0, nil + } +} + +// Deadline test +func TestStreamDecodingDeadline(t *testing.T) { + dec := Stream.NewDecoder(&StreamReader{}) + now := time.Now() + dec.SetDeadline(now) + deadline, _ := dec.Deadline() + assert.Equal(t, now.String(), deadline.String(), "dec.now and now should be equal") + assert.Equal(t, now.String(), dec.deadline.String(), "dec.now and now should be equal") +} + +func TestStreamDecodingErrNotSet(t *testing.T) { + dec := Stream.NewDecoder(&StreamReader{}) + assert.Nil(t, dec.Err(), "dec.Err should be nim") +} diff --git a/decode_string.go b/decode_string.go @@ -0,0 +1,174 @@ +package gojay + +import ( + "fmt" + "unsafe" +) + +// DecodeString reads the next JSON-encoded value from its input and stores it in the string pointed to by v. +// +// See the documentation for Unmarshal for details about the conversion of JSON into a Go value. +func (dec *Decoder) DecodeString(v *string) error { + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + // is string + continue + case '"': + dec.cursor = dec.cursor + 1 + start, end, err := dec.getString() + if err != nil { + return err + } + // we do minus one to remove the last quote + d := dec.data[start : end-1] + *v = *(*string)(unsafe.Pointer(&d)) + dec.cursor = end + return nil + // is nil + case 'n': + dec.cursor = dec.cursor + 4 + return nil + default: + dec.err = InvalidTypeError( + fmt.Sprintf( + "Cannot unmarshall to string, wrong char '%s' found at pos %d", + string(dec.data[dec.cursor]), + dec.cursor, + ), + ) + err := dec.skipData() + if err != nil { + return err + } + return nil + } + } + return nil +} + +func (dec *Decoder) parseEscapedString() error { + // know where to stop slash + start := dec.cursor + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + if dec.data[dec.cursor] != '\\' { + d := dec.data[dec.cursor] + dec.cursor = dec.cursor + 1 + nSlash := dec.cursor - start + switch d { + case '"': + // nSlash must be odd + if nSlash&1 != 1 { + return InvalidJSONError("Invalid JSON unescaped character") + } + diff := (nSlash - 1) >> 1 + dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) + dec.length = len(dec.data) + dec.cursor -= nSlash - diff + return nil + case 'n', 'r', 't': + // number of slash must be even + // if is odd number of slashes + // divide nSlash - 1 by 2 and leave last one + // else divide nSlash by 2 and leave the letter + var diff int + if nSlash&1 == 1 { + diff = (nSlash - 1) >> 1 + dec.data = append(dec.data[:start+diff], dec.data[dec.cursor-1:]...) + } else { + diff = nSlash >> 1 + dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) + } + dec.length = len(dec.data) + dec.cursor -= nSlash - diff + return nil + default: + // nSlash must be even + if nSlash&1 == 1 { + return InvalidJSONError("Invalid JSON unescaped character") + } + diff := nSlash >> 1 + dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) + dec.length = len(dec.data) + dec.cursor -= (nSlash - diff) + return nil + } + } + } + return nil +} + +func (dec *Decoder) getString() (int, int, error) { + // extract key + var keyStart = dec.cursor + // var str *Builder + for dec.cursor < dec.length || dec.read() { + switch dec.data[dec.cursor] { + // string found + case '"': + dec.cursor = dec.cursor + 1 + return keyStart, dec.cursor, nil + // slash found + case '\\': + dec.cursor = dec.cursor + 1 + err := dec.parseEscapedString() + if err != nil { + return 0, 0, err + } + default: + dec.cursor = dec.cursor + 1 + continue + } + } + return 0, 0, InvalidJSONError("Invalid JSON while parsing string") +} + +func (dec *Decoder) skipEscapedString() error { + start := dec.cursor + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + if dec.data[dec.cursor] != '\\' { + d := dec.data[dec.cursor] + dec.cursor = dec.cursor + 1 + nSlash := dec.cursor - start + switch d { + case '"': + // nSlash must be odd + if nSlash&1 != 1 { + return InvalidJSONError("Invalid JSON unescaped character") + } + return nil + case 'n', 'r', 't': + return nil + default: + // nSlash must be even + if nSlash&1 == 1 { + return InvalidJSONError("Invalid JSON unescaped character") + } + return nil + } + } + } + return nil +} + +func (dec *Decoder) skipString() error { + for dec.cursor < dec.length || dec.read() { + switch dec.data[dec.cursor] { + // string found + case '"': + dec.cursor = dec.cursor + 1 + return nil + // slash found + case '\\': + dec.cursor = dec.cursor + 1 + err := dec.skipEscapedString() + if err != nil { + return err + } + default: + dec.cursor = dec.cursor + 1 + continue + } + } + return InvalidJSONError("Invalid JSON while parsing string") +} diff --git a/decode_string_test.go b/decode_string_test.go @@ -0,0 +1,47 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecoderStringBasic(t *testing.T) { + json := []byte(`"string"`) + var v string + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, "string", v, "v must be equal to 'string'") +} + +func TestDecoderStringComplex(t *testing.T) { + json := []byte(` "string with spaces and \"escape\"d \"quotes\" and escaped line returns \\n and escaped \\\\ escaped char"`) + var v string + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, "string with spaces and \"escape\"d \"quotes\" and escaped line returns \\n and escaped \\\\ escaped char", v, "v is not equal to the value expected") +} + +func TestDecoderStringNull(t *testing.T) { + json := []byte(`null`) + var v string + err := Unmarshal(json, &v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, "", v, "v must be equal to ''") +} + +func TestDecoderStringInvalidJSON(t *testing.T) { + json := []byte(`"invalid JSONs`) + var v string + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} + +func TestDecoderStringInvalidType(t *testing.T) { + json := []byte(`1`) + var v string + err := Unmarshal(json, &v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidTypeError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_test.go b/decode_test.go @@ -0,0 +1,498 @@ +package gojay + +import ( + "fmt" + "io" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testDecodeObj struct { + test string +} + +func (t *testDecodeObj) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "test": + return dec.AddString(&t.test) + } + return nil +} +func (t *testDecodeObj) NKeys() int { + return 1 +} + +type testDecodeSlice []*testDecodeObj + +func (t *testDecodeSlice) UnmarshalArray(dec *Decoder) error { + obj := &testDecodeObj{} + if err := dec.AddObject(obj); err != nil { + return err + } + *t = append(*t, obj) + return nil +} + +// Unmarshal tests +func TestUnmarshalAllTypes(t *testing.T) { + testCases := []struct { + name string + v interface{} + d []byte + expectations func(err error, v interface{}, t *testing.T) + }{ + { + v: new(string), + d: []byte(`"test string"`), + name: "test decode string", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*string) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "test string", *vt, "v must be equal to 1") + }, + }, + { + v: new(string), + d: []byte(`null`), + name: "test decode string null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*string) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", *vt, "v must be equal to 1") + }, + }, + { + v: new(int), + d: []byte(`1`), + name: "test decode int", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, 1, *vt, "v must be equal to 1") + }, + }, + { + v: new(int64), + d: []byte(`1`), + name: "test decode int64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, int64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint64), + d: []byte(`1`), + name: "test decode uint64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint64), + d: []byte(`-1`), + name: "test decode uint64 negative", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(int32), + d: []byte(`1`), + name: "test decode int32", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, int32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint32), + d: []byte(`1`), + name: "test decode uint32", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint32), + d: []byte(`-1`), + name: "test decode uint32 negative", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(float64), + d: []byte(`1.15`), + name: "test decode float64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*float64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, float64(1.15), *vt, "v must be equal to 1") + }, + }, + { + v: new(float64), + d: []byte(`null`), + name: "test decode float64 null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*float64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, float64(0), *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + d: []byte(`true`), + name: "test decode bool true", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, true, *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + d: []byte(`false`), + name: "test decode bool false", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, false, *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + d: []byte(`null`), + name: "test decode bool null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, false, *vt, "v must be equal to 1") + }, + }, + { + v: new(testDecodeObj), + d: []byte(`{"test":"test"}`), + name: "test decode object", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "test", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeObj), + d: []byte(`{"test":null}`), + name: "test decode object null key", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeObj), + d: []byte(`null`), + name: "test decode object null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeSlice), + d: []byte(`[{"test":"test"}]`), + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 1, "len of vt must be 1") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + }, + }, + { + v: new(testDecodeSlice), + d: []byte(`[{"test":"test"},{"test":"test2"}]`), + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 2, "len of vt must be 2") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + assert.Equal(t, "test2", vt[1].test, "vt[1].test must be equal to 'test2'") + }, + }, + { + v: new(struct{}), + d: []byte(`{"test":"test"}`), + name: "test decode invalid type", + expectations: func(err error, v interface{}, t *testing.T) { + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidUnmarshalError(""), err, "err must be of type InvalidUnmarshalError") + assert.Equal(t, fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(v).String()), err.Error(), "err message should be equal to invalidUnmarshalErrorMsg") + }, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(*testing.T) { + err := Unmarshal(testCase.d, testCase.v) + testCase.expectations(err, testCase.v, t) + }) + } +} + +// Decode tests + +type TestReader struct { + data string + done bool +} + +func (r *TestReader) Read(b []byte) (int, error) { + if !r.done { + n := copy(b, r.data) + r.done = true + return n, nil + } + return 0, io.EOF +} + +func TestDecodeAllTypes(t *testing.T) { + testCases := []struct { + name string + v interface{} + r TestReader + expectations func(err error, v interface{}, t *testing.T) + }{ + { + v: new(string), + r: TestReader{`"test string"`, false}, + name: "test decode string", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*string) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "test string", *vt, "v must be equal to 1") + }, + }, + { + v: new(string), + r: TestReader{`null`, false}, + name: "test decode string null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*string) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", *vt, "v must be equal to 1") + }, + }, + { + v: new(int), + r: TestReader{`1`, false}, + name: "test decode int", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, 1, *vt, "v must be equal to 1") + }, + }, + { + v: new(int64), + r: TestReader{`1`, false}, + name: "test decode int64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, int64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint64), + r: TestReader{`1`, false}, + name: "test decode uint64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint64), + r: TestReader{`-1`, false}, + name: "test decode uint64 negative", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint64(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(int32), + r: TestReader{`1`, false}, + name: "test decode int32", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*int32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, int32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint32), + r: TestReader{`1`, false}, + name: "test decode uint32", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(uint32), + r: TestReader{`-1`, false}, + name: "test decode uint32 negative", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*uint32) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, uint32(1), *vt, "v must be equal to 1") + }, + }, + { + v: new(float64), + r: TestReader{`1.15`, false}, + name: "test decode float64", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*float64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, float64(1.15), *vt, "v must be equal to 1") + }, + }, + { + v: new(float64), + r: TestReader{`null`, false}, + name: "test decode float64 null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*float64) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, float64(0), *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + r: TestReader{`true`, false}, + name: "test decode bool true", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, true, *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + r: TestReader{`false`, false}, + name: "test decode bool false", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, false, *vt, "v must be equal to 1") + }, + }, + { + v: new(bool), + r: TestReader{`null`, false}, + name: "test decode bool null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*bool) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, false, *vt, "v must be equal to 1") + }, + }, + { + v: new(testDecodeObj), + r: TestReader{`{"test":"test"}`, false}, + name: "test decode object", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "test", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeObj), + r: TestReader{`{"test":null}`, false}, + name: "test decode object null key", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeObj), + r: TestReader{`null`, false}, + name: "test decode object null", + expectations: func(err error, v interface{}, t *testing.T) { + vt := v.(*testDecodeObj) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "", vt.test, "v.test must be equal to 'test'") + }, + }, + { + v: new(testDecodeSlice), + r: TestReader{`[{"test":"test"}]`, false}, + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 1, "len of vt must be 1") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + }, + }, + { + v: new(testDecodeSlice), + r: TestReader{`[{"test":"test"},{"test":"test2"}]`, false}, + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 2, "len of vt must be 2") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + assert.Equal(t, "test2", vt[1].test, "vt[1].test must be equal to 'test2'") + }, + }, + { + v: new(struct{}), + r: TestReader{`{"test":"test"}`, false}, + name: "test decode invalid type", + expectations: func(err error, v interface{}, t *testing.T) { + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidUnmarshalError(""), err, "err must be of type InvalidUnmarshalError") + assert.Equal(t, fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(v).String()), err.Error(), "err message should be equal to invalidUnmarshalErrorMsg") + }, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(*testing.T) { + dec := NewDecoder(&testCase.r) + err := dec.Decode(testCase.v) + testCase.expectations(err, testCase.v, t) + }) + } +} diff --git a/encode.go b/encode.go @@ -0,0 +1,196 @@ +package gojay + +// MarshalObject returns the JSON encoding of v. +// +// It takes a struct implementing Marshaler to a JSON slice of byte +// it returns a slice of bytes and an error. +// Example with an Marshaler: +// type TestStruct struct { +// id int +// } +// func (s *TestStruct) MarshalObject(enc *gojay.Encoder) { +// enc.AddIntKey("id", s.id) +// } +// func (s *TestStruct) IsNil() bool { +// return s == nil +// } +// +// func main() { +// test := &TestStruct{ +// id: 123456, +// } +// b, _ := gojay.Marshal(test) +// fmt.Println(b) // {"id":123456} +// } +func MarshalObject(v MarshalerObject) ([]byte, error) { + enc := NewEncoder() + enc.grow(200) + enc.writeByte('{') + v.MarshalObject(enc) + enc.writeByte('}') + defer enc.addToPool() + return enc.buf, nil +} + +// MarshalArray returns the JSON encoding of v. +// +// It takes an array or a slice implementing Marshaler to a JSON slice of byte +// it returns a slice of bytes and an error. +// Example with an Marshaler: +// type TestSlice []*TestStruct +// +// func (t TestSlice) MarshalArray(enc *Encoder) { +// for _, e := range t { +// enc.AddObject(e) +// } +// } +// +// func main() { +// test := &TestSlice{ +// &TestStruct{123456}, +// &TestStruct{7890}, +// } +// b, _ := Marshal(test) +// fmt.Println(b) // [{"id":123456},{"id":7890}] +// } +func MarshalArray(v MarshalerArray) ([]byte, error) { + enc := NewEncoder() + enc.grow(200) + enc.writeByte('[') + v.(MarshalerArray).MarshalArray(enc) + enc.writeByte(']') + defer enc.addToPool() + return enc.buf, nil +} + +// Marshal returns the JSON encoding of v. +// +// Marshal takes interface v and encodes it according to its type. +// Basic example with a string: +// b, err := gojay.Marshal("test") +// fmt.Println(b) // "test" +// +// If v implements Marshaler or Marshaler interface +// it will call the corresponding methods. +// +// If a struct, slice, or array is passed and does not implement these interfaces +// it will return a a non nil InvalidTypeError error. +// Example with an Marshaler: +// type TestStruct struct { +// id int +// } +// func (s *TestStruct) MarshalObject(enc *gojay.Encoder) { +// enc.AddIntKey("id", s.id) +// } +// func (s *TestStruct) IsNil() bool { +// return s == nil +// } +// +// func main() { +// test := &TestStruct{ +// id: 123456, +// } +// b, _ := gojay.Marshal(test) +// fmt.Println(b) // {"id":123456} +// } +func Marshal(v interface{}) ([]byte, error) { + var b []byte + var err error = InvalidTypeError("Unknown type to Marshal") + switch vt := v.(type) { + case MarshalerObject: + enc := NewEncoder() + enc.writeByte('{') + vt.MarshalObject(enc) + enc.writeByte('}') + b = enc.buf + defer enc.addToPool() + return b, nil + case MarshalerArray: + enc := NewEncoder() + enc.writeByte('[') + vt.MarshalArray(enc) + enc.writeByte(']') + b = enc.buf + defer enc.addToPool() + return b, nil + case string: + enc := NewEncoder() + b, err = enc.encodeString(vt) + defer enc.addToPool() + case bool: + enc := NewEncoder() + err = enc.AddBool(vt) + b = enc.buf + defer enc.addToPool() + case int: + enc := NewEncoder() + b, err = enc.encodeInt(int64(vt)) + defer enc.addToPool() + case int64: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(vt) + case int32: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case int16: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case int8: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case uint64: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case uint32: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case uint16: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeInt(int64(vt)) + case uint8: + enc := NewEncoder() + b, err = enc.encodeInt(int64(vt)) + defer enc.addToPool() + case float64: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeFloat(vt) + case float32: + enc := NewEncoder() + defer enc.addToPool() + return enc.encodeFloat(float64(vt)) + } + return b, err +} + +// MarshalerObject is the interface to implement for struct to be encoded +type MarshalerObject interface { + MarshalObject(enc *Encoder) + IsNil() bool +} + +// MarshalerArray is the interface to implement +// for a slice or an array to be encoded +type MarshalerArray interface { + MarshalArray(enc *Encoder) +} + +// An Encoder writes JSON values to an output stream. +type Encoder struct { + buf []byte +} + +func (enc *Encoder) getPreviousRune() (byte, bool) { + last := len(enc.buf) - 1 + if last < 0 { + return 0, false + } + return enc.buf[last], true +} diff --git a/encode_array.go b/encode_array.go @@ -0,0 +1,30 @@ +package gojay + +// AddArray adds an array or slice to be encoded, must be used inside a slice or array encoding (does not encode a key) +// value must implement Marshaler +func (enc *Encoder) AddArray(value MarshalerArray) error { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('[') + value.MarshalArray(enc) + enc.writeByte(']') + return nil +} + +// AddArrayKey adds an array or slice to be encoded, must be used inside an object as it will encode a key +// value must implement Marshaler +func (enc *Encoder) AddArrayKey(key string, value MarshalerArray) error { + // grow to avoid allocs (length of key/value + quotes) + r, ok := enc.getPreviousRune() + if ok && r != '[' && r != '{' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKeyArr) + value.MarshalArray(enc) + enc.writeByte(']') + return nil +} diff --git a/encode_array_test.go b/encode_array_test.go @@ -0,0 +1,106 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestEncodingArr []*TestEncoding + +func (t TestEncodingArr) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddObject(e) + } +} +func TestEncoderArrayObjects(t *testing.T) { + v := &TestEncodingArr{ + &TestEncoding{ + test: "hello world", + test2: "漢字", + testInt: 1, + testBool: true, + testInterface: 1, + sub: &SubObject{ + test1: 10, + test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, + }, + }, + &TestEncoding{ + test: "hello world", + test2: "漢字", + testInt: 1, + testBool: true, + sub: &SubObject{ + test1: 10, + test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, + }, + }, + } + r, err := Marshal(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `[{"test":"hello world","test2":"漢字","testInt":1,"testBool":true,`+ + `"testArr":[],"testF64":0,"testF32":0,"testInterface":1,"sub":{"test1":10,"test2":"hello world",`+ + `"test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world",`+ + `"test3":0,"testBool":false}}},{"test":"hello world","test2":"漢字","testInt":1,`+ + `"testBool":true,"testArr":[],"testF64":0,"testF32":0,"sub":{"test1":10,"test2":"hello world","test3":1.23543,`+ + `"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false}}}]`, + string(r), + "Result of marshalling is different as the one expected") +} + +type testEncodingArrInterfaces []interface{} + +func (t testEncodingArrInterfaces) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddInterface(e) + } +} + +func TestEncoderArrayInterfaces(t *testing.T) { + v := &testEncodingArrInterfaces{ + 1, + int64(1), + int32(1), + int16(1), + int8(1), + uint64(1), + uint32(1), + uint16(1), + uint8(1), + float64(1.31), + // float32(1.31), + &TestEncodingArr{}, + true, + "test", + &TestEncoding{ + test: "hello world", + test2: "foobar", + testInt: 1, + testBool: true, + }, + } + r, err := MarshalArray(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `[1,1,1,1,1,1,1,1,1.31,[],true,"test",{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[],"testF64":0,"testF32":0}]`, + string(r), + "Result of marshalling is different as the one expected") +} diff --git a/encode_bool.go b/encode_bool.go @@ -0,0 +1,30 @@ +package gojay + +import "strconv" + +// AddBool adds a bool to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddBool(value bool) error { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + if value { + enc.writeString("true") + } else { + enc.writeString("false") + } + return nil +} + +// AddBoolKey adds a bool to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddBoolKey(key string, value bool) error { + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKey) + enc.buf = strconv.AppendBool(enc.buf, value) + return nil +} diff --git a/encode_builder.go b/encode_builder.go @@ -0,0 +1,37 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gojay + +// grow grows b's capacity, if necessary, to guarantee space for +// another n bytes. After grow(n), at least n bytes can be written to b +// without another allocation. If n is negative, grow panics. +func (enc *Encoder) grow(n int) { + if n < 0 { + panic("Builder.grow: negative count") + } + if cap(enc.buf)-len(enc.buf) < n { + Buf := make([]byte, len(enc.buf), 2*cap(enc.buf)+n) + copy(Buf, enc.buf) + enc.buf = Buf + } +} + +// Write appends the contents of p to b's Buffer. +// Write always returns len(p), nil. +func (enc *Encoder) write(p []byte) { + enc.buf = append(enc.buf, p...) +} + +// WriteByte appends the byte c to b's Buffer. +// The returned error is always nil. +func (enc *Encoder) writeByte(c byte) { + enc.buf = append(enc.buf, c) +} + +// WriteString appends the contents of s to b's Buffer. +// It returns the length of s and a nil error. +func (enc *Encoder) writeString(s string) { + enc.buf = append(enc.buf, s...) +} diff --git a/encode_interface.go b/encode_interface.go @@ -0,0 +1,75 @@ +package gojay + +// AddInterface adds an interface{} to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddInterface(value interface{}) error { + switch value.(type) { + case string: + return enc.AddString(value.(string)) + case bool: + return enc.AddBool(value.(bool)) + case MarshalerArray: + return enc.AddArray(value.(MarshalerArray)) + case MarshalerObject: + return enc.AddObject(value.(MarshalerObject)) + case int: + return enc.AddInt(value.(int)) + case int64: + return enc.AddInt(int(value.(int64))) + case int32: + return enc.AddInt(int(value.(int32))) + case int8: + return enc.AddInt(int(value.(int8))) + case uint64: + return enc.AddInt(int(value.(uint64))) + case uint32: + return enc.AddInt(int(value.(uint32))) + case uint16: + return enc.AddInt(int(value.(uint16))) + case uint8: + return enc.AddInt(int(value.(uint8))) + case float64: + return enc.AddFloat(value.(float64)) + case float32: + return enc.AddFloat(float64(value.(float32))) + } + + return nil +} + +// AddInterfaceKey adds an interface{} to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddInterfaceKey(key string, value interface{}) error { + switch value.(type) { + case string: + return enc.AddStringKey(key, value.(string)) + case bool: + return enc.AddBoolKey(key, value.(bool)) + case MarshalerArray: + return enc.AddArrayKey(key, value.(MarshalerArray)) + case MarshalerObject: + return enc.AddObjectKey(key, value.(MarshalerObject)) + case int: + return enc.AddIntKey(key, value.(int)) + case int64: + return enc.AddIntKey(key, int(value.(int64))) + case int32: + return enc.AddIntKey(key, int(value.(int32))) + case int16: + return enc.AddIntKey(key, int(value.(int16))) + case int8: + return enc.AddIntKey(key, int(value.(int8))) + case uint64: + return enc.AddIntKey(key, int(value.(uint64))) + case uint32: + return enc.AddIntKey(key, int(value.(uint32))) + case uint16: + return enc.AddIntKey(key, int(value.(uint16))) + case uint8: + return enc.AddIntKey(key, int(value.(uint8))) + case float64: + return enc.AddFloatKey(key, value.(float64)) + case float32: + return enc.AddFloat32Key(key, value.(float32)) + } + + return nil +} diff --git a/encode_number.go b/encode_number.go @@ -0,0 +1,81 @@ +package gojay + +import "strconv" + +// encodeInt encodes an int to JSON +func (enc *Encoder) encodeInt(n int64) ([]byte, error) { + s := strconv.Itoa(int(n)) + enc.writeString(s) + return enc.buf, nil +} + +// encodeFloat encodes a float64 to JSON +func (enc *Encoder) encodeFloat(n float64) ([]byte, error) { + s := strconv.FormatFloat(n, 'f', -1, 64) + enc.writeString(s) + return enc.buf, nil +} + +// AddInt adds an int to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddInt(value int) error { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendInt(enc.buf, int64(value), 10) + return nil +} + +// AddFloat adds a float64 to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddFloat(value float64) error { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendFloat(enc.buf, value, 'f', -1, 64) + + return nil +} + +// AddIntKey adds an int to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddIntKey(key string, value int) error { + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKey) + enc.buf = strconv.AppendInt(enc.buf, int64(value), 10) + + return nil +} + +// AddFloatKey adds a float64 to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddFloatKey(key string, value float64) error { + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKey) + enc.buf = strconv.AppendFloat(enc.buf, value, 'f', -1, 64) + + return nil +} + +// AddFloat32Key adds a float32 to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddFloat32Key(key string, value float32) error { + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeByte('"') + enc.writeByte(':') + enc.buf = strconv.AppendFloat(enc.buf, float64(value), 'f', -1, 32) + + return nil +} diff --git a/encode_number_test.go b/encode_number_test.go @@ -0,0 +1,103 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncoderInt(t *testing.T) { + r, err := Marshal(1) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderInt64(t *testing.T) { + r, err := Marshal(int64(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderInt32(t *testing.T) { + r, err := Marshal(int32(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderInt16(t *testing.T) { + r, err := Marshal(int16(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderInt8(t *testing.T) { + r, err := Marshal(int8(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderUint64(t *testing.T) { + r, err := Marshal(uint64(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} +func TestEncoderUint32(t *testing.T) { + r, err := Marshal(uint32(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} +func TestEncoderUint16(t *testing.T) { + r, err := Marshal(uint16(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} +func TestEncoderUint8(t *testing.T) { + r, err := Marshal(uint8(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") +} +func TestEncoderFloat(t *testing.T) { + r, err := Marshal(1.1) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1.1`, + string(r), + "Result of marshalling is different as the one expected") +} diff --git a/encode_object.go b/encode_object.go @@ -0,0 +1,40 @@ +package gojay + +var objKeyStr = []byte(`":"`) +var objKeyObj = []byte(`":{`) +var objKeyArr = []byte(`":[`) +var objKey = []byte(`":`) + +// AddObject adds an object to be encoded, must be used inside a slice or array encoding (does not encode a key) +// value must implement Marshaler +func (enc *Encoder) AddObject(value MarshalerObject) error { + if value.IsNil() { + return nil + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('{') + value.MarshalObject(enc) + enc.writeByte('}') + return nil +} + +// AddObjectKey adds a struct to be encoded, must be used inside an object as it will encode a key +// value must implement Marshaler +func (enc *Encoder) AddObjectKey(key string, value MarshalerObject) error { + if value.IsNil() { + return nil + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKeyObj) + value.MarshalObject(enc) + enc.writeByte('}') + return nil +} diff --git a/encode_object_test.go b/encode_object_test.go @@ -0,0 +1,249 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type testObject struct { + testStr string + testInt int + testInt64 int64 + testInt32 int32 + testInt16 int16 + testInt8 int8 + testUint64 uint64 + testUint32 uint32 + testUint16 uint16 + testUint8 uint8 + testFloat64 float64 + testFloat32 float32 + testBool bool +} + +func (t *testObject) IsNil() bool { + return t == nil +} + +func (t *testObject) MarshalObject(enc *Encoder) { + enc.AddStringKey("testStr", t.testStr) + enc.AddIntKey("testInt", t.testInt) + enc.AddIntKey("testInt64", int(t.testInt64)) + enc.AddIntKey("testInt32", int(t.testInt32)) + enc.AddIntKey("testInt16", int(t.testInt16)) + enc.AddIntKey("testInt8", int(t.testInt8)) + enc.AddIntKey("testUint64", int(t.testUint64)) + enc.AddIntKey("testUint32", int(t.testUint32)) + enc.AddIntKey("testUint16", int(t.testUint16)) + enc.AddIntKey("testUint8", int(t.testUint8)) + enc.AddFloatKey("testFloat64", t.testFloat64) + enc.AddFloat32Key("testFloat32", t.testFloat32) + enc.AddBoolKey("testBool", t.testBool) +} + +func TestEncodeBasicObject(t *testing.T) { + r, err := Marshal(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"testStr":"漢字","testInt":1,"testInt64":1,"testInt32":1,"testInt16":1,"testInt8":1,"testUint64":1,"testUint32":1,"testUint16":1,"testUint8":1,"testFloat64":1.1,"testFloat32":1.1,"testBool":true}`, + string(r), + "Result of marshalling is different as the one expected", + ) +} + +type TestEncoding struct { + test string + test2 string + testInt int + testBool bool + testF32 float32 + testF64 float64 + testInterface interface{} + testArr TestEncodingArr + sub *SubObject +} + +func (t *TestEncoding) IsNil() bool { + return t == nil +} + +func (t *TestEncoding) MarshalObject(enc *Encoder) { + enc.AddStringKey("test", t.test) + enc.AddStringKey("test2", t.test2) + enc.AddIntKey("testInt", t.testInt) + enc.AddBoolKey("testBool", t.testBool) + enc.AddArrayKey("testArr", t.testArr) + enc.AddInterfaceKey("testF64", t.testF64) + enc.AddInterfaceKey("testF32", t.testF32) + enc.AddInterfaceKey("testInterface", t.testInterface) + enc.AddObjectKey("sub", t.sub) +} + +type SubObject struct { + test1 int + test2 string + test3 float64 + testBool bool + sub *SubObject +} + +func (t *SubObject) IsNil() bool { + return t == nil +} + +func (t *SubObject) MarshalObject(enc *Encoder) { + enc.AddIntKey("test1", t.test1) + enc.AddStringKey("test2", t.test2) + enc.AddFloatKey("test3", t.test3) + enc.AddBoolKey("testBool", t.testBool) + enc.AddObjectKey("sub", t.sub) +} + +func TestEncoderComplexObject(t *testing.T) { + v := &TestEncoding{ + test: "hello world", + test2: "foobar", + testInt: 1, + testBool: true, + testF32: 120.53, + testF64: 120.15, + testInterface: true, + testArr: TestEncodingArr{ + &TestEncoding{ + test: "1", + }, + }, + sub: &SubObject{ + test1: 10, + test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, + }, + } + r, err := MarshalObject(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[{"test":"1","test2":"","testInt":0,"testBool":false,"testArr":[],"testF64":0,"testF32":0}],"testF64":120.15,"testF32":120.53,"testInterface":true,"sub":{"test1":10,"test2":"hello world","test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false}}}`, + string(r), + "Result of marshalling is different as the one expected", + ) +} + +type testEncodingObjInterfaces struct { + interfaceVal interface{} +} + +func (t *testEncodingObjInterfaces) IsNil() bool { + return t == nil +} + +func (t *testEncodingObjInterfaces) MarshalObject(enc *Encoder) { + enc.AddInterfaceKey("interfaceVal", t.interfaceVal) +} + +func TestObjInterfaces(t *testing.T) { + v := testEncodingObjInterfaces{"string"} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":"string"}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{1} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{int64(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{int32(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{int16(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{int8(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{uint64(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{uint32(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{uint16(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{uint8(1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{float64(1.1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1.1}`, + string(r), + "Result of marshalling is different as the one expected") + v = testEncodingObjInterfaces{float32(1.1)} + r, err = Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1.1}`, + string(r), + "Result of marshalling is different as the one expected") +} diff --git a/encode_pool.go b/encode_pool.go @@ -0,0 +1,21 @@ +package gojay + +var encObjPool = make(chan *Encoder, 16) + +// NewEncoder returns a new encoder or borrows one from the pool +func NewEncoder() *Encoder { + select { + case enc := <-encObjPool: + return enc + default: + return &Encoder{} + } +} + +func (enc *Encoder) addToPool() { + enc.buf = nil + select { + case encObjPool <- enc: + default: + } +} diff --git a/encode_string.go b/encode_string.go @@ -0,0 +1,38 @@ +package gojay + +// encodeString encodes a string to +func (enc *Encoder) encodeString(s string) ([]byte, error) { + enc.writeByte('"') + enc.writeString(s) + enc.writeByte('"') + return enc.buf, nil +} + +// AddString adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddString(value string) error { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(value) + enc.writeByte('"') + + return nil +} + +// AddStringKey adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddStringKey(key, value string) error { + // grow to avoid allocs (length of key/value + quotes) + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.write(objKeyStr) + enc.writeString(value) + enc.writeByte('"') + + return nil +} +\ No newline at end of file diff --git a/encode_string_test.go b/encode_string_test.go @@ -0,0 +1,27 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncoderString(t *testing.T) { + r, err := Marshal("string") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"string"`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderStringUTF8(t *testing.T) { + r, err := Marshal("漢字") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"漢字"`, + string(r), + "Result of marshalling is different as the one expected") +} diff --git a/encode_test.go b/encode_test.go @@ -0,0 +1 @@ +package gojay diff --git a/errors.go b/errors.go @@ -0,0 +1,35 @@ +package gojay + +// InvalidJSONError is a type representing an error returned when +// Decoding encounters invalid JSON. +type InvalidJSONError string + +func (err InvalidJSONError) Error() string { + return string(err) +} + +// InvalidTypeError is a type representing an error returned when +// Decoding cannot unmarshal JSON to the receiver type for various reasons. +type InvalidTypeError string + +func (err InvalidTypeError) Error() string { + return string(err) +} + +const invalidUnmarshalErrorMsg = "Invalid type %s provided to Unmarshal" + +// InvalidUnmarshalError is a type representing an error returned when +// Decoding did not find the proper way to decode +type InvalidUnmarshalError string + +func (err InvalidUnmarshalError) Error() string { + return string(err) +} + +// NoReaderError is a type representing an error returned when +// decoding requires a reader and none was given +type NoReaderError string + +func (err NoReaderError) Error() string { + return string(err) +} diff --git a/gojay.go b/gojay.go @@ -0,0 +1,7 @@ +// Package gojay Package implements encoding and decoding of JSON as defined in RFC 7159. +// The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// It aims at performance and usability by relying on simple interfaces +// to decode or encode structures, slices, arrays and even channels. +package gojay