Skip to content

Commit c998855

Browse files
Merge pull request #2 from akshaybharambe14/ab_Add_DecodeAPIs
Add: DecodeBytes ans DecodeString APIs
2 parents d388e8e + cffe9d3 commit c998855

File tree

4 files changed

+200
-16
lines changed

4 files changed

+200
-16
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Gets converted to (spaces omitted)
3131
{ "string": "foo", "bool": false, "number": 42, "array": [1, 2, 3] }
3232
```
3333

34+
## Motivation
35+
36+
[jsonc](https://github.com/muhammadmuzzammil1998/jsonc) is great. But this package provides significant performance improvements and simple API to use it with standard library.
37+
3438
## Usage
3539

3640
Get this package
@@ -43,8 +47,8 @@ go get github.com/akshaybharambe14/go-jsonc
4347

4448
## Example
4549

46-
see [examples](https://github.com/akshaybharambe14/go-jsonc/examples)
50+
see [examples](https://github.com/akshaybharambe14/go-jsonc/tree/master/examples)
4751

4852
## License
4953

50-
`go-jsonc` is available under [MIT License](License.md)
54+
`go-jsonc` is open source and available under [MIT License](License.md)

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# JSONC examples
2+
3+
Examples for go-jsonc

jsonc.go

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package jsonc
33
import (
44
"errors"
55
"io"
6+
"reflect"
7+
"unsafe"
68
)
79

810
type (
@@ -41,42 +43,42 @@ const (
4143
)
4244

4345
var (
44-
ErrUnexpectedEndOfJSON = errors.New("unexpected end of json")
46+
ErrUnexpectedEndOfComment = errors.New("unexpected end of comment")
4547
)
4648

47-
// New a new io.Reader wrapping the provided one.
49+
// NewDecoder returns a new Decoder wrapping the provided io.Reader. The returned decoder implements io.Reader.
4850
func NewDecoder(r io.Reader) *Decoder {
4951
return &Decoder{
5052
c: comment{},
5153
r: r,
5254
}
5355
}
5456

55-
// Read reads from underlying writer and processes the stream to omit comments.
57+
// Read reads from underlying reader and processes the stream to omit comments.
5658
// A single read doesn't guaranttee a valid JSON. Depends on length of passed slice.
5759
//
58-
// Produces ErrUnexpectedEndOfJSON for incomplete comments
60+
// Produces ErrUnexpectedEndOfComment for incomplete comments.
5961
func (d *Decoder) Read(p []byte) (int, error) {
6062

6163
n, err := d.r.Read(p)
6264
if err != nil {
63-
return n, err
65+
return 0, err
6466
}
6567

6668
shortRead := n <= len(p)
67-
n = d.decode(p[:n])
69+
n = decode(p[:n], &d.c)
6870

69-
if shortRead && d.c.state != stopped {
70-
return 0, ErrUnexpectedEndOfJSON
71+
if shortRead && !d.c.complete() {
72+
return 0, ErrUnexpectedEndOfComment
7173
}
7274

7375
return n, nil
7476
}
7577

76-
func (d *Decoder) decode(p []byte) int {
78+
func decode(p []byte, c *comment) int {
7779
i := 0
7880
for _, s := range p {
79-
if d.c.handle(s) {
81+
if c.handle(s) {
8082
p[i] = s
8183
i++
8284
}
@@ -124,17 +126,66 @@ func (c *comment) handle(s byte) bool {
124126
}
125127

126128
if s == newLine && !c.multiLn {
127-
c.state = stopped
129+
c.reset()
128130
}
129131

130132
case canStop:
131133

132134
if s == fwdSlash || s == charN {
133-
c.state = stopped
134-
c.multiLn = false
135+
c.reset()
135136
}
136137

137138
}
138139

139140
return false
140141
}
142+
143+
func (c *comment) reset() {
144+
c.state = stopped
145+
c.multiLn = false
146+
}
147+
148+
func (c *comment) complete() bool {
149+
return c.state == stopped
150+
}
151+
152+
// DecodeBytes decodes passed commented json byte slice to normal json.
153+
// It modifies the passed slice. The passed slice must be refferred till returned count, if there is no error.
154+
//
155+
// The error doesn't include errors related to invalid json. If not nil, it must be ErrUnexpectedEndOfComment.
156+
//
157+
// The returned json must be checked for validity.
158+
func DecodeBytes(p []byte) (int, error) {
159+
c := &comment{}
160+
n := decode(p, c)
161+
162+
if !c.complete() {
163+
return 0, ErrUnexpectedEndOfComment
164+
}
165+
166+
return n, nil
167+
}
168+
169+
// DecodeString decodes passed commented json to normal json.
170+
// It uses "unsafe" way to convert a byte slice to result string. This saves allocations and improves performance is case of large json.
171+
//
172+
// The error doesn't include errors related to invalid json. If not nil, it must be ErrUnexpectedEndOfComment.
173+
//
174+
// The returned json must be checked for validity.
175+
func DecodeString(s string) (string, error) {
176+
p := []byte(s)
177+
178+
n, err := DecodeBytes(p)
179+
if err != nil {
180+
return "", err
181+
}
182+
183+
p = p[:n]
184+
185+
// following operation is safe to do till p is not being changed. This reduces allocations.
186+
sh := *(*reflect.SliceHeader)(unsafe.Pointer(&p))
187+
return *(*string)(unsafe.Pointer(&reflect.StringHeader{
188+
Data: sh.Data,
189+
Len: sh.Len,
190+
})), nil
191+
}

jsonc_test.go

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ package jsonc
22

33
import (
44
"bytes"
5+
"io"
6+
"reflect"
57
"testing"
8+
"testing/iotest"
69
)
710

8-
func ts(b []byte) *Decoder { return &Decoder{r: bytes.NewBuffer(b)} }
11+
func ts(b []byte) *Decoder { return &Decoder{r: bytes.NewBuffer(b)} }
12+
func tsErr(b []byte) *Decoder { return &Decoder{r: iotest.DataErrReader(bytes.NewBuffer(b))} }
913

1014
var (
1115
validSingle = []byte(`{"foo": // this is a single line comment\n"bar foo", "true": false, "number": 42, "object": { "test": "done" }, "array" : [1, 2, 3], "url" : "https://github.com" }`)
1216
invalidSingle = []byte(`{"foo": // this is a single line comment "bar foo", "true": false, "number": 42, "object": { "test": "done" }, "array" : [1, 2, 3], "url" : "https://github.com" }`)
1317

18+
validSingleESC = []byte("{\"foo\": // this is a single line comment\n\"bar foo\", \"true\": false, \"number\": 42, \"object\": { \"test\": \"done\" }, \"array\" : [1, 2, 3], \"url\" : \"https://github.com\" }")
19+
invalidSingleESC = []byte("{\"foo\": // this is a single line comment\"bar foo\", \"true\": false, \"number\": 42, \"object\": { \"test\": \"done\" }, \"array\" : [1, 2, 3], \"url\" : \"https://github.com\" }")
20+
1421
validBlock = []byte(`{"foo": /* this is a block comment */ "bar foo", "true": false, "number": 42, "object": { "test": "done" }, "array" : [1, 2, 3], "url" : "https://github.com" }`)
1522
invalidBlock = []byte(`{"foo": /* this is a block comment "bar foo", "true": false, "number": 42, "object": { "test": "done" }, "array" : [1, 2, 3], "url" : "https://github.com" }`)
1623
)
@@ -42,6 +49,20 @@ func Test_Decoder_Read(t *testing.T) {
4249
want: 0,
4350
wantErr: true,
4451
},
52+
{
53+
name: "Valid single line comment (escaped json)",
54+
d: ts(validSingleESC),
55+
args: args{p: make([]byte, len(validSingleESC))},
56+
want: 110, // (163(total) - 34(comments) - 19(spaces))
57+
wantErr: false,
58+
},
59+
{
60+
name: "Invalid single line comment (escaped json)",
61+
d: ts(invalidSingleESC),
62+
args: args{p: make([]byte, len(invalidSingleESC))},
63+
want: 0,
64+
wantErr: true,
65+
},
4566
{
4667
name: "Valid block comment",
4768
d: ts(validBlock),
@@ -56,6 +77,13 @@ func Test_Decoder_Read(t *testing.T) {
5677
want: 0,
5778
wantErr: true,
5879
},
80+
{
81+
name: "Invalid Read",
82+
d: tsErr(validBlock),
83+
args: args{p: make([]byte, len(validBlock))},
84+
want: 0,
85+
wantErr: true,
86+
},
5987
}
6088

6189
for _, tt := range tests {
@@ -71,3 +99,101 @@ func Test_Decoder_Read(t *testing.T) {
7199
})
72100
}
73101
}
102+
103+
func TestNewDecoder(t *testing.T) {
104+
type args struct {
105+
r io.Reader
106+
}
107+
tests := []struct {
108+
name string
109+
args args
110+
want *Decoder
111+
}{
112+
{
113+
name: "Valid Decoder",
114+
args: args{r: nil},
115+
want: &Decoder{},
116+
},
117+
}
118+
for _, tt := range tests {
119+
t.Run(tt.name, func(t *testing.T) {
120+
if got := NewDecoder(tt.args.r); !reflect.DeepEqual(got, tt.want) {
121+
t.Errorf("NewDecoder() = %v, want %v", got, tt.want)
122+
}
123+
})
124+
}
125+
}
126+
127+
func TestDecodeBytes(t *testing.T) {
128+
type args struct {
129+
p []byte
130+
}
131+
tests := []struct {
132+
name string
133+
args args
134+
want int
135+
wantErr bool
136+
}{
137+
{
138+
name: "Valid input",
139+
args: args{p: []byte(string(validBlock))},
140+
want: 110,
141+
wantErr: false,
142+
},
143+
{
144+
name: "Invalid input",
145+
args: args{p: []byte(string(invalidBlock))},
146+
want: 0,
147+
wantErr: true,
148+
},
149+
}
150+
for _, tt := range tests {
151+
t.Run(tt.name, func(t *testing.T) {
152+
got, err := DecodeBytes(tt.args.p)
153+
if (err != nil) != tt.wantErr {
154+
t.Errorf("DecodeBytes() error = %v, wantErr %v", err, tt.wantErr)
155+
return
156+
}
157+
if got != tt.want {
158+
t.Errorf("DecodeBytes() = %v, want %v", got, tt.want)
159+
}
160+
})
161+
}
162+
}
163+
164+
func TestDecodeString(t *testing.T) {
165+
type args struct {
166+
s string
167+
}
168+
tests := []struct {
169+
name string
170+
args args
171+
want string
172+
wantErr bool
173+
}{
174+
{
175+
name: "Valid input",
176+
args: args{s: string(validBlock)},
177+
want: `{"foo":"bar foo","true":false,"number":42,"object":{"test":"done"},"array":[1,2,3],"url":"https://github.com"}`,
178+
wantErr: false,
179+
},
180+
{
181+
name: "Invalid input",
182+
args: args{s: string(invalidBlock)},
183+
want: "",
184+
wantErr: true,
185+
},
186+
}
187+
for _, tt := range tests {
188+
t.Run(tt.name, func(t *testing.T) {
189+
got, err := DecodeString(tt.args.s)
190+
if (err != nil) != tt.wantErr {
191+
t.Errorf("DecodeString() error = %v, wantErr %v", err, tt.wantErr)
192+
return
193+
}
194+
if got != tt.want {
195+
t.Errorf("DecodeString() = %v, want %v", got, tt.want)
196+
}
197+
})
198+
}
199+
}

0 commit comments

Comments
 (0)