211 lines
5.2 KiB
Go
211 lines
5.2 KiB
Go
|
package jsonpath
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"io"
|
||
|
)
|
||
|
|
||
|
// 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.
|
||
|
type Decoder struct {
|
||
|
json.Decoder
|
||
|
|
||
|
path JsonPath
|
||
|
context jsonContext
|
||
|
}
|
||
|
|
||
|
// NewDecoder creates a new instance of the extended JSON Decoder.
|
||
|
func NewDecoder(r io.Reader) *Decoder {
|
||
|
return &Decoder{Decoder: *json.NewDecoder(r)}
|
||
|
}
|
||
|
|
||
|
// SeekTo causes the Decoder to move forward to a given path in the JSON structure.
|
||
|
//
|
||
|
// The path argument must consist of strings or integers. Each string specifies an JSON object key, and
|
||
|
// each integer specifies an index into a JSON array.
|
||
|
//
|
||
|
// Consider the JSON structure
|
||
|
//
|
||
|
// { "a": [0,"s",12e4,{"b":0,"v":35} ] }
|
||
|
//
|
||
|
// SeekTo("a",3,"v") will move to the value referenced by the "a" key in the current object,
|
||
|
// followed by a move to the 4th value (index 3) in the array, followed by a move to the value at key "v".
|
||
|
// In this example, a subsequent call to the decoder's Decode() would unmarshal the value 35.
|
||
|
//
|
||
|
// SeekTo returns a boolean value indicating whether a match was found.
|
||
|
//
|
||
|
// Decoder is intended to be used with a stream of tokens. As a result it navigates forward only.
|
||
|
func (d *Decoder) SeekTo(path ...interface{}) (bool, error) {
|
||
|
|
||
|
if len(path) == 0 {
|
||
|
return len(d.path) == 0, nil
|
||
|
}
|
||
|
last := len(path) - 1
|
||
|
if i, ok := path[last].(int); ok {
|
||
|
path[last] = i - 1
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
if d.path.Equal(path) {
|
||
|
return true, nil
|
||
|
}
|
||
|
_, err := d.Token()
|
||
|
if err == io.EOF {
|
||
|
return false, nil
|
||
|
} else if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. This is
|
||
|
// equivalent to encoding/json.Decode().
|
||
|
func (d *Decoder) Decode(v interface{}) error {
|
||
|
switch d.context {
|
||
|
case objValue:
|
||
|
d.context = objKey
|
||
|
break
|
||
|
case arrValue:
|
||
|
d.path.incTop()
|
||
|
break
|
||
|
}
|
||
|
return d.Decoder.Decode(v)
|
||
|
}
|
||
|
|
||
|
// Path returns a slice of string and/or int values representing the path from the root of the JSON object to the
|
||
|
// position of the most-recently parsed token.
|
||
|
func (d *Decoder) Path() JsonPath {
|
||
|
p := make(JsonPath, len(d.path))
|
||
|
copy(p, d.path)
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
// Token is equivalent to the Token() method on json.Decoder. The primary difference is that it distinguishes
|
||
|
// between strings that are keys and and strings that are values. String tokens that are object keys are returned as a
|
||
|
// KeyString rather than as a native string.
|
||
|
func (d *Decoder) Token() (json.Token, error) {
|
||
|
t, err := d.Decoder.Token()
|
||
|
if err != nil {
|
||
|
return t, err
|
||
|
}
|
||
|
|
||
|
if t == nil {
|
||
|
switch d.context {
|
||
|
case objValue:
|
||
|
d.context = objKey
|
||
|
break
|
||
|
case arrValue:
|
||
|
d.path.incTop()
|
||
|
break
|
||
|
}
|
||
|
return t, err
|
||
|
}
|
||
|
|
||
|
switch t := t.(type) {
|
||
|
case json.Delim:
|
||
|
switch t {
|
||
|
case json.Delim('{'):
|
||
|
if d.context == arrValue {
|
||
|
d.path.incTop()
|
||
|
}
|
||
|
d.path.push("")
|
||
|
d.context = objKey
|
||
|
break
|
||
|
case json.Delim('}'):
|
||
|
d.path.pop()
|
||
|
d.context = d.path.inferContext()
|
||
|
break
|
||
|
case json.Delim('['):
|
||
|
if d.context == arrValue {
|
||
|
d.path.incTop()
|
||
|
}
|
||
|
d.path.push(-1)
|
||
|
d.context = arrValue
|
||
|
break
|
||
|
case json.Delim(']'):
|
||
|
d.path.pop()
|
||
|
d.context = d.path.inferContext()
|
||
|
break
|
||
|
}
|
||
|
case float64, json.Number, bool:
|
||
|
switch d.context {
|
||
|
case objValue:
|
||
|
d.context = objKey
|
||
|
break
|
||
|
case arrValue:
|
||
|
d.path.incTop()
|
||
|
break
|
||
|
}
|
||
|
break
|
||
|
case string:
|
||
|
switch d.context {
|
||
|
case objKey:
|
||
|
d.path.nameTop(t)
|
||
|
d.context = objValue
|
||
|
return KeyString(t), err
|
||
|
case objValue:
|
||
|
d.context = objKey
|
||
|
case arrValue:
|
||
|
d.path.incTop()
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
|
||
|
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 false, err
|
||
|
}
|
||
|
|
||
|
match:
|
||
|
var relPath JsonPath
|
||
|
|
||
|
// 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 {
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// match the relative path against the path actions
|
||
|
if node := ext.node.match(relPath); node != nil {
|
||
|
if node.action != nil {
|
||
|
// 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
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|