Skip to content

Commit

Permalink
feat(p/json): remove unnecessary code and optimize (#2939)
Browse files Browse the repository at this point in the history
# Description

Optimized the JSON package and simplified JSON node creation using the
builder pattern.

- in `buffer.gno` and `escape.gno` files are modified the use of map for
lookup tables to use slice array instead.
- refactor the `Unquote` function in `escape.gno` file
- modified the existing functions that parsed numbers to use `strconv`
package, and deleted related files and functions
- especially, the `eisel_lemire` and `ryu` packages were deleted since
they were files that had been added to handle `ParseUint` and
`ParseFloat` in `strconv` package.

## JSON Generate Example

**Plain JSON**

```go
node := Builder().
	WithString("name", "Alice").
	WithNumber("age", 30).
	WithBool("is_student", false).
	Node()

value, err := Marshal(node)
if err != nil {
	t.Errorf("unexpected error: %s", err)
}

Output:
{"name":"Alice","age":30,"is_student":false}
```

**Nested Structure**

```go
node := Builder().
	WriteString("name", "Alice").
	WriteObject("address", func(b *NodeBuilder) {
		b.WriteString("city", "New York").
		WriteNumber("zipcode", 10001)
	}).
	Node()
// ...

Output:
{"name":"Alice","address":{"city":"New York","zipcode":10001}}
```

## Benchmark Result for Unquote

**Before**

```plain
BenchmarkUnquote-8              	12433488	        98.06 ns/op	     144 B/op	       2 allocs/op
BenchmarkUnquoteWorstCase-8     	24727736	        50.46 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnquoteBestCase-8      	22542354	        52.69 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnquoteEmptyString-8   	394868628	         3.067 ns/op	       0 B/op	       0 allocs/op
```

**After**

```plain
BenchmarkUnquote-8              	12464704	        96.61 ns/op	     144 B/op	       2 allocs/op
BenchmarkUnquoteWorstCase-8     	25084070	        48.02 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnquoteBestCase-8      	23383227	        52.66 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnquoteEmptyString-8   	400496838	         2.968 ns/op	       0 B/op	       0 allocs/op
```
  • Loading branch information
notJoon authored Oct 30, 2024
1 parent 850182c commit 494976d
Show file tree
Hide file tree
Showing 23 changed files with 383 additions and 2,561 deletions.
65 changes: 21 additions & 44 deletions examples/gno.land/p/demo/json/buffer.gno
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package json
import (
"errors"
"io"
"strings"

"gno.land/p/demo/ufmt"
)
Expand Down Expand Up @@ -112,28 +111,6 @@ func (b *buffer) skip(bs byte) error {
return io.EOF
}

// skipAny moves the index until it encounters one of the given set of bytes.
func (b *buffer) skipAny(endTokens map[byte]bool) error {
for b.index < b.length {
if _, exists := endTokens[b.data[b.index]]; exists {
return nil
}

b.index++
}

// build error message
var tokens []string
for token := range endTokens {
tokens = append(tokens, string(token))
}

return ufmt.Errorf(
"EOF reached before encountering one of the expected tokens: %s",
strings.Join(tokens, ", "),
)
}

// skipAndReturnIndex moves the buffer index forward by one and returns the new index.
func (b *buffer) skipAndReturnIndex() (int, error) {
err := b.step()
Expand Down Expand Up @@ -165,7 +142,7 @@ func (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) {

// significantTokens is a map where the keys are the significant characters in a JSON path.
// The values in the map are all true, which allows us to use the map as a set for quick lookups.
var significantTokens = map[byte]bool{
var significantTokens = [256]bool{
dot: true, // access properties of an object
dollarSign: true, // root object
atSign: true, // current object
Expand All @@ -174,7 +151,7 @@ var significantTokens = map[byte]bool{
}

// filterTokens stores the filter expression tokens.
var filterTokens = map[byte]bool{
var filterTokens = [256]bool{
aesterisk: true, // wildcard
andSign: true,
orSign: true,
Expand All @@ -186,7 +163,7 @@ func (b *buffer) skipToNextSignificantToken() {
for b.index < b.length {
current := b.data[b.index]

if _, ok := significantTokens[current]; ok {
if significantTokens[current] {
break
}

Expand All @@ -205,7 +182,7 @@ func (b *buffer) backslash() bool {

count := 0
for i := b.index - 1; ; i-- {
if i >= b.length || b.data[i] != backSlash {
if b.data[i] != backSlash {
break
}

Expand All @@ -220,7 +197,7 @@ func (b *buffer) backslash() bool {
}

// numIndex holds a map of valid numeric characters
var numIndex = map[byte]bool{
var numIndex = [256]bool{
'0': true,
'1': true,
'2': true,
Expand Down Expand Up @@ -255,11 +232,11 @@ func (b *buffer) pathToken() error {
}

if err := b.skip(c); err != nil {
return errors.New("unmatched quote in path")
return errUnmatchedQuotePath
}

if b.index >= b.length {
return errors.New("unmatched quote in path")
return errUnmatchedQuotePath
}

case c == bracketOpen || c == parenOpen:
Expand All @@ -269,7 +246,7 @@ func (b *buffer) pathToken() error {
case c == bracketClose || c == parenClose:
inToken = true
if len(stack) == 0 || (c == bracketClose && stack[len(stack)-1] != bracketOpen) || (c == parenClose && stack[len(stack)-1] != parenOpen) {
return errors.New("mismatched bracket or parenthesis")
return errUnmatchedParenthesis
}

stack = stack[:len(stack)-1]
Expand All @@ -284,7 +261,7 @@ func (b *buffer) pathToken() error {
inToken = true
inNumber = true
} else if !inToken {
return errors.New("unexpected operator at start of token")
return errInvalidToken
}

default:
Expand All @@ -300,7 +277,7 @@ func (b *buffer) pathToken() error {

end:
if len(stack) != 0 {
return errors.New("unclosed bracket or parenthesis at end of path")
return errUnmatchedParenthesis
}

if first == b.index {
Expand All @@ -315,15 +292,15 @@ end:
}

func pathStateContainsValidPathToken(c byte) bool {
if _, ok := significantTokens[c]; ok {
if significantTokens[c] {
return true
}

if _, ok := filterTokens[c]; ok {
if filterTokens[c] {
return true
}

if _, ok := numIndex[c]; ok {
if numIndex[c] {
return true
}

Expand All @@ -342,7 +319,7 @@ func (b *buffer) numeric(token bool) error {
for ; b.index < b.length; b.index++ {
b.class = b.getClasses(doubleQuote)
if b.class == __ {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

b.state = StateTransitionTable[b.last][b.class]
Expand All @@ -351,7 +328,7 @@ func (b *buffer) numeric(token bool) error {
break
}

return errors.New("invalid token found while parsing path")
return errInvalidToken
}

if b.state < __ {
Expand All @@ -366,7 +343,7 @@ func (b *buffer) numeric(token bool) error {
}

if b.last != ZE && b.last != IN && b.last != FR && b.last != E3 {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

return nil
Expand Down Expand Up @@ -407,12 +384,12 @@ func (b *buffer) string(search byte, token bool) error {
b.class = b.getClasses(search)

if b.class == __ {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

b.state = StateTransitionTable[b.last][b.class]
if b.state == __ {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

if b.state < __ {
Expand All @@ -431,11 +408,11 @@ func (b *buffer) word(bs []byte) error {
max := len(bs)
index := 0

for ; b.index < b.length; b.index++ {
for ; b.index < b.length && index < max; b.index++ {
c = b.data[b.index]

if c != bs[index] {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

index++
Expand All @@ -445,7 +422,7 @@ func (b *buffer) word(bs []byte) error {
}

if index != max {
return errors.New("invalid token found while parsing path")
return errInvalidToken
}

return nil
Expand Down
35 changes: 3 additions & 32 deletions examples/gno.land/p/demo/json/buffer_test.gno
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package json

import "testing"
import (
"testing"
)

func TestBufferCurrent(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -242,37 +244,6 @@ func TestBufferSkip(t *testing.T) {
}
}

func TestBufferSkipAny(t *testing.T) {
tests := []struct {
name string
buffer *buffer
s map[byte]bool
wantErr bool
}{
{
name: "Skip any valid byte",
buffer: &buffer{data: []byte("test"), length: 4, index: 0},
s: map[byte]bool{'e': true, 'o': true},
wantErr: false,
},
{
name: "Skip any to EOF",
buffer: &buffer{data: []byte("test"), length: 4, index: 0},
s: map[byte]bool{'x': true, 'y': true},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.buffer.skipAny(tt.s)
if (err != nil) != tt.wantErr {
t.Errorf("buffer.skipAny() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestSkipToNextSignificantToken(t *testing.T) {
tests := []struct {
name string
Expand Down
89 changes: 89 additions & 0 deletions examples/gno.land/p/demo/json/builder.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package json

type NodeBuilder struct {
node *Node
}

func Builder() *NodeBuilder {
return &NodeBuilder{node: ObjectNode("", nil)}
}

func (b *NodeBuilder) WriteString(key, value string) *NodeBuilder {
b.node.AppendObject(key, StringNode("", value))
return b
}

func (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder {
b.node.AppendObject(key, NumberNode("", value))
return b
}

func (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder {
b.node.AppendObject(key, BoolNode("", value))
return b
}

func (b *NodeBuilder) WriteNull(key string) *NodeBuilder {
b.node.AppendObject(key, NullNode(""))
return b
}

func (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder {
nestedBuilder := &NodeBuilder{node: ObjectNode("", nil)}
fn(nestedBuilder)
b.node.AppendObject(key, nestedBuilder.node)
return b
}

func (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder {
arrayBuilder := &ArrayBuilder{nodes: []*Node{}}
fn(arrayBuilder)
b.node.AppendObject(key, ArrayNode("", arrayBuilder.nodes))
return b
}

func (b *NodeBuilder) Node() *Node {
return b.node
}

type ArrayBuilder struct {
nodes []*Node
}

func (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder {
ab.nodes = append(ab.nodes, StringNode("", value))
return ab
}

func (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder {
ab.nodes = append(ab.nodes, NumberNode("", value))
return ab
}

func (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder {
return ab.WriteNumber(float64(value))
}

func (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder {
ab.nodes = append(ab.nodes, BoolNode("", value))
return ab
}

func (ab *ArrayBuilder) WriteNull() *ArrayBuilder {
ab.nodes = append(ab.nodes, NullNode(""))
return ab
}

func (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder {
nestedBuilder := &NodeBuilder{node: ObjectNode("", nil)}
fn(nestedBuilder)
ab.nodes = append(ab.nodes, nestedBuilder.node)
return ab
}

func (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder {
nestedArrayBuilder := &ArrayBuilder{nodes: []*Node{}}
fn(nestedArrayBuilder)
ab.nodes = append(ab.nodes, ArrayNode("", nestedArrayBuilder.nodes))
return ab
}
Loading

0 comments on commit 494976d

Please sign in to comment.