Skip to content

Commit 0c4d67f

Browse files
authored
Add Object Path selector (#48)
### 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" } } ```
1 parent d9aecff commit 0c4d67f

5 files changed

Lines changed: 364 additions & 0 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ For any `Iter` it is possible to marshal the recursive content of the Iter using
108108

109109
Currently, it is not possible to unmarshal into structs.
110110

111+
### Search by path
112+
113+
It is possible to search by path to find elements by traversing objects.
114+
115+
For example:
116+
117+
```
118+
// Find element in path.
119+
elem, err := i.FindElement("Image/URL", nil)
120+
```
121+
122+
Will locate the field inside a json object with the following structure:
123+
124+
```
125+
{
126+
"Image": {
127+
"URL": "value"
128+
}
129+
}
130+
```
131+
132+
The values can be any type. The [Element](https://pkg.go.dev/github.com/minio/simdjson-go#Element)
133+
will contain the element information and an Iter to access the content.
134+
111135
## Parsing Objects
112136

113137
If you are only interested in one key in an object you can use `FindKey` to quickly select it.

parsed_json.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,44 @@ func (i *Iter) Root(dst *Iter) (Type, *Iter, error) {
749749
return dst.AdvanceInto().Type(), dst, nil
750750
}
751751

752+
// FindElement allows searching for fields and objects by path from the iter and forward,
753+
// moving into root and objects, but not arrays.
754+
// Separate each object name by /.
755+
// For example `Image/Url` will search the current root/object for an "Image"
756+
// object and return the value of the "Url" element.
757+
// ErrPathNotFound is returned if any part of the path cannot be found.
758+
// If the tape contains an error it will be returned.
759+
// The iter will *not* be advanced.
760+
func (i *Iter) FindElement(path string, dst *Element) (*Element, error) {
761+
// Local copy.
762+
cp := *i
763+
for {
764+
switch cp.t {
765+
case TagObjectStart:
766+
var o Object
767+
obj, err := cp.Object(&o)
768+
if err != nil {
769+
return dst, err
770+
}
771+
return obj.FindPath(path, dst)
772+
case TagRoot:
773+
_, _, err := cp.Root(&cp)
774+
if err != nil {
775+
return dst, err
776+
}
777+
continue
778+
case TagEnd:
779+
tag := cp.AdvanceInto()
780+
if tag == TagEnd {
781+
return dst, ErrPathNotFound
782+
}
783+
continue
784+
default:
785+
return dst, fmt.Errorf("type %q found before object was found", cp.t)
786+
}
787+
}
788+
}
789+
752790
// Bool returns the bool value.
753791
func (i *Iter) Bool() (bool, error) {
754792
switch i.t {

parsed_json_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"fmt"
2323
"io/ioutil"
24+
"log"
2425
"path/filepath"
2526
"testing"
2627
"time"
@@ -750,3 +751,48 @@ func TestIter_SetStringBytes(t *testing.T) {
750751
})
751752
}
752753
}
754+
755+
func ExampleIter_FindElement() {
756+
input := `{
757+
"Image":
758+
{
759+
"Animated": false,
760+
"Height": 600,
761+
"IDs":
762+
[
763+
116,
764+
943,
765+
234,
766+
38793
767+
],
768+
"Thumbnail":
769+
{
770+
"Height": 125,
771+
"Url": "http://www.example.com/image/481989943",
772+
"Width": 100
773+
},
774+
"Title": "View from 15th Floor",
775+
"Width": 800
776+
},
777+
"Alt": "Image of city"
778+
}`
779+
pj, err := Parse([]byte(input), nil)
780+
if err != nil {
781+
log.Fatal(err)
782+
}
783+
i := pj.Iter()
784+
785+
// Find element in path.
786+
elem, err := i.FindElement("Image/Thumbnail/Width", nil)
787+
if err != nil {
788+
log.Fatal(err)
789+
}
790+
791+
// Print result:
792+
fmt.Println(elem.Type)
793+
fmt.Println(elem.Iter.StringCvt())
794+
795+
// Output:
796+
// int
797+
// 100 <nil>
798+
}

parsed_object.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package simdjson
1919
import (
2020
"errors"
2121
"fmt"
22+
"strings"
2223
)
2324

2425
// Object represents a JSON object.
@@ -137,6 +138,76 @@ func (o *Object) FindKey(key string, dst *Element) *Element {
137138
}
138139
}
139140

141+
// ErrPathNotFound is returned
142+
var ErrPathNotFound = errors.New("path not found")
143+
144+
// FindPath allows searching for fields and objects by path.
145+
// Separate each object name by /.
146+
// For example `Image/Url` will search the current object for an "Image"
147+
// object and return the value of the "Url" element.
148+
// ErrPathNotFound is returned if any part of the path cannot be found.
149+
// If the tape contains an error it will be returned.
150+
// The object will not be advanced.
151+
func (o *Object) FindPath(path string, dst *Element) (*Element, error) {
152+
tmp := o.tape.Iter()
153+
tmp.off = o.off
154+
p := strings.Split(path, "/")
155+
key := p[0]
156+
p = p[1:]
157+
for {
158+
typ := tmp.Advance()
159+
// We want name and at least one value.
160+
if typ != TypeString || tmp.off+1 >= len(tmp.tape.Tape) {
161+
return dst, ErrPathNotFound
162+
}
163+
// Advance must be string or end of object
164+
offset := tmp.cur
165+
length := tmp.tape.Tape[tmp.off]
166+
if int(length) != len(key) {
167+
// Skip the value.
168+
t := tmp.Advance()
169+
if t == TypeNone {
170+
// Not found...
171+
return dst, ErrPathNotFound
172+
}
173+
continue
174+
}
175+
// Read name
176+
name, err := tmp.tape.stringByteAt(offset, length)
177+
if err != nil {
178+
return dst, err
179+
}
180+
181+
if string(name) != key {
182+
// Skip the value
183+
tmp.Advance()
184+
continue
185+
}
186+
// Done...
187+
if len(p) == 0 {
188+
if dst == nil {
189+
dst = &Element{}
190+
}
191+
dst.Name = key
192+
dst.Type, err = tmp.AdvanceIter(&dst.Iter)
193+
if err != nil {
194+
return dst, err
195+
}
196+
return dst, nil
197+
}
198+
199+
t, err := tmp.AdvanceIter(&tmp)
200+
if err != nil {
201+
return dst, err
202+
}
203+
if t != TypeObject {
204+
return dst, fmt.Errorf("value of key %v is not an object", key)
205+
}
206+
key = p[0]
207+
p = p[1:]
208+
}
209+
}
210+
140211
// NextElement sets dst to the next element and returns the name.
141212
// TypeNone with nil error will be returned if there are no more elements.
142213
func (o *Object) NextElement(dst *Iter) (name string, t Type, err error) {

0 commit comments

Comments
 (0)