Skip to content

Commit

Permalink
Update api, examples and docs. Allow DecoderAction to return an error.
Browse files Browse the repository at this point in the history
  • Loading branch information
peterwald committed Oct 13, 2015
1 parent 663afef commit 3ac9268
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 57 deletions.
37 changes: 26 additions & 11 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
)

// KeyString is returned from Decoder.Token() to represent each object key string.
// KeyString is returned from Decoder.Token to represent each key in a JSON object value.
type KeyString string

// Decoder extends the Go runtime's encoding/json.Decoder to support navigating in a stream of JSON tokens.
Expand Down Expand Up @@ -156,37 +156,52 @@ func (d *Decoder) Token() (json.Token, error) {
return t, err
}

// Scan moves forward over the JSON stream consuming all the tokens at the current level (current object, current array)
// invoking each matching PathAction along the way.
//
// Scan returns true if there are more contiguous values to scan (for example in an array).
func (d *Decoder) Scan(ext *PathActions) (bool, error) {

matched := false
rootPath := d.Path()

// If this is an array path, increment the root path in our local copy.
if rootPath.inferContext() == arrValue {
rootPath.incTop()
}

for {
// advance the token position
_, err := d.Token()
if err != nil {
return matched, err
return false, err
}

match:
path := d.Path()
relPath := JsonPath{}
var relPath JsonPath

// fmt.Printf("rootPath: %v path: %v rel: %v\n", rootPath, path, relPath)
// capture the new JSON path
path := d.Path()

if len(path) > len(rootPath) {
// capture the path relative to where the scan started
relPath = path[len(rootPath):]
} else {
return matched, nil
// if the path is not longer than the root, then we are done with this scan
// return boolean flag indicating if there are more items to scan at the same level
return d.Decoder.More(), nil
}

if node, ok := ext.node.match(relPath); ok {
// match the relative path against the path actions
if node := ext.node.match(relPath); node != nil {
if node.action != nil {
matched = true
node.action(d)
if d.context == arrValue && d.Decoder.More() {
// we have a match so execute the action
err = node.action(d)
if err != nil {
return d.Decoder.More(), err
}
// The action may have advanced the decoder. If we are in an array, advancing it further would
// skip tokens. So, if we are scanning an array, jump to the top without advancing the token.
if d.path.inferContext() == arrValue && d.Decoder.More() {
goto match
}
}
Expand Down
75 changes: 75 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jsonpath
import (
"bytes"
"fmt"
"io"
)

func ExampleDecoder_SeekTo() {
Expand Down Expand Up @@ -32,3 +33,77 @@ func ExampleDecoder_SeekTo() {
// 0 Point Cr -10
// 1 Point G 218
}

func ExampleDecoder_Scan() {

var j = []byte(`{"colors":[
{"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10, "A": 58}},
{"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255, "A": 231}}
]}`)

var actions PathActions

// Extract the value at Point.A
actions.Add(func(d *Decoder) error {
var alpha int
err := d.Decode(&alpha)
fmt.Printf("Alpha: %v\n", alpha)
return err
}, "Point", "A")

w := NewDecoder(bytes.NewReader(j))
w.SeekTo("colors", 0)

var ok = true
var err error
for ok {
ok, err = w.Scan(&actions)
if err != nil && err != io.EOF {
panic(err)
}
}

// Output:
// Alpha: 58
// Alpha: 231
}

func ExampleDecoder_Scan_anyIndex() {

var j = []byte(`{"colors":[
{"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10, "A": 58}},
{"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255, "A": 231}}
]}`)

var actions PathActions

// Extract the value at Point.A
actions.Add(func(d *Decoder) error {
var cr int
err := d.Decode(&cr)
fmt.Printf("Chrominance (Cr): %v\n", cr)
return err
}, "colors", AnyIndex, "Point", "Cr")

actions.Add(func(d *Decoder) error {
var r int
err := d.Decode(&r)
fmt.Printf("Red: %v\n", r)
return err
}, "colors", AnyIndex, "Point", "R")

w := NewDecoder(bytes.NewReader(j))

var ok = true
var err error
for ok {
ok, err = w.Scan(&actions)
if err != nil && err != io.EOF {
panic(err)
}
}

// Output:
// Chrominance (Cr): -10
// Red: 98
}
17 changes: 11 additions & 6 deletions pathaction.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package jsonpath

// pathNode is used to construct a trie of paths to be matched
type pathNode struct {
matchOn interface{} // string, or integer
childNodes []pathNode
action DecodeAction
}

func (n *pathNode) match(path JsonPath) (*pathNode, bool) {
// match climbs the trie to find a node that matches the given JSON path.
func (n *pathNode) match(path JsonPath) *pathNode {
var node *pathNode = n
for _, ps := range path {
found := false
Expand All @@ -22,20 +24,23 @@ func (n *pathNode) match(path JsonPath) (*pathNode, bool) {
}
}
if !found {
return nil, false
return nil
}
}
return node, true
return node
}

// PathActions represents a collection of DecodeAction functions that should be called at certain path positions
// when scanning the JSON stream. PathActions can be created once and used many times in one or more JSON streams.
type PathActions struct {
node pathNode
}

type DecodeAction func(d *Decoder)
// DecodeAction handlers are called by the Decoder when scanning objects. See PathActions.Add for more detail.
type DecodeAction func(d *Decoder) error

// Action specifies an action to call on the Decoder when a particular path is encountered.
func (je *PathActions) Action(action DecodeAction, path ...interface{}) {
// Add specifies an action to call on the Decoder when the specified path is encountered.
func (je *PathActions) Add(action DecodeAction, path ...interface{}) {

var node *pathNode = &je.node
for _, ps := range path {
Expand Down
Loading

0 comments on commit 3ac9268

Please sign in to comment.