Skip to content

Add Object Path selector #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -747,6 +747,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