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