Skip to content

Commit d643f30

Browse files
committed
✨ jsonz: add Builder to reconstruct json
1 parent 1cfc87a commit d643f30

File tree

8 files changed

+255
-113
lines changed

8 files changed

+255
-113
lines changed

jsonz/parser.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,25 @@ func Parse(in []byte) iter.Seq2[Item, error] {
4343

4444
value:
4545
switch {
46-
case tok.typ == TokenArrayStart:
46+
case tok.typ == TokenArrayOpen:
4747
if !yieldValue(last.Key) {
4848
return
4949
}
5050
push()
5151
advance()
52-
if tok.typ == TokenArrayEnd {
52+
if tok.typ == TokenArrayClose {
5353
goto close
5454
} else {
5555
goto value
5656
}
5757

58-
case tok.typ == TokenObjectStart:
58+
case tok.typ == TokenObjectOpen:
5959
if !yieldValue(last.Key) {
6060
return
6161
}
6262
push()
6363
advance()
64-
if tok.typ == TokenObjectEnd {
64+
if tok.typ == TokenObjectClose {
6565
goto close
6666
} else {
6767
goto key_value
@@ -102,8 +102,8 @@ func Parse(in []byte) iter.Seq2[Item, error] {
102102

103103
close:
104104
switch {
105-
case tok.typ == TokenArrayEnd:
106-
if last.Token.typ != TokenArrayStart {
105+
case tok.typ == TokenArrayClose:
106+
if last.Token.typ != TokenArrayOpen {
107107
panicf("parser: unexpected array end")
108108
}
109109
pop()
@@ -117,8 +117,8 @@ func Parse(in []byte) iter.Seq2[Item, error] {
117117
goto end
118118
}
119119

120-
case tok.typ == TokenObjectEnd:
121-
if last.Token.typ != TokenObjectStart {
120+
case tok.typ == TokenObjectClose:
121+
if last.Token.typ != TokenObjectOpen {
122122
panicf("parser: unexpected object end")
123123
}
124124
pop()
@@ -137,9 +137,9 @@ func Parse(in []byte) iter.Seq2[Item, error] {
137137
last.Key = RawToken{}
138138
advance()
139139
switch {
140-
case last.Token.typ == TokenArrayStart:
140+
case last.Token.typ == TokenArrayOpen:
141141
goto value
142-
case last.Token.typ == TokenObjectStart:
142+
case last.Token.typ == TokenObjectOpen:
143143
goto key_value
144144
default:
145145
panicf("parser: unexpected comma")

jsonz/reconstruct.go

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ package jsonz
22

33
import (
44
"bytes"
5-
6-
"ezpkg.io/bytez"
5+
"fmt"
76
)
87

98
// Reconstruct is an example of how to reconstruct a JSON from Parse().
@@ -31,7 +30,7 @@ func Reconstruct(in []byte) ([]byte, error) {
3130

3231
// Reformat is an example of how to reconstruct a JSON from Parse(), and format with indentation.
3332
func Reformat(in []byte, prefix, indent string) ([]byte, error) {
34-
b := bytez.Buffer{}
33+
b := bytes.Buffer{}
3534
b.Grow(len(in))
3635

3736
var lastToken TokenType
@@ -43,28 +42,140 @@ func Reformat(in []byte, prefix, indent string) ([]byte, error) {
4342
b.WriteByte(',')
4443
}
4544
if lastToken != 0 {
46-
b.Println(prefix)
45+
b.WriteByte('\n')
4746
}
47+
b.WriteString(prefix)
4848
for range item.Level {
4949
b.WriteString(indent)
5050
}
5151
if item.Key.IsValue() {
5252
b.Write(item.Key.Raw())
53-
b.WriteByte(':')
53+
b.WriteString(": ")
5454
}
5555
b.Write(item.Token.Raw())
5656
lastToken = item.Token.Type()
5757
}
5858
return b.Bytes(), nil
5959
}
6060

61+
type Builder struct {
62+
bytes.Buffer
63+
indent string
64+
prefix string
65+
66+
lastTok TokenType
67+
level int
68+
stack []TokenType // array or object
69+
err error
70+
}
71+
72+
// NewBuilder creates a new Builder. It's optional to set the prefix and indent. A zero Builder is valid.
73+
func NewBuilder(prefix, indent string) *Builder {
74+
return &Builder{
75+
indent: indent,
76+
prefix: prefix,
77+
}
78+
}
79+
80+
// Bytes returns the bytes of the builder with an error if any.
81+
func (b *Builder) Bytes() ([]byte, error) {
82+
return b.Buffer.Bytes(), b.err
83+
}
84+
85+
func (b *Builder) AddRaw(key, token RawToken) {
86+
switch {
87+
case token.IsOpen():
88+
if ShouldAddComma(b.lastTok, token.Type()) {
89+
b.WriteByte(',')
90+
}
91+
b.writeIndent()
92+
b.writeKey(key)
93+
b.WriteByte(byte(token.Type()))
94+
b.lastTok = token.Type()
95+
b.stack = append(b.stack, token.Type())
96+
b.level++
97+
98+
case token.IsClose():
99+
if key.Type() != 0 {
100+
b.addErrorf("unexpected key(%s) before close token(%s)", key, token.Type())
101+
return
102+
}
103+
if b.level <= 0 {
104+
b.addErrorf("unexpected close token(%s)", token.Type())
105+
return
106+
}
107+
b.level--
108+
b.stack = b.stack[:len(b.stack)-1]
109+
b.writeIndent()
110+
b.WriteByte(byte(token.Type()))
111+
b.lastTok = token.Type()
112+
113+
case token.IsValue():
114+
if ShouldAddComma(b.lastTok, token.Type()) {
115+
b.WriteByte(',')
116+
}
117+
b.writeIndent()
118+
b.writeKey(key)
119+
b.Write(token.Raw())
120+
b.lastTok = token.Type()
121+
}
122+
}
123+
124+
func (b *Builder) writeKey(key RawToken) {
125+
if b.level <= 0 {
126+
if key.Type() != 0 {
127+
b.addErrorf("unexpected key(%s) at root", key)
128+
}
129+
return
130+
}
131+
top := b.stack[len(b.stack)-1]
132+
switch top {
133+
case TokenArrayOpen:
134+
if key.Type() != 0 {
135+
b.addErrorf("unexpected key(%s) in array", key)
136+
return
137+
}
138+
case TokenObjectOpen:
139+
if key.Type() == 0 {
140+
b.addErrorf("missing key in object")
141+
}
142+
b.Write(key.Raw())
143+
b.WriteByte(':')
144+
if b.indent != "" {
145+
b.WriteByte(' ')
146+
}
147+
default:
148+
panic("unexpected stack")
149+
}
150+
}
151+
152+
func (b *Builder) writeIndent() {
153+
if b.prefix == "" && b.indent == "" {
154+
return
155+
}
156+
if b.lastTok != 0 {
157+
b.WriteByte('\n')
158+
}
159+
b.WriteString(b.prefix)
160+
for range b.level {
161+
b.WriteString(b.indent)
162+
}
163+
}
164+
165+
func (b *Builder) addErrorf(msg string, args ...any) {
166+
if b.err == nil {
167+
b.err = fmt.Errorf(msg, args...)
168+
}
169+
}
170+
171+
// ShouldAddComma returns true if a comma should be added before the next token.
61172
func ShouldAddComma(lastToken, nextToken TokenType) bool {
62173
switch lastToken {
63-
case 0, TokenArrayStart, TokenObjectStart:
174+
case 0, TokenArrayOpen, TokenObjectOpen:
64175
return false
65176
default:
66177
switch nextToken {
67-
case TokenArrayEnd, TokenObjectEnd:
178+
case TokenArrayClose, TokenObjectClose:
68179
return false
69180
default:
70181
return true

jsonz/reconstruct_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func TestReconstruct(t *testing.T) {
1515
Convey("Reconstruct", t, func() {
16-
Convey("no ident", func() {
16+
Convey("no indent", func() {
1717
tcase := jtest.GetTestcase("pass01.json")
1818
out, err := jsonz.Reconstruct(tcase.Data)
1919
Ω(err).ToNot(HaveOccurred())
@@ -24,9 +24,40 @@ func TestReconstruct(t *testing.T) {
2424
expect := reformatWithStdjson(tcase.Data)
2525
ΩxNoDiffByLine(actual, expect)
2626
})
27-
Convey("with ident", func() {
27+
Convey("with indent", func() {
2828
tcase := jtest.GetTestcase("pass01.json")
29-
out, err := jsonz.Reformat(tcase.Data, "", "\t")
29+
out, err := jsonz.Reformat(tcase.Data, "→ ", "\t")
30+
Ω(err).ToNot(HaveOccurred())
31+
32+
fmt.Printf("\n--- reformat ---\n%s\n---\n", out)
33+
ΩxNoDiffByLine(string(out), tcase.ExpectFormat)
34+
})
35+
})
36+
Convey("Builder", t, func() {
37+
Convey("no indent", func() {
38+
tcase := jtest.GetTestcase("pass01.json")
39+
b := jsonz.NewBuilder("", "")
40+
for item, err := range jsonz.Parse(tcase.Data) {
41+
Ω(err).ToNot(HaveOccurred())
42+
b.AddRaw(item.Key, item.Token)
43+
}
44+
out, err := b.Bytes()
45+
Ω(err).ToNot(HaveOccurred())
46+
47+
fmt.Printf("\n--- reconstruct ---\n%s\n---\n", out)
48+
49+
actual := reformatWithStdjson(out)
50+
expect := reformatWithStdjson(tcase.Data)
51+
ΩxNoDiffByLine(actual, expect)
52+
})
53+
Convey("with indent", func() {
54+
tcase := jtest.GetTestcase("pass01.json")
55+
b := jsonz.NewBuilder("→ ", "\t")
56+
for item, err := range jsonz.Parse(tcase.Data) {
57+
Ω(err).ToNot(HaveOccurred())
58+
b.AddRaw(item.Key, item.Token)
59+
}
60+
out, err := b.Bytes()
3061
Ω(err).ToNot(HaveOccurred())
3162

3263
fmt.Printf("\n--- reformat ---\n%s\n---\n", out)

jsonz/token.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ const (
1616
TokenFalse TokenType = 'f'
1717
TokenNumber TokenType = '0'
1818
TokenString TokenType = '"'
19-
TokenObjectStart TokenType = '{'
20-
TokenObjectEnd TokenType = '}'
21-
TokenArrayStart TokenType = '['
22-
TokenArrayEnd TokenType = ']'
19+
TokenObjectOpen TokenType = '{'
20+
TokenObjectClose TokenType = '}'
21+
TokenArrayOpen TokenType = '['
22+
TokenArrayClose TokenType = ']'
2323
TokenComma TokenType = ','
2424
TokenColon TokenType = ':'
2525
)
@@ -92,12 +92,12 @@ func (r RawToken) IsValue() bool {
9292

9393
// IsOpen returns true if the token is an open token '[' or '{'.
9494
func (r RawToken) IsOpen() bool {
95-
return r.typ == TokenArrayStart || r.typ == TokenObjectStart
95+
return r.typ == TokenArrayOpen || r.typ == TokenObjectOpen
9696
}
9797

9898
// IsClose returns true if the token is a close token ']' or '}'.
9999
func (r RawToken) IsClose() bool {
100-
return r.typ == TokenArrayEnd || r.typ == TokenObjectEnd
100+
return r.typ == TokenArrayClose || r.typ == TokenObjectClose
101101
}
102102

103103
// GetNumber returns the number value of the token.
@@ -308,7 +308,7 @@ func (r RawToken) GetValue() (any, error) {
308308
return r.GetNumber()
309309
case TokenString:
310310
return r.GetString()
311-
case TokenObjectStart, TokenObjectEnd, TokenArrayStart, TokenArrayEnd, TokenComma, TokenColon:
311+
case TokenObjectOpen, TokenObjectClose, TokenArrayOpen, TokenArrayClose, TokenComma, TokenColon:
312312
return r.typ, nil
313313
}
314314
return nil, fmt.Errorf("invalid token type: %v", r.typ)

jsonz/types.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,20 @@ func (p RawPath) Format(f fmt.State, c rune) {
146146

147147
// IsArray returns true if the path item is inside an array.
148148
func (p PathItem) IsArray() bool {
149-
return p.Token.typ == TokenArrayStart
149+
return p.Token.typ == TokenArrayOpen
150150
}
151151

152152
// IsObject returns true if the path item is inside an object.
153153
func (p PathItem) IsObject() bool {
154-
return p.Token.typ == TokenObjectStart
154+
return p.Token.typ == TokenObjectOpen
155155
}
156156

157157
// Value returns the value of the path item. If the item is inside an array, it returns the index. If the item is inside an object, it returns the key.
158158
func (p PathItem) Value() any {
159159
switch p.Token.typ {
160-
case TokenArrayStart:
160+
case TokenArrayOpen:
161161
return p.Index
162-
case TokenObjectStart:
162+
case TokenObjectOpen:
163163
v, _ := p.Key.GetString()
164164
return v
165165
default:

0 commit comments

Comments
 (0)