jsonpath/jsonpath.go
Rama master · réplica de lectura del repositorio original.
package jsonpath
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
// Eval is a half-baked json path implementation that needs some love
//
// Keys are separated by dots (i.e. data.name). Array elements are addressed by their index
// between brackets (i.e. data[0].name). Object keys that contain dots must be wrapped in
// single or double quotes between brackets (i.e. data["name.with.dots"]), because an unquoted
// dot is always treated as a key separator.
func Eval(path string, b []byte) (string, int, error) {
if len(path) == 0 && !(len(b) != 0 && b[0] == '[' && b[len(b)-1] == ']') {
// if there's no path AND the value is not a JSON array, then there's nothing to walk
return string(b), len(b), nil
}
var object interface{}
if err := json.Unmarshal(b, &object); err != nil {
return "", 0, err
}
return walk(path, object)
}
// walk traverses the object and returns the value as a string as well as its length
func walk(path string, object interface{}) (string, int, error) {
var keys []string
startOfCurrentKey, bracketDepth := 0, 0
var quoteCharacter byte
for i := range path {
// If we're inside a quoted accessor, everything up to the closing quote is part of the key
if quoteCharacter != 0 {
if path[i] == quoteCharacter {
quoteCharacter = 0
}
continue
}
if (path[i] == '"' || path[i] == '\'') && bracketDepth > 0 {
quoteCharacter = path[i]
continue
}
if path[i] == '[' {
bracketDepth++
} else if path[i] == ']' {
bracketDepth--
}
// If we encounter a dot, we've reached the end of a key unless we're inside a bracket
if path[i] == '.' && bracketDepth == 0 {
keys = append(keys, path[startOfCurrentKey:i])
startOfCurrentKey = i + 1
}
}
if startOfCurrentKey <= len(path) {
keys = append(keys, path[startOfCurrentKey:])
}
currentKey := keys[0]
switch value := extractValue(currentKey, object).(type) {
case map[string]interface{}:
newPath := strings.Replace(path, fmt.Sprintf("%s.", currentKey), "", 1)
if path == newPath {
// If the path hasn't changed, it means we're at the end of the path
// So we'll treat it as a string by re-marshaling it to JSON since it's a map.
// Note that the output JSON will be minified.
b, err := json.Marshal(value)
return string(b), len(b), err
}
return walk(newPath, value)
case string:
if len(keys) > 1 {
return "", 0, fmt.Errorf("couldn't walk through '%s', because '%s' was a string instead of an object", keys[1], currentKey)
}
return value, len(value), nil
case []interface{}:
return fmt.Sprintf("%v", value), len(value), nil
case interface{}:
newValue := fmt.Sprintf("%v", value)
return newValue, len(newValue), nil
default:
return "", 0, fmt.Errorf("couldn't walk through '%s' because type was '%T', but expected 'map[string]interface{}'", currentKey, value)
}
}
func extractValue(currentKey string, value interface{}) interface{} {
// Check if the current key has an accessor, i.e. [#] for an array index or ["key"] for an object key
if strings.HasSuffix(currentKey, "]") && strings.Contains(currentKey, "[") {
var hasMoreAccessors bool
var accessor string
startOfBracket, endOfBracket, bracketDepth := 0, 0, 0
var quoteCharacter byte
for i := range currentKey {
// If we're inside a quoted accessor, everything up to the closing quote is part of the key
if quoteCharacter != 0 {
if currentKey[i] == quoteCharacter {
quoteCharacter = 0
}
continue
}
if (currentKey[i] == '"' || currentKey[i] == '\'') && bracketDepth == 1 {
quoteCharacter = currentKey[i]
} else if currentKey[i] == '[' {
startOfBracket = i
bracketDepth++
} else if currentKey[i] == ']' && bracketDepth == 1 {
bracketDepth--
endOfBracket = i
accessor = currentKey[startOfBracket+1 : i]
if len(currentKey) > i+1 && currentKey[i+1] == '[' {
hasMoreAccessors = true // there's more accessors.
}
break
}
}
// Resolve the value that the accessor applies to. If there's a key before the accessor
// (i.e. data[0]), then the accessor applies to the value behind that key.
target := value
if keyBeforeAccessor := currentKey[:startOfBracket]; len(keyBeforeAccessor) > 0 {
valueAsMap, ok := value.(map[string]interface{})
if !ok || valueAsMap[keyBeforeAccessor] == nil {
return nil
}
target = valueAsMap[keyBeforeAccessor]
}
var extractedValue interface{}
if key, isQuoted := unquote(accessor); isQuoted {
// A quoted accessor (i.e. ["key"]) is an object key rather than an array index.
// This is the only way to address keys that contain dots, because an unquoted dot
// is always treated as a key separator.
valueAsMap, ok := target.(map[string]interface{})
if !ok {
return nil
}
extractedValue = valueAsMap[key]
} else {
arrayIndex, err := strconv.Atoi(accessor)
if err != nil || arrayIndex < 0 {
return nil
}
array, _ := target.([]interface{})
if len(array) <= arrayIndex {
return nil
}
extractedValue = array[arrayIndex]
}
if hasMoreAccessors {
return extractValue(currentKey[endOfBracket+1:], extractedValue)
}
return extractedValue
}
if valueAsSlice, ok := value.([]interface{}); ok {
// If the type is a slice, return it
// This happens when the body (value) is a JSON array
return valueAsSlice
}
if valueAsMap, ok := value.(map[string]interface{}); ok {
// If the value is a map, then we get the currentKey from that map
// This happens when the body (value) is a JSON object
return valueAsMap[currentKey]
}
// If the value is neither a map, nor a slice, nor an index, then we cannot retrieve the currentKey
// from said value. This usually happens when the body (value) is null.
return value
}
// unquote removes the surrounding single or double quotes from s, if there are any.
// The second return value indicates whether s was quoted.
func unquote(s string) (string, bool) {
if len(s) >= 2 && (s[0] == '"' || s[0] == '\'') && s[len(s)-1] == s[0] {
return s[1 : len(s)-1], true
}
return s, false
}