Skip to content

Commit

Permalink
Add Object Path selector (#48)
Browse files Browse the repository at this point in the history
### Search by path

It is possible to search by path to find elements by traversing objects.

For example:

```
	// Find element in path.
	elem, err := i.FindElement("Image/URL", nil)
```

Will locate the field inside a json object with the following structure:

```
{
    "Image": {
        "URL": "value"
    }
}
```
  • Loading branch information
klauspost authored Nov 24, 2021
1 parent d9aecff commit 0c4d67f
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 0 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ For any `Iter` it is possible to marshal the recursive content of the Iter using

Currently, it is not possible to unmarshal into structs.

### Search by path

It is possible to search by path to find elements by traversing objects.

For example:

```
// Find element in path.
elem, err := i.FindElement("Image/URL", nil)
```

Will locate the field inside a json object with the following structure:

```
{
"Image": {
"URL": "value"
}
}
```

The values can be any type. The [Element](https://pkg.go.dev/github.com/minio/simdjson-go#Element)
will contain the element information and an Iter to access the content.

## Parsing Objects

If you are only interested in one key in an object you can use `FindKey` to quickly select it.
Expand Down
38 changes: 38 additions & 0 deletions parsed_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,44 @@ func (i *Iter) Root(dst *Iter) (Type, *Iter, error) {
return dst.AdvanceInto().Type(), dst, nil
}

// FindElement allows searching for fields and objects by path from the iter and forward,
// moving into root and objects, but not arrays.
// Separate each object name by /.
// For example `Image/Url` will search the current root/object for an "Image"
// object and return the value of the "Url" element.
// ErrPathNotFound is returned if any part of the path cannot be found.
// If the tape contains an error it will be returned.
// The iter will *not* be advanced.
func (i *Iter) FindElement(path string, dst *Element) (*Element, error) {
// Local copy.
cp := *i
for {
switch cp.t {
case TagObjectStart:
var o Object
obj, err := cp.Object(&o)
if err != nil {
return dst, err
}
return obj.FindPath(path, dst)
case TagRoot:
_, _, err := cp.Root(&cp)
if err != nil {
return dst, err
}
continue
case TagEnd:
tag := cp.AdvanceInto()
if tag == TagEnd {
return dst, ErrPathNotFound
}
continue
default:
return dst, fmt.Errorf("type %q found before object was found", cp.t)
}
}
}

// Bool returns the bool value.
func (i *Iter) Bool() (bool, error) {
switch i.t {
Expand Down
46 changes: 46 additions & 0 deletions parsed_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"testing"
"time"
Expand Down Expand Up @@ -750,3 +751,48 @@ func TestIter_SetStringBytes(t *testing.T) {
})
}
}

func ExampleIter_FindElement() {
input := `{
"Image":
{
"Animated": false,
"Height": 600,
"IDs":
[
116,
943,
234,
38793
],
"Thumbnail":
{
"Height": 125,
"Url": "http://www.example.com/image/481989943",
"Width": 100
},
"Title": "View from 15th Floor",
"Width": 800
},
"Alt": "Image of city"
}`
pj, err := Parse([]byte(input), nil)
if err != nil {
log.Fatal(err)
}
i := pj.Iter()

// Find element in path.
elem, err := i.FindElement("Image/Thumbnail/Width", nil)
if err != nil {
log.Fatal(err)
}

// Print result:
fmt.Println(elem.Type)
fmt.Println(elem.Iter.StringCvt())

// Output:
// int
// 100 <nil>
}
71 changes: 71 additions & 0 deletions parsed_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package simdjson
import (
"errors"
"fmt"
"strings"
)

// Object represents a JSON object.
Expand Down Expand Up @@ -137,6 +138,76 @@ func (o *Object) FindKey(key string, dst *Element) *Element {
}
}

// ErrPathNotFound is returned
var ErrPathNotFound = errors.New("path not found")

// FindPath allows searching for fields and objects by path.
// Separate each object name by /.
// For example `Image/Url` will search the current object for an "Image"
// object and return the value of the "Url" element.
// ErrPathNotFound is returned if any part of the path cannot be found.
// If the tape contains an error it will be returned.
// The object will not be advanced.
func (o *Object) FindPath(path string, dst *Element) (*Element, error) {
tmp := o.tape.Iter()
tmp.off = o.off
p := strings.Split(path, "/")
key := p[0]
p = p[1:]
for {
typ := tmp.Advance()
// We want name and at least one value.
if typ != TypeString || tmp.off+1 >= len(tmp.tape.Tape) {
return dst, ErrPathNotFound
}
// Advance must be string or end of object
offset := tmp.cur
length := tmp.tape.Tape[tmp.off]
if int(length) != len(key) {
// Skip the value.
t := tmp.Advance()
if t == TypeNone {
// Not found...
return dst, ErrPathNotFound
}
continue
}
// Read name
name, err := tmp.tape.stringByteAt(offset, length)
if err != nil {
return dst, err
}

if string(name) != key {
// Skip the value
tmp.Advance()
continue
}
// Done...
if len(p) == 0 {
if dst == nil {
dst = &Element{}
}
dst.Name = key
dst.Type, err = tmp.AdvanceIter(&dst.Iter)
if err != nil {
return dst, err
}
return dst, nil
}

t, err := tmp.AdvanceIter(&tmp)
if err != nil {
return dst, err
}
if t != TypeObject {
return dst, fmt.Errorf("value of key %v is not an object", key)
}
key = p[0]
p = p[1:]
}
}

// NextElement sets dst to the next element and returns the name.
// TypeNone with nil error will be returned if there are no more elements.
func (o *Object) NextElement(dst *Iter) (name string, t Type, err error) {
Expand Down
Loading

0 comments on commit 0c4d67f

Please sign in to comment.