Codebase list golang-github-exponent-io-jsonpath / 3fb47109-db4b-47d5-a266-cb52e31f61e8/main decoder.go
3fb47109-db4b-47d5-a266-cb52e31f61e8/main

Tree @3fb47109-db4b-47d5-a266-cb52e31f61e8/main (Download .tar.gz)

decoder.go @3fb47109-db4b-47d5-a266-cb52e31f61e8/mainraw · history · blame

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
				}
			}
		}
	}
}