github.com/richardwilkes/toolbox@v1.121.0/formats/json/json.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  // Package json provides manipulation of JSON data.
    11  package json
    12  
    13  import (
    14  	"bytes"
    15  	"encoding/json"
    16  	"io"
    17  	"strconv"
    18  	"strings"
    19  )
    20  
    21  // Data provides conveniences for working with JSON data.
    22  type Data struct {
    23  	obj any
    24  }
    25  
    26  // MustParse is the same as calling Parse, but without the error code on return.
    27  func MustParse(data []byte) *Data {
    28  	result, err := Parse(data)
    29  	if err != nil {
    30  		result = &Data{}
    31  	}
    32  	return result
    33  }
    34  
    35  // Parse JSON data from bytes. If the data can't be loaded, a valid, empty Data will still be returned, along with an
    36  // error.
    37  func Parse(data []byte) (*Data, error) {
    38  	var obj any
    39  	decoder := json.NewDecoder(bytes.NewReader(data))
    40  	decoder.UseNumber()
    41  	if err := decoder.Decode(&obj); err != nil {
    42  		return &Data{}, err
    43  	}
    44  	return &Data{obj: obj}, nil
    45  }
    46  
    47  // MustParseStream is the same as calling ParseStream, but without the error code on return.
    48  func MustParseStream(in io.Reader) *Data {
    49  	result, err := ParseStream(in)
    50  	if err != nil {
    51  		result = &Data{}
    52  	}
    53  	return result
    54  }
    55  
    56  // ParseStream parses JSON data from the stream. If the data can't be loaded, a valid, empty Data will still be
    57  // returned, along with an error.
    58  func ParseStream(in io.Reader) (*Data, error) {
    59  	var obj any
    60  	decoder := json.NewDecoder(in)
    61  	decoder.UseNumber()
    62  	if err := decoder.Decode(&obj); err != nil {
    63  		return &Data{}, err
    64  	}
    65  	return &Data{obj: obj}, nil
    66  }
    67  
    68  // Raw returns the underlying data.
    69  func (j *Data) Raw() any {
    70  	return j.obj
    71  }
    72  
    73  // Path searches the dot-separated path and returns the object at that point. If the search encounters an array and has
    74  // not reached the end target, then it will iterate through the array for the target and return all results in a Data
    75  // array.
    76  func (j *Data) Path(path string) *Data {
    77  	return j.path(strings.Split(path, ".")...)
    78  }
    79  
    80  func (j *Data) path(path ...string) *Data {
    81  	if len(path) == 1 && path[0] == "" {
    82  		path = nil
    83  	}
    84  	obj := j.obj
    85  	for i := 0; i < len(path); i++ {
    86  		if m, ok := obj.(map[string]any); ok {
    87  			obj = m[path[i]]
    88  		} else {
    89  			var a []any
    90  			if a, ok = obj.([]any); ok {
    91  				t := make([]any, 0)
    92  				for _, one := range a {
    93  					tj := &Data{obj: one}
    94  					if result := tj.path(path[i:]...).obj; result != nil {
    95  						t = append(t, result)
    96  					}
    97  				}
    98  				if len(a) != 0 {
    99  					return &Data{obj: t}
   100  				}
   101  			}
   102  			return &Data{}
   103  		}
   104  	}
   105  	return &Data{obj}
   106  }
   107  
   108  // Exists returns true if the path exists in the data.
   109  func (j *Data) Exists(path string) bool {
   110  	return j.Path(path).obj != nil
   111  }
   112  
   113  // IsArray returns true if this is a Data array.
   114  func (j *Data) IsArray() bool {
   115  	_, ok := j.obj.([]any)
   116  	return ok
   117  }
   118  
   119  // IsMap returns true if this is a Data map.
   120  func (j *Data) IsMap() bool {
   121  	_, ok := j.obj.(map[string]any)
   122  	return ok
   123  }
   124  
   125  // Keys returns the keys of a map, or an empty slice if this is not a map.
   126  func (j *Data) Keys() []string {
   127  	if m, ok := j.obj.(map[string]any); ok {
   128  		keys := make([]string, 0, len(m))
   129  		for key := range m {
   130  			keys = append(keys, key)
   131  		}
   132  		return keys
   133  	}
   134  	return make([]string, 0)
   135  }
   136  
   137  // Size returns the number of elements in an array or map, or 0 if this is neither type.
   138  func (j *Data) Size() int {
   139  	if m, ok := j.obj.(map[string]any); ok {
   140  		return len(m)
   141  	}
   142  	if a, ok := j.obj.([]any); ok {
   143  		return len(a)
   144  	}
   145  	return 0
   146  }
   147  
   148  // Index returns the object at the specified index within an array, or nil if this isn't an array or the index isn't
   149  // valid.
   150  func (j *Data) Index(index int) *Data {
   151  	if a, ok := j.obj.([]any); ok {
   152  		if index >= 0 && index < len(a) {
   153  			return &Data{obj: a[index]}
   154  		}
   155  	}
   156  	return &Data{}
   157  }
   158  
   159  // Bytes converts the data into a Data []byte.
   160  func (j *Data) Bytes() []byte {
   161  	if j.obj != nil {
   162  		if data, err := json.Marshal(j.obj); err == nil {
   163  			return data
   164  		}
   165  	}
   166  	return []byte("{}")
   167  }
   168  
   169  // String converts the data into a Data string.
   170  func (j *Data) String() string {
   171  	return string(j.Bytes())
   172  }
   173  
   174  // Str extracts a string from the path. Returns the empty string if the path isn't present or isn't a string type.
   175  func (j *Data) Str(path string) string {
   176  	if str, ok := j.Path(path).obj.(string); ok {
   177  		return str
   178  	}
   179  	return ""
   180  }
   181  
   182  // Bool extracts a bool from the path. Returns false if the path isn't present or isn't a boolean type.
   183  func (j *Data) Bool(path string) bool {
   184  	if b, ok := j.Path(path).obj.(bool); ok {
   185  		return b
   186  	}
   187  	return false
   188  }
   189  
   190  // BoolRelaxed extracts a bool from the path. Returns false if the path isn't present or can't be converted to a boolean
   191  // type.
   192  func (j *Data) BoolRelaxed(path string) bool {
   193  	if b, ok := j.Path(path).obj.(bool); ok {
   194  		return b
   195  	}
   196  	return strings.EqualFold(j.Str(path), "true")
   197  }
   198  
   199  // Float64 extracts an float64 from the path. Returns 0 if the path isn't present or isn't a numeric type.
   200  func (j *Data) Float64(path string) float64 {
   201  	if n, ok := j.Path(path).obj.(json.Number); ok {
   202  		if f, err := n.Float64(); err == nil {
   203  			return f
   204  		}
   205  	}
   206  	return 0
   207  }
   208  
   209  // Float64Relaxed extracts an float64 from the path. Returns 0 if the path isn't present or can't be converted to a
   210  // numeric type.
   211  func (j *Data) Float64Relaxed(path string) float64 {
   212  	if n, ok := j.Path(path).obj.(json.Number); ok {
   213  		if f, err := n.Float64(); err == nil {
   214  			return f
   215  		}
   216  	} else {
   217  		if f, err := strconv.ParseFloat(j.Str(path), 64); err == nil {
   218  			return f
   219  		}
   220  	}
   221  	return 0
   222  }
   223  
   224  // Int64 extracts an int64 from the path. Returns 0 if the path isn't present or isn't a numeric type.
   225  func (j *Data) Int64(path string) int64 {
   226  	if n, ok := j.Path(path).obj.(json.Number); ok {
   227  		if i, err := n.Int64(); err == nil {
   228  			return i
   229  		}
   230  	}
   231  	return 0
   232  }
   233  
   234  // Int64Relaxed extracts an int64 from the path. Returns 0 if the path isn't present or can't be converted to a numeric
   235  // type.
   236  func (j *Data) Int64Relaxed(path string) int64 {
   237  	if n, ok := j.Path(path).obj.(json.Number); ok {
   238  		if i, err := n.Int64(); err == nil {
   239  			return i
   240  		}
   241  	} else {
   242  		if i, err := strconv.ParseInt(j.Str(path), 10, 64); err == nil {
   243  			return i
   244  		}
   245  	}
   246  	return 0
   247  }
   248  
   249  // Unmarshal parses the data at the path and stores the result into value.
   250  func (j *Data) Unmarshal(path string, value any) error {
   251  	return json.Unmarshal(j.Path(path).Bytes(), value)
   252  }
   253  
   254  // NewMap creates a map at the specified path. Any parts of the path that do not exist will be created. Returns true if
   255  // successful, or false if a collision occurs with a non-object type while traversing the path.
   256  func (j *Data) NewMap(path string) bool {
   257  	return j.set(path, make(map[string]any))
   258  }
   259  
   260  // NewArray creates an array at the specified path. Any parts of the path that do not exist will be created. Returns
   261  // true if successful, or false if a collision occurs with a non-object type while traversing the path.
   262  func (j *Data) NewArray(path string) bool {
   263  	return j.set(path, make([]any, 0))
   264  }
   265  
   266  // SetStr a string at the specified path. Any parts of the path that do not exist will be created. Returns true if
   267  // successful, or false if a collision occurs with a non-object type while traversing the path.
   268  func (j *Data) SetStr(path, value string) bool {
   269  	return j.set(path, value)
   270  }
   271  
   272  // SetBool a bool at the specified path. Any parts of the path that do not exist will be created. Returns true if
   273  // successful, or false if a collision occurs with a non-object type while traversing the path.
   274  func (j *Data) SetBool(path string, value bool) bool {
   275  	return j.set(path, value)
   276  }
   277  
   278  // SetFloat64 a float64 at the specified path. Any parts of the path that do not exist will be created. Returns true if
   279  // successful, or false if a collision occurs with a non-object type while traversing the path.
   280  func (j *Data) SetFloat64(path string, value float64) bool {
   281  	return j.set(path, value)
   282  }
   283  
   284  // SetInt64 an int64 at the specified path. Any parts of the path that do not exist will be created. Returns true if
   285  // successful, or false if a collision occurs with a non-object type while traversing the path.
   286  func (j *Data) SetInt64(path string, value int64) bool {
   287  	return j.set(path, value)
   288  }
   289  
   290  // Set a Data value at the specified path. Any parts of the path that do not exist will be created. Returns true if
   291  // successful, or false if a collision occurs with a non-object type while traversing the path.
   292  func (j *Data) Set(path string, value *Data) bool {
   293  	var v any
   294  	if value != nil {
   295  		v = value.obj
   296  	}
   297  	return j.set(path, v)
   298  }
   299  
   300  func (j *Data) set(path string, value any) bool {
   301  	paths := strings.Split(path, ".")
   302  	if len(paths) == 0 {
   303  		j.obj = value
   304  	} else {
   305  		if j.obj == nil {
   306  			j.obj = make(map[string]any)
   307  		}
   308  		obj := j.obj
   309  		for i := 0; i < len(paths); i++ {
   310  			if m, ok := obj.(map[string]any); ok {
   311  				if i == len(paths)-1 {
   312  					m[paths[i]] = value
   313  				} else if m[paths[i]] == nil {
   314  					m[paths[i]] = make(map[string]any)
   315  				}
   316  				obj = m[paths[i]]
   317  			} else {
   318  				return false
   319  			}
   320  		}
   321  	}
   322  	return true
   323  }
   324  
   325  // AppendMap appends a new map to an array at the specified path. The array must already exist. Returns true if
   326  // successful.
   327  func (j *Data) AppendMap(path string) bool {
   328  	return j.append(path, make(map[string]any))
   329  }
   330  
   331  // AppendArray appends a new array to an array at the specified path. The array must already exist. Returns true if
   332  // successful.
   333  func (j *Data) AppendArray(path string) bool {
   334  	return j.append(path, make([]any, 0))
   335  }
   336  
   337  // AppendStr appends a string to an array at the specified path. The array must already exist. Returns true if
   338  // successful.
   339  func (j *Data) AppendStr(path, value string) bool {
   340  	return j.append(path, value)
   341  }
   342  
   343  // AppendBool appends a bool to an array at the specified path. The array must already exist. Returns true if
   344  // successful.
   345  func (j *Data) AppendBool(path string, value bool) bool {
   346  	return j.append(path, value)
   347  }
   348  
   349  // AppendFloat64 appends a float64 to an array at the specified path. The array must already exist. Returns true if
   350  // successful.
   351  func (j *Data) AppendFloat64(path string, value float64) bool {
   352  	return j.append(path, value)
   353  }
   354  
   355  // AppendInt64 appends an int64 to an array at the specified path. The array must already exist. Returns true if
   356  // successful.
   357  func (j *Data) AppendInt64(path string, value int64) bool {
   358  	return j.append(path, value)
   359  }
   360  
   361  // Append a Data value to an array at the specified path. The array must already exist. Returns true if successful.
   362  func (j *Data) Append(path string, value *Data) bool {
   363  	var v any
   364  	if value != nil {
   365  		v = value.obj
   366  	}
   367  	return j.append(path, v)
   368  }
   369  
   370  func (j *Data) append(path string, value any) bool {
   371  	if array, ok := j.Path(path).obj.([]any); ok {
   372  		return j.set(path, append(array, value))
   373  	}
   374  	return false
   375  }
   376  
   377  // Delete a value at the specified path. Returns true if successful.
   378  func (j *Data) Delete(path string) bool {
   379  	if j.obj == nil {
   380  		return false
   381  	}
   382  	paths := strings.Split(path, ".")
   383  	if len(paths) == 0 {
   384  		j.obj = nil
   385  		return true
   386  	}
   387  	obj := j.obj
   388  	for i := 0; i < len(paths); i++ {
   389  		if m, ok := obj.(map[string]any); ok {
   390  			if i == len(paths)-1 {
   391  				if _, ok = m[paths[i]]; ok {
   392  					delete(m, paths[i])
   393  					return true
   394  				}
   395  			}
   396  			obj = m[paths[i]]
   397  		}
   398  	}
   399  	return false
   400  }