Skip to content

Commit 1e9be66

Browse files
Add: jsonc decoder
1. Add: Decode commented json. 2. Add: Implement io.Reader.
1 parent dd9adcb commit 1e9be66

File tree

5 files changed

+320
-1
lines changed

5 files changed

+320
-1
lines changed

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,50 @@
1-
go-jsonc
1+
# JSON with comments for GO
2+
3+
- Decodes a "commented json" to "json". Provided, yhe input must be a valid jsonc document.
4+
- Supports io.Reader
5+
6+
Inspired by [muhammadmuzzammil1998](https://github.com/muhammadmuzzammil1998/jsonc)
7+
8+
```jsonc
9+
{
10+
/*
11+
some block comment
12+
*/
13+
"string": "foo", // a string
14+
"bool": false, // a boolean
15+
"number": 42, // a number
16+
// "object":{
17+
// "key":"val"
18+
// },
19+
"array": [
20+
// example of an array
21+
1,
22+
2,
23+
3
24+
]
25+
}
26+
```
27+
28+
Gets converted to (spaces omitted)
29+
30+
```json
31+
{ "string": "foo", "bool": false, "number": 42, "array": [1, 2, 3] }
32+
```
33+
34+
## Usage
35+
36+
Get this package
37+
38+
```sh
39+
40+
go get github.com/akshaybharambe14/go-jsonc
41+
42+
```
43+
44+
## Example
45+
46+
see [examples](https://github.com/akshaybharambe14/go-jsonc/examples)
47+
48+
## License
49+
50+
`go-jsonc` is available under [MIT License](License.md)

examples/main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
9+
"github.com/akshaybharambe14/go-jsonc"
10+
)
11+
12+
func main() {
13+
// create new decoder
14+
d := jsonc.NewDecoder(bytes.NewBuffer([]byte(`{
15+
/*
16+
some block comment
17+
*/
18+
"string": "foo", // a string
19+
"bool": true, // a boolean
20+
"number": 42, // a number
21+
// "object":{
22+
// "key":"val"
23+
// },
24+
"array": [ // example of an array
25+
1,
26+
2,
27+
3
28+
]
29+
}`)))
30+
31+
// read json
32+
res, err := ioutil.ReadAll(d)
33+
if err != nil {
34+
fmt.Println("error decoding commented json: ", err)
35+
return
36+
}
37+
38+
// Unmarshal to a struct
39+
40+
type Test struct {
41+
Str string `json:"string"`
42+
Bln bool `json:"bool"`
43+
Num int `json:"number"`
44+
Arr []int `json:"array"`
45+
}
46+
47+
t := Test{}
48+
if err = json.Unmarshal(res, &t); err != nil {
49+
fmt.Println("error while json Unmarshal: ", err)
50+
return
51+
}
52+
53+
fmt.Printf("%+v\n", t)
54+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/akshaybharambe14/go-jsonc
2+
3+
go 1.13

jsonc.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package jsonc
2+
3+
import (
4+
"errors"
5+
"io"
6+
)
7+
8+
type (
9+
state int
10+
11+
comment struct {
12+
state state
13+
multiLn bool
14+
isJSON bool
15+
}
16+
17+
// Decoder implements io.Reader. It wraps provided source.
18+
Decoder struct {
19+
r io.Reader
20+
c comment
21+
}
22+
)
23+
24+
const (
25+
// byte representations of string literals
26+
tab = 9 // ( )
27+
newLine = 10 // (\n)
28+
space = 32 // ( )
29+
quote = 34 // ("")
30+
star = 42 // (*)
31+
fwdSlash = 47 // (/)
32+
bkdSlash = 92 // (\)
33+
charN = 110 // (n)
34+
)
35+
36+
const (
37+
stopped state = iota // 0, default state
38+
canStart // 1
39+
started // 2
40+
canStop // 3
41+
)
42+
43+
var (
44+
ErrUnexpectedEndOfJSON = errors.New("unexpected end of json")
45+
)
46+
47+
// New a new io.Reader wrapping the provided one.
48+
func NewDecoder(r io.Reader) *Decoder {
49+
return &Decoder{
50+
c: comment{},
51+
r: r,
52+
}
53+
}
54+
55+
// Read reads from underlying writer and processes the stream to omit comments.
56+
// A single read doesn't guaranttee a valid JSON. Depends on length of passed slice.
57+
//
58+
// Produces ErrUnexpectedEndOfJSON for incomplete comments
59+
func (d *Decoder) Read(p []byte) (int, error) {
60+
61+
n, err := d.r.Read(p)
62+
if err != nil {
63+
return n, err
64+
}
65+
66+
shortRead := n <= len(p)
67+
n = d.decode(p[:n])
68+
69+
if shortRead && d.c.state != stopped {
70+
return 0, ErrUnexpectedEndOfJSON
71+
}
72+
73+
return n, nil
74+
}
75+
76+
func (d *Decoder) decode(p []byte) int {
77+
i := 0
78+
for _, s := range p {
79+
if d.c.handle(s) {
80+
p[i] = s
81+
i++
82+
}
83+
}
84+
85+
return i
86+
}
87+
88+
// handle the current byte, if returned true, add the byte to result.
89+
func (c *comment) handle(s byte) bool {
90+
switch c.state {
91+
92+
case stopped:
93+
if s == quote { // all characters between "" are valid, can be added to result
94+
c.isJSON = !c.isJSON
95+
}
96+
97+
if c.isJSON {
98+
return true
99+
}
100+
101+
if s == space || s == tab || s == newLine {
102+
return false
103+
}
104+
105+
if s == fwdSlash {
106+
c.state = canStart
107+
return false
108+
}
109+
110+
return true
111+
112+
case canStart:
113+
114+
if s == star || s == fwdSlash {
115+
c.state = started
116+
}
117+
118+
c.multiLn = (s == star)
119+
120+
case started:
121+
122+
if s == star || s == bkdSlash {
123+
c.state = canStop
124+
}
125+
126+
if s == newLine && !c.multiLn {
127+
c.state = stopped
128+
}
129+
130+
case canStop:
131+
132+
if s == fwdSlash || s == charN {
133+
c.state = stopped
134+
c.multiLn = false
135+
}
136+
137+
}
138+
139+
return false
140+
}

jsonc_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package jsonc
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func ts(b []byte) *Decoder { return &Decoder{r: bytes.NewBuffer(b)} }
9+
10+
var (
11+
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" }`)
12+
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" }`)
13+
14+
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" }`)
15+
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" }`)
16+
)
17+
18+
func Test_Decoder_Read(t *testing.T) {
19+
20+
type args struct {
21+
p []byte
22+
}
23+
24+
tests := []struct {
25+
name string
26+
d *Decoder
27+
args args
28+
want int
29+
wantErr bool
30+
}{
31+
{
32+
name: "Valid single line comment",
33+
d: ts(validSingle),
34+
args: args{p: make([]byte, len(validSingle))},
35+
want: 110, // (163(total) - 34(comments) - 19(spaces))
36+
wantErr: false,
37+
},
38+
{
39+
name: "Invalid single line comment",
40+
d: ts(invalidSingle),
41+
args: args{p: make([]byte, len(invalidSingle))},
42+
want: 0,
43+
wantErr: true,
44+
},
45+
{
46+
name: "Valid block comment",
47+
d: ts(validBlock),
48+
args: args{p: make([]byte, len(validBlock))},
49+
want: 110, // (159(total) - 29(comments) - 20(spaces))
50+
wantErr: false,
51+
},
52+
{
53+
name: "Invalid block comment",
54+
d: ts(invalidBlock),
55+
args: args{p: make([]byte, len(invalidBlock))},
56+
want: 0,
57+
wantErr: true,
58+
},
59+
}
60+
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
got, err := tt.d.Read(tt.args.p)
64+
if (err != nil) != tt.wantErr {
65+
t.Errorf("Decoder.Read() error = %v, wantErr %v", err, tt.wantErr)
66+
return
67+
}
68+
if got != tt.want {
69+
t.Errorf("Decoder.Read() = %v, want %v", got, tt.want)
70+
}
71+
})
72+
}
73+
}

0 commit comments

Comments
 (0)