mirror of https://github.com/status-im/consul.git
580 lines
18 KiB
Go
580 lines
18 KiB
Go
/*
|
|
Copyright (c) 2014 Ashley Jeffs
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
|
|
// Package gabs implements a simplified wrapper around creating and parsing JSON.
|
|
package gabs
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"strings"
|
|
)
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
var (
|
|
// ErrOutOfBounds - Index out of bounds.
|
|
ErrOutOfBounds = errors.New("out of bounds")
|
|
|
|
// ErrNotObjOrArray - The target is not an object or array type.
|
|
ErrNotObjOrArray = errors.New("not an object or array")
|
|
|
|
// ErrNotObj - The target is not an object type.
|
|
ErrNotObj = errors.New("not an object")
|
|
|
|
// ErrNotArray - The target is not an array type.
|
|
ErrNotArray = errors.New("not an array")
|
|
|
|
// ErrPathCollision - Creating a path failed because an element collided with an existing value.
|
|
ErrPathCollision = errors.New("encountered value collision whilst building path")
|
|
|
|
// ErrInvalidInputObj - The input value was not a map[string]interface{}.
|
|
ErrInvalidInputObj = errors.New("invalid input object")
|
|
|
|
// ErrInvalidInputText - The input data could not be parsed.
|
|
ErrInvalidInputText = errors.New("input text could not be parsed")
|
|
|
|
// ErrInvalidPath - The filepath was not valid.
|
|
ErrInvalidPath = errors.New("invalid file path")
|
|
|
|
// ErrInvalidBuffer - The input buffer contained an invalid JSON string
|
|
ErrInvalidBuffer = errors.New("input buffer contained invalid JSON")
|
|
)
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// Container - an internal structure that holds a reference to the core interface map of the parsed
|
|
// json. Use this container to move context.
|
|
type Container struct {
|
|
object interface{}
|
|
}
|
|
|
|
// Data - Return the contained data as an interface{}.
|
|
func (g *Container) Data() interface{} {
|
|
if g == nil {
|
|
return nil
|
|
}
|
|
return g.object
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// Path - Search for a value using dot notation.
|
|
func (g *Container) Path(path string) *Container {
|
|
return g.Search(strings.Split(path, ".")...)
|
|
}
|
|
|
|
// Search - Attempt to find and return an object within the JSON structure by specifying the
|
|
// hierarchy of field names to locate the target. If the search encounters an array and has not
|
|
// reached the end target then it will iterate each object of the array for the target and return
|
|
// all of the results in a JSON array.
|
|
func (g *Container) Search(hierarchy ...string) *Container {
|
|
var object interface{}
|
|
|
|
object = g.Data()
|
|
for target := 0; target < len(hierarchy); target++ {
|
|
if mmap, ok := object.(map[string]interface{}); ok {
|
|
object, ok = mmap[hierarchy[target]]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
} else if marray, ok := object.([]interface{}); ok {
|
|
tmpArray := []interface{}{}
|
|
for _, val := range marray {
|
|
tmpGabs := &Container{val}
|
|
res := tmpGabs.Search(hierarchy[target:]...)
|
|
if res != nil {
|
|
tmpArray = append(tmpArray, res.Data())
|
|
}
|
|
}
|
|
if len(tmpArray) == 0 {
|
|
return nil
|
|
}
|
|
return &Container{tmpArray}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return &Container{object}
|
|
}
|
|
|
|
// S - Shorthand method, does the same thing as Search.
|
|
func (g *Container) S(hierarchy ...string) *Container {
|
|
return g.Search(hierarchy...)
|
|
}
|
|
|
|
// Exists - Checks whether a path exists.
|
|
func (g *Container) Exists(hierarchy ...string) bool {
|
|
return g.Search(hierarchy...) != nil
|
|
}
|
|
|
|
// ExistsP - Checks whether a dot notation path exists.
|
|
func (g *Container) ExistsP(path string) bool {
|
|
return g.Exists(strings.Split(path, ".")...)
|
|
}
|
|
|
|
// Index - Attempt to find and return an object within a JSON array by index.
|
|
func (g *Container) Index(index int) *Container {
|
|
if array, ok := g.Data().([]interface{}); ok {
|
|
if index >= len(array) {
|
|
return &Container{nil}
|
|
}
|
|
return &Container{array[index]}
|
|
}
|
|
return &Container{nil}
|
|
}
|
|
|
|
// Children - Return a slice of all the children of the array. This also works for objects, however,
|
|
// the children returned for an object will NOT be in order and you lose the names of the returned
|
|
// objects this way.
|
|
func (g *Container) Children() ([]*Container, error) {
|
|
if array, ok := g.Data().([]interface{}); ok {
|
|
children := make([]*Container, len(array))
|
|
for i := 0; i < len(array); i++ {
|
|
children[i] = &Container{array[i]}
|
|
}
|
|
return children, nil
|
|
}
|
|
if mmap, ok := g.Data().(map[string]interface{}); ok {
|
|
children := []*Container{}
|
|
for _, obj := range mmap {
|
|
children = append(children, &Container{obj})
|
|
}
|
|
return children, nil
|
|
}
|
|
return nil, ErrNotObjOrArray
|
|
}
|
|
|
|
// ChildrenMap - Return a map of all the children of an object.
|
|
func (g *Container) ChildrenMap() (map[string]*Container, error) {
|
|
if mmap, ok := g.Data().(map[string]interface{}); ok {
|
|
children := map[string]*Container{}
|
|
for name, obj := range mmap {
|
|
children[name] = &Container{obj}
|
|
}
|
|
return children, nil
|
|
}
|
|
return nil, ErrNotObj
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be
|
|
// constructed, and if a collision occurs with a non object type whilst iterating the path an error
|
|
// is returned.
|
|
func (g *Container) Set(value interface{}, path ...string) (*Container, error) {
|
|
if len(path) == 0 {
|
|
g.object = value
|
|
return g, nil
|
|
}
|
|
var object interface{}
|
|
if g.object == nil {
|
|
g.object = map[string]interface{}{}
|
|
}
|
|
object = g.object
|
|
for target := 0; target < len(path); target++ {
|
|
if mmap, ok := object.(map[string]interface{}); ok {
|
|
if target == len(path)-1 {
|
|
mmap[path[target]] = value
|
|
} else if mmap[path[target]] == nil {
|
|
mmap[path[target]] = map[string]interface{}{}
|
|
}
|
|
object = mmap[path[target]]
|
|
} else {
|
|
return &Container{nil}, ErrPathCollision
|
|
}
|
|
}
|
|
return &Container{object}, nil
|
|
}
|
|
|
|
// SetP - Does the same as Set, but using a dot notation JSON path.
|
|
func (g *Container) SetP(value interface{}, path string) (*Container, error) {
|
|
return g.Set(value, strings.Split(path, ".")...)
|
|
}
|
|
|
|
// SetIndex - Set a value of an array element based on the index.
|
|
func (g *Container) SetIndex(value interface{}, index int) (*Container, error) {
|
|
if array, ok := g.Data().([]interface{}); ok {
|
|
if index >= len(array) {
|
|
return &Container{nil}, ErrOutOfBounds
|
|
}
|
|
array[index] = value
|
|
return &Container{array[index]}, nil
|
|
}
|
|
return &Container{nil}, ErrNotArray
|
|
}
|
|
|
|
// Object - Create a new JSON object at a path. Returns an error if the path contains a collision
|
|
// with a non object type.
|
|
func (g *Container) Object(path ...string) (*Container, error) {
|
|
return g.Set(map[string]interface{}{}, path...)
|
|
}
|
|
|
|
// ObjectP - Does the same as Object, but using a dot notation JSON path.
|
|
func (g *Container) ObjectP(path string) (*Container, error) {
|
|
return g.Object(strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an
|
|
// array or the index is out of bounds.
|
|
func (g *Container) ObjectI(index int) (*Container, error) {
|
|
return g.SetIndex(map[string]interface{}{}, index)
|
|
}
|
|
|
|
// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with
|
|
// a non object type.
|
|
func (g *Container) Array(path ...string) (*Container, error) {
|
|
return g.Set([]interface{}{}, path...)
|
|
}
|
|
|
|
// ArrayP - Does the same as Array, but using a dot notation JSON path.
|
|
func (g *Container) ArrayP(path string) (*Container, error) {
|
|
return g.Array(strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an
|
|
// array or the index is out of bounds.
|
|
func (g *Container) ArrayI(index int) (*Container, error) {
|
|
return g.SetIndex([]interface{}{}, index)
|
|
}
|
|
|
|
// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the
|
|
// path contains a collision with a non object type.
|
|
func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) {
|
|
a := make([]interface{}, size)
|
|
return g.Set(a, path...)
|
|
}
|
|
|
|
// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path.
|
|
func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) {
|
|
return g.ArrayOfSize(size, strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error
|
|
// if the object is not an array or the index is out of bounds.
|
|
func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) {
|
|
a := make([]interface{}, size)
|
|
return g.SetIndex(a, index)
|
|
}
|
|
|
|
// Delete - Delete an element at a JSON path, an error is returned if the element does not exist.
|
|
func (g *Container) Delete(path ...string) error {
|
|
var object interface{}
|
|
|
|
if g.object == nil {
|
|
return ErrNotObj
|
|
}
|
|
object = g.object
|
|
for target := 0; target < len(path); target++ {
|
|
if mmap, ok := object.(map[string]interface{}); ok {
|
|
if target == len(path)-1 {
|
|
if _, ok := mmap[path[target]]; ok {
|
|
delete(mmap, path[target])
|
|
} else {
|
|
return ErrNotObj
|
|
}
|
|
}
|
|
object = mmap[path[target]]
|
|
} else {
|
|
return ErrNotObj
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteP - Does the same as Delete, but using a dot notation JSON path.
|
|
func (g *Container) DeleteP(path string) error {
|
|
return g.Delete(strings.Split(path, ".")...)
|
|
}
|
|
|
|
// Merge - Merges two gabs-containers
|
|
func (g *Container) Merge(toMerge *Container) error {
|
|
var recursiveFnc func(map[string]interface{}, []string) error
|
|
recursiveFnc = func(mmap map[string]interface{}, path []string) error {
|
|
for key, value := range mmap {
|
|
newPath := append(path, key)
|
|
if g.Exists(newPath...) {
|
|
target := g.Search(newPath...)
|
|
switch t := value.(type) {
|
|
case map[string]interface{}:
|
|
switch targetV := target.Data().(type) {
|
|
case map[string]interface{}:
|
|
if err := recursiveFnc(t, newPath); err != nil {
|
|
return err
|
|
}
|
|
case []interface{}:
|
|
g.Set(append(targetV, t), newPath...)
|
|
default:
|
|
newSlice := append([]interface{}{}, targetV)
|
|
g.Set(append(newSlice, t), newPath...)
|
|
}
|
|
case []interface{}:
|
|
for _, valueOfSlice := range t {
|
|
if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
switch targetV := target.Data().(type) {
|
|
case []interface{}:
|
|
g.Set(append(targetV, t), newPath...)
|
|
default:
|
|
newSlice := append([]interface{}{}, targetV)
|
|
g.Set(append(newSlice, t), newPath...)
|
|
}
|
|
}
|
|
} else {
|
|
// path doesn't exist. So set the value
|
|
if _, err := g.Set(value, newPath...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if mmap, ok := toMerge.Data().(map[string]interface{}); ok {
|
|
return recursiveFnc(mmap, []string{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
/*
|
|
Array modification/search - Keeping these options simple right now, no need for anything more
|
|
complicated since you can just cast to []interface{}, modify and then reassign with Set.
|
|
*/
|
|
|
|
// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be
|
|
// converted into one, with its contents as the first element of the array.
|
|
func (g *Container) ArrayAppend(value interface{}, path ...string) error {
|
|
if array, ok := g.Search(path...).Data().([]interface{}); ok {
|
|
array = append(array, value)
|
|
_, err := g.Set(array, path...)
|
|
return err
|
|
}
|
|
|
|
newArray := []interface{}{}
|
|
newArray = append(newArray, g.Search(path...).Data())
|
|
newArray = append(newArray, value)
|
|
|
|
_, err := g.Set(newArray, path...)
|
|
return err
|
|
}
|
|
|
|
// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path.
|
|
func (g *Container) ArrayAppendP(value interface{}, path string) error {
|
|
return g.ArrayAppend(value, strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ArrayRemove - Remove an element from a JSON array.
|
|
func (g *Container) ArrayRemove(index int, path ...string) error {
|
|
if index < 0 {
|
|
return ErrOutOfBounds
|
|
}
|
|
array, ok := g.Search(path...).Data().([]interface{})
|
|
if !ok {
|
|
return ErrNotArray
|
|
}
|
|
if index < len(array) {
|
|
array = append(array[:index], array[index+1:]...)
|
|
} else {
|
|
return ErrOutOfBounds
|
|
}
|
|
_, err := g.Set(array, path...)
|
|
return err
|
|
}
|
|
|
|
// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path.
|
|
func (g *Container) ArrayRemoveP(index int, path string) error {
|
|
return g.ArrayRemove(index, strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ArrayElement - Access an element from a JSON array.
|
|
func (g *Container) ArrayElement(index int, path ...string) (*Container, error) {
|
|
if index < 0 {
|
|
return &Container{nil}, ErrOutOfBounds
|
|
}
|
|
array, ok := g.Search(path...).Data().([]interface{})
|
|
if !ok {
|
|
return &Container{nil}, ErrNotArray
|
|
}
|
|
if index < len(array) {
|
|
return &Container{array[index]}, nil
|
|
}
|
|
return &Container{nil}, ErrOutOfBounds
|
|
}
|
|
|
|
// ArrayElementP - Access an element from a JSON array using a dot notation JSON path.
|
|
func (g *Container) ArrayElementP(index int, path string) (*Container, error) {
|
|
return g.ArrayElement(index, strings.Split(path, ".")...)
|
|
}
|
|
|
|
// ArrayCount - Count the number of elements in a JSON array.
|
|
func (g *Container) ArrayCount(path ...string) (int, error) {
|
|
if array, ok := g.Search(path...).Data().([]interface{}); ok {
|
|
return len(array), nil
|
|
}
|
|
return 0, ErrNotArray
|
|
}
|
|
|
|
// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path.
|
|
func (g *Container) ArrayCountP(path string) (int, error) {
|
|
return g.ArrayCount(strings.Split(path, ".")...)
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
// Bytes - Converts the contained object back to a JSON []byte blob.
|
|
func (g *Container) Bytes() []byte {
|
|
if g.Data() != nil {
|
|
if bytes, err := json.Marshal(g.object); err == nil {
|
|
return bytes
|
|
}
|
|
}
|
|
return []byte("{}")
|
|
}
|
|
|
|
// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent.
|
|
func (g *Container) BytesIndent(prefix string, indent string) []byte {
|
|
if g.object != nil {
|
|
if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil {
|
|
return bytes
|
|
}
|
|
}
|
|
return []byte("{}")
|
|
}
|
|
|
|
// String - Converts the contained object to a JSON formatted string.
|
|
func (g *Container) String() string {
|
|
return string(g.Bytes())
|
|
}
|
|
|
|
// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent.
|
|
func (g *Container) StringIndent(prefix string, indent string) string {
|
|
return string(g.BytesIndent(prefix, indent))
|
|
}
|
|
|
|
// EncodeOpt is a functional option for the EncodeJSON method.
|
|
type EncodeOpt func(e *json.Encoder)
|
|
|
|
// EncodeOptHTMLEscape sets the encoder to escape the JSON for html.
|
|
func EncodeOptHTMLEscape(doEscape bool) EncodeOpt {
|
|
return func(e *json.Encoder) {
|
|
e.SetEscapeHTML(doEscape)
|
|
}
|
|
}
|
|
|
|
// EncodeOptIndent sets the encoder to indent the JSON output.
|
|
func EncodeOptIndent(prefix string, indent string) EncodeOpt {
|
|
return func(e *json.Encoder) {
|
|
e.SetIndent(prefix, indent)
|
|
}
|
|
}
|
|
|
|
// EncodeJSON - Encodes the contained object back to a JSON formatted []byte
|
|
// using a variant list of modifier functions for the encoder being used.
|
|
// Functions for modifying the output are prefixed with EncodeOpt, e.g.
|
|
// EncodeOptHTMLEscape.
|
|
func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte {
|
|
var b bytes.Buffer
|
|
encoder := json.NewEncoder(&b)
|
|
encoder.SetEscapeHTML(false) // Do not escape by default.
|
|
for _, opt := range encodeOpts {
|
|
opt(encoder)
|
|
}
|
|
if err := encoder.Encode(g.object); err != nil {
|
|
return []byte("{}")
|
|
}
|
|
result := b.Bytes()
|
|
if len(result) > 0 {
|
|
result = result[:len(result)-1]
|
|
}
|
|
return result
|
|
}
|
|
|
|
// New - Create a new gabs JSON object.
|
|
func New() *Container {
|
|
return &Container{map[string]interface{}{}}
|
|
}
|
|
|
|
// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object.
|
|
func Consume(root interface{}) (*Container, error) {
|
|
return &Container{root}, nil
|
|
}
|
|
|
|
// ParseJSON - Convert a string into a representation of the parsed JSON.
|
|
func ParseJSON(sample []byte) (*Container, error) {
|
|
var gabs Container
|
|
|
|
if err := json.Unmarshal(sample, &gabs.object); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &gabs, nil
|
|
}
|
|
|
|
// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON.
|
|
func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
|
|
var gabs Container
|
|
|
|
if err := decoder.Decode(&gabs.object); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &gabs, nil
|
|
}
|
|
|
|
// ParseJSONFile - Read a file and convert into a representation of the parsed JSON.
|
|
func ParseJSONFile(path string) (*Container, error) {
|
|
if len(path) > 0 {
|
|
cBytes, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
container, err := ParseJSON(cBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return container, nil
|
|
}
|
|
return nil, ErrInvalidPath
|
|
}
|
|
|
|
// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON.
|
|
func ParseJSONBuffer(buffer io.Reader) (*Container, error) {
|
|
var gabs Container
|
|
jsonDecoder := json.NewDecoder(buffer)
|
|
if err := jsonDecoder.Decode(&gabs.object); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &gabs, nil
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|