New Upstream Release - golang-github-jeffail-gabs

Ready changes

Summary

Merged new upstream version: 2.7.0 (was: 2.6.1).

Resulting package

Built on 2023-05-05T16:01 (took 11m2s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-jeffail-gabs-dev

Lintian Result

Diff

diff --git a/.errcheck.txt b/.errcheck.txt
new file mode 100644
index 0000000..7176915
--- /dev/null
+++ b/.errcheck.txt
@@ -0,0 +1,8 @@
+(*github.com/Jeffail/gabs/v2.Container).Array
+(*github.com/Jeffail/gabs/v2.Container).ArrayAppend
+(*github.com/Jeffail/gabs/v2.Container).ArrayAppend
+(*github.com/Jeffail/gabs/v2.Container).ArrayConcat
+(*github.com/Jeffail/gabs/v2.Container).ArrayConcatP
+(*github.com/Jeffail/gabs/v2.Container).ArrayP
+(*github.com/Jeffail/gabs/v2.Container).Set
+(*github.com/Jeffail/gabs/v2.Container).SetIndex
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..f1f4207
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
+name: Test
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.16.x
+
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Tidy
+        run: go mod tidy && git diff-index --quiet HEAD || { >&2 echo "Stale go.{mod,sum} detected. This can be fixed with 'go mod tidy'."; exit 1; }
+
+      - name: Test
+        run: go test -count 100 ./...
+
+  golangci-lint:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: latest
+          args: --timeout 10m
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..4dd3bc7
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,41 @@
+run:
+  timeout: 30s
+
+issues:
+  max-issues-per-linter: 0
+  max-same-issues: 0
+
+linters-settings:
+  errcheck:
+    exclude: .errcheck.txt
+  gocritic:
+    enabled-tags:
+      - diagnostic
+      - experimental
+      - opinionated
+      - performance
+      - style
+
+linters:
+  disable-all: true
+  enable:
+    # Default linters reported by golangci-lint help linters` in v1.39.0
+    - gosimple
+    - staticcheck
+    - unused
+    - errcheck
+    - govet
+    - ineffassign
+    - typecheck
+    # Extra linters:
+    - wastedassign
+    - stylecheck
+    - gofmt
+    - goimports
+    - gocritic
+    - revive
+    - unconvert
+    - durationcheck
+    - depguard
+    - gosec
+    - bodyclose
diff --git a/README.md b/README.md
index 6163e7a..64cf19a 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ import (
 
 ```go
 jsonParsed, err := gabs.ParseJSON([]byte(`{
-	"outter":{
+	"outer":{
 		"inner":{
 			"value1":10,
 			"value2":22
@@ -50,16 +50,16 @@ if err != nil {
 var value float64
 var ok bool
 
-value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64)
+value, ok = jsonParsed.Path("outer.inner.value1").Data().(float64)
 // value == 10.0, ok == true
 
-value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64)
+value, ok = jsonParsed.Search("outer", "inner", "value1").Data().(float64)
 // value == 10.0, ok == true
 
-value, ok = jsonParsed.Search("outter", "alsoInner", "array1", "1").Data().(float64)
+value, ok = jsonParsed.Search("outer", "alsoInner", "array1", "1").Data().(float64)
 // value == 40.0, ok == true
 
-gObj, err := jsonParsed.JSONPointer("/outter/alsoInner/array1/1")
+gObj, err := jsonParsed.JSONPointer("/outer/alsoInner/array1/1")
 if err != nil {
 	panic(err)
 }
@@ -69,7 +69,7 @@ value, ok = gObj.Data().(float64)
 value, ok = jsonParsed.Path("does.not.exist").Data().(float64)
 // value == 0.0, ok == false
 
-exists := jsonParsed.Exists("outter", "inner", "value1")
+exists := jsonParsed.Exists("outer", "inner", "value1")
 // exists == true
 
 exists = jsonParsed.ExistsP("does.not.exist")
@@ -86,7 +86,7 @@ if err != nil {
 
 // S is shorthand for Search
 for key, child := range jsonParsed.S("object").ChildrenMap() {
-	fmt.Printf("key: %v, value: %v\n", key, child.Data().(string))
+	fmt.Printf("key: %v, value: %v\n", key, child.Data().(float64))
 }
 ```
 
@@ -133,9 +133,9 @@ Will print `2`.
 jsonObj := gabs.New()
 // or gabs.Wrap(jsonObject) to work on an existing map[string]interface{}
 
-jsonObj.Set(10, "outter", "inner", "value")
-jsonObj.SetP(20, "outter.inner.value2")
-jsonObj.Set(30, "outter", "inner2", "value3")
+jsonObj.Set(10, "outer", "inner", "value")
+jsonObj.SetP(20, "outer.inner.value2")
+jsonObj.Set(30, "outer", "inner2", "value3")
 
 fmt.Println(jsonObj.String())
 ```
@@ -143,7 +143,7 @@ fmt.Println(jsonObj.String())
 Will print:
 
 ```
-{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}
+{"outer":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}
 ```
 
 To pretty-print:
@@ -156,7 +156,7 @@ Will print:
 
 ```
 {
-  "outter": {
+  "outer": {
     "inner": {
       "value": 10,
       "value2": 20
@@ -222,33 +222,33 @@ This is the easiest part:
 
 ```go
 jsonParsedObj, _ := gabs.ParseJSON([]byte(`{
-	"outter":{
+	"outer":{
 		"values":{
 			"first":10,
 			"second":11
 		}
 	},
-	"outter2":"hello world"
+	"outer2":"hello world"
 }`))
 
 jsonOutput := jsonParsedObj.String()
-// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}`
+// Becomes `{"outer":{"values":{"first":10,"second":11}},"outer2":"hello world"}`
 ```
 
 And to serialize a specific segment is as simple as:
 
 ```go
 jsonParsedObj := gabs.ParseJSON([]byte(`{
-	"outter":{
+	"outer":{
 		"values":{
 			"first":10,
 			"second":11
 		}
 	},
-	"outter2":"hello world"
+	"outer2":"hello world"
 }`))
 
-jsonOutput := jsonParsedObj.Search("outter").String()
+jsonOutput := jsonParsedObj.Search("outer").String()
 // Becomes `{"values":{"first":10,"second":11}}`
 ```
 
@@ -257,11 +257,11 @@ jsonOutput := jsonParsedObj.Search("outter").String()
 You can merge a JSON structure into an existing one, where collisions will be converted into a JSON array.
 
 ```go
-jsonParsed1, _ := ParseJSON([]byte(`{"outter":{"value1":"one"}}`))
-jsonParsed2, _ := ParseJSON([]byte(`{"outter":{"inner":{"value3":"three"}},"outter2":{"value2":"two"}}`))
+jsonParsed1, _ := ParseJSON([]byte(`{"outer":{"value1":"one"}}`))
+jsonParsed2, _ := ParseJSON([]byte(`{"outer":{"inner":{"value3":"three"}},"outer2":{"value2":"two"}}`))
 
 jsonParsed1.Merge(jsonParsed2)
-// Becomes `{"outter":{"inner":{"value3":"three"},"value1":"one"},"outter2":{"value2":"two"}}`
+// Becomes `{"outer":{"inner":{"value3":"three"},"value1":"one"},"outer2":{"value2":"two"}}`
 ```
 
 Arrays are merged:
diff --git a/debian/changelog b/debian/changelog
index 6951843..f9278e0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,11 +1,16 @@
-golang-github-jeffail-gabs (2.5.1-1) UNRELEASED; urgency=medium
+golang-github-jeffail-gabs (2.7.0-1) UNRELEASED; urgency=medium
 
+  [ Dawid Dziurla ]
   * d/control: bump standards
   * d/copyright: delete Files-Excluded
   * New upstream version 2.5.1
   * d/control: add newline at the end
 
- -- Dawid Dziurla <dawidd0811@gmail.com>  Sun, 24 May 2020 15:00:02 +0200
+  [ Debian Janitor ]
+  * New upstream release.
+  * New upstream release.
+
+ -- Dawid Dziurla <dawidd0811@gmail.com>  Fri, 05 May 2023 15:51:02 -0000
 
 golang-github-jeffail-gabs (2.3.0-1) unstable; urgency=medium
 
diff --git a/gabs.go b/gabs.go
index 9249f3d..b846954 100644
--- a/gabs.go
+++ b/gabs.go
@@ -28,7 +28,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
+	"os"
 	"strconv"
 	"strings"
 )
@@ -76,6 +76,16 @@ var (
 	ErrInvalidBuffer = errors.New("input buffer contained invalid JSON")
 )
 
+var (
+	r1 *strings.Replacer
+	r2 *strings.Replacer
+)
+
+func init() {
+	r1 = strings.NewReplacer("~1", "/", "~0", "~")
+	r2 = strings.NewReplacer("~1", ".", "~0", "~")
+}
+
 //------------------------------------------------------------------------------
 
 // JSONPointerToSlice parses a JSON pointer path
@@ -86,32 +96,31 @@ var (
 // gabs paths, '~' needs to be encoded as '~0' and '/' needs to be encoded as
 // '~1' when these characters appear in a reference key.
 func JSONPointerToSlice(path string) ([]string, error) {
-	if len(path) < 1 {
-		return nil, errors.New("failed to resolve JSON pointer: path must not be empty")
+	if path == "" {
+		return nil, nil
 	}
 	if path[0] != '/' {
 		return nil, errors.New("failed to resolve JSON pointer: path must begin with '/'")
 	}
+	if path == "/" {
+		return []string{""}, nil
+	}
 	hierarchy := strings.Split(path, "/")[1:]
 	for i, v := range hierarchy {
-		v = strings.Replace(v, "~1", "/", -1)
-		v = strings.Replace(v, "~0", "~", -1)
-		hierarchy[i] = v
+		hierarchy[i] = r1.Replace(v)
 	}
 	return hierarchy, nil
 }
 
 // DotPathToSlice returns a slice of path segments parsed out of a dot path.
 //
-// Because the characters '~' (%x7E) and '.' (%x2E) have special meanings in
-// gabs paths, '~' needs to be encoded as '~0' and '.' needs to be encoded as
-// '~1' when these characters appear in a reference key.
+// Because '.' (%x2E) is the segment separator, it must be encoded as '~1'
+// if it appears in the reference key. Likewise, '~' (%x7E) must be encoded
+// as '~0' since it is the escape character for encoding '.'.
 func DotPathToSlice(path string) []string {
 	hierarchy := strings.Split(path, ".")
 	for i, v := range hierarchy {
-		v = strings.Replace(v, "~1", ".", -1)
-		v = strings.Replace(v, "~0", "~", -1)
-		hierarchy[i] = v
+		hierarchy[i] = r2.Replace(v)
 	}
 	return hierarchy
 }
@@ -138,24 +147,30 @@ func (g *Container) searchStrict(allowWildcard bool, hierarchy ...string) (*Cont
 	object := g.Data()
 	for target := 0; target < len(hierarchy); target++ {
 		pathSeg := hierarchy[target]
-		if mmap, ok := object.(map[string]interface{}); ok {
-			object, ok = mmap[pathSeg]
-			if !ok {
+		switch typedObj := object.(type) {
+		case map[string]interface{}:
+			var ok bool
+			if object, ok = typedObj[pathSeg]; !ok {
 				return nil, fmt.Errorf("failed to resolve path segment '%v': key '%v' was not found", target, pathSeg)
 			}
-		} else if marray, ok := object.([]interface{}); ok {
+		case []interface{}:
 			if allowWildcard && pathSeg == "*" {
-				tmpArray := []interface{}{}
-				for _, val := range marray {
-					if (target + 1) >= len(hierarchy) {
-						tmpArray = append(tmpArray, val)
-					} else if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil {
-						tmpArray = append(tmpArray, res.Data())
+				var tmpArray []interface{}
+				if (target + 1) >= len(hierarchy) {
+					tmpArray = typedObj
+				} else {
+					tmpArray = make([]interface{}, 0, len(typedObj))
+					for _, val := range typedObj {
+						if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil {
+							tmpArray = append(tmpArray, res.Data())
+						}
 					}
 				}
+
 				if len(tmpArray) == 0 {
 					return nil, nil
 				}
+
 				return &Container{tmpArray}, nil
 			}
 			index, err := strconv.Atoi(pathSeg)
@@ -165,11 +180,11 @@ func (g *Container) searchStrict(allowWildcard bool, hierarchy ...string) (*Cont
 			if index < 0 {
 				return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' is invalid", target, pathSeg)
 			}
-			if len(marray) <= index {
-				return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(marray))
+			if len(typedObj) <= index {
+				return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(typedObj))
 			}
-			object = marray[index]
-		} else {
+			object = typedObj[index]
+		default:
 			return nil, fmt.Errorf("failed to resolve path segment '%v': field '%v' was not found", target, pathSeg)
 		}
 	}
@@ -252,7 +267,7 @@ func (g *Container) Children() []*Container {
 		return children
 	}
 	if mmap, ok := g.Data().(map[string]interface{}); ok {
-		children := []*Container{}
+		children := make([]*Container, 0, len(mmap))
 		for _, obj := range mmap {
 			children = append(children, &Container{obj})
 		}
@@ -300,15 +315,16 @@ func (g *Container) Set(value interface{}, hierarchy ...string) (*Container, err
 
 	for target := 0; target < len(hierarchy); target++ {
 		pathSeg := hierarchy[target]
-		if mmap, ok := object.(map[string]interface{}); ok {
+		switch typedObj := object.(type) {
+		case map[string]interface{}:
 			if target == len(hierarchy)-1 {
 				object = value
-				mmap[pathSeg] = object
-			} else if object = mmap[pathSeg]; object == nil {
-				mmap[pathSeg] = map[string]interface{}{}
-				object = mmap[pathSeg]
+				typedObj[pathSeg] = object
+			} else if object = typedObj[pathSeg]; object == nil {
+				typedObj[pathSeg] = map[string]interface{}{}
+				object = typedObj[pathSeg]
 			}
-		} else if marray, ok := object.([]interface{}); ok {
+		case []interface{}:
 			if pathSeg == "-" {
 				if target < 1 {
 					return nil, errors.New("unable to append new array index at root of path")
@@ -318,8 +334,8 @@ func (g *Container) Set(value interface{}, hierarchy ...string) (*Container, err
 				} else {
 					object = map[string]interface{}{}
 				}
-				marray = append(marray, object)
-				if _, err := g.Set(marray, hierarchy[:target]...); err != nil {
+				typedObj = append(typedObj, object)
+				if _, err := g.Set(typedObj, hierarchy[:target]...); err != nil {
 					return nil, err
 				}
 			} else {
@@ -330,17 +346,17 @@ func (g *Container) Set(value interface{}, hierarchy ...string) (*Container, err
 				if index < 0 {
 					return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' is invalid", target, pathSeg)
 				}
-				if len(marray) <= index {
-					return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(marray))
+				if len(typedObj) <= index {
+					return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(typedObj))
 				}
 				if target == len(hierarchy)-1 {
 					object = value
-					marray[index] = object
-				} else if object = marray[index]; object == nil {
+					typedObj[index] = object
+				} else if object = typedObj[index]; object == nil {
 					return nil, fmt.Errorf("failed to resolve path segment '%v': field '%v' was not found", target, pathSeg)
 				}
 			}
-		} else {
+		default:
 			return nil, ErrPathCollision
 		}
 	}
@@ -496,7 +512,9 @@ func (g *Container) MergeFn(source *Container, collisionFn func(destination, sou
 	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)
+			newPath := make([]string, len(path))
+			copy(newPath, path)
+			newPath = append(newPath, key)
 			if g.Exists(newPath...) {
 				existingData := g.Search(newPath...).Data()
 				switch t := value.(type) {
@@ -516,11 +534,9 @@ func (g *Container) MergeFn(source *Container, collisionFn func(destination, sou
 						return err
 					}
 				}
-			} else {
+			} else if _, err := g.Set(value, newPath...); err != nil {
 				// path doesn't exist. So set the value
-				if _, err := g.Set(value, newPath...); err != nil {
-					return err
-				}
+				return err
 			}
 		}
 		return nil
@@ -699,23 +715,29 @@ func (g *Container) ArrayCountP(path string) (int, error) {
 
 //------------------------------------------------------------------------------
 
-func walkObject(path string, obj map[string]interface{}, flat map[string]interface{}) {
+func walkObject(path string, obj, flat map[string]interface{}, includeEmpty bool) {
+	if includeEmpty && len(obj) == 0 {
+		flat[path] = struct{}{}
+	}
 	for elePath, v := range obj {
 		if len(path) > 0 {
 			elePath = path + "." + elePath
 		}
 		switch t := v.(type) {
 		case map[string]interface{}:
-			walkObject(elePath, t, flat)
+			walkObject(elePath, t, flat, includeEmpty)
 		case []interface{}:
-			walkArray(elePath, t, flat)
+			walkArray(elePath, t, flat, includeEmpty)
 		default:
 			flat[elePath] = t
 		}
 	}
 }
 
-func walkArray(path string, arr []interface{}, flat map[string]interface{}) {
+func walkArray(path string, arr []interface{}, flat map[string]interface{}, includeEmpty bool) {
+	if includeEmpty && len(arr) == 0 {
+		flat[path] = []struct{}{}
+	}
 	for i, ele := range arr {
 		elePath := strconv.Itoa(i)
 		if len(path) > 0 {
@@ -723,9 +745,9 @@ func walkArray(path string, arr []interface{}, flat map[string]interface{}) {
 		}
 		switch t := ele.(type) {
 		case map[string]interface{}:
-			walkObject(elePath, t, flat)
+			walkObject(elePath, t, flat, includeEmpty)
 		case []interface{}:
-			walkArray(elePath, t, flat)
+			walkArray(elePath, t, flat, includeEmpty)
 		default:
 			flat[elePath] = t
 		}
@@ -737,16 +759,34 @@ func walkArray(path string, arr []interface{}, flat map[string]interface{}) {
 // notation matching the spec for the method Path.
 //
 // E.g. the structure `{"foo":[{"bar":"1"},{"bar":"2"}]}` would flatten into the
-// object: `{"foo.0.bar":"1","foo.1.bar":"2"}`.
+// object: `{"foo.0.bar":"1","foo.1.bar":"2"}`. `{"foo": [{"bar":[]},{"bar":{}}]}`
+// would flatten into the object `{}`
 //
 // Returns an error if the target is not a JSON object or array.
 func (g *Container) Flatten() (map[string]interface{}, error) {
+	return g.flatten(false)
+}
+
+// FlattenIncludeEmpty a JSON array or object into an object of key/value pairs
+// for each field, just as Flatten, but includes empty arrays and objects, where
+// the key is the full path of the structured field in dot path notation matching
+// the spec for the method Path.
+//
+// E.g. the structure `{"foo": [{"bar":[]},{"bar":{}}]}` would flatten into the
+// object: `{"foo.0.bar":[],"foo.1.bar":{}}`.
+//
+// Returns an error if the target is not a JSON object or array.
+func (g *Container) FlattenIncludeEmpty() (map[string]interface{}, error) {
+	return g.flatten(true)
+}
+
+func (g *Container) flatten(includeEmpty bool) (map[string]interface{}, error) {
 	flattened := map[string]interface{}{}
 	switch t := g.Data().(type) {
 	case map[string]interface{}:
-		walkObject("", t, flattened)
+		walkObject("", t, flattened, includeEmpty)
 	case []interface{}:
-		walkArray("", t, flattened)
+		walkArray("", t, flattened, includeEmpty)
 	default:
 		return nil, ErrNotObjOrArray
 	}
@@ -757,18 +797,18 @@ func (g *Container) Flatten() (map[string]interface{}, error) {
 
 // Bytes marshals an element to a JSON []byte blob.
 func (g *Container) Bytes() []byte {
-	if bytes, err := json.Marshal(g.Data()); err == nil {
-		return bytes
+	if data, err := json.Marshal(g.Data()); err == nil {
+		return data
 	}
 	return []byte("null")
 }
 
 // BytesIndent marshals an element to a JSON []byte blob formatted with a prefix
 // and indent string.
-func (g *Container) BytesIndent(prefix string, indent string) []byte {
+func (g *Container) BytesIndent(prefix, indent string) []byte {
 	if g.object != nil {
-		if bytes, err := json.MarshalIndent(g.Data(), prefix, indent); err == nil {
-			return bytes
+		if data, err := json.MarshalIndent(g.Data(), prefix, indent); err == nil {
+			return data
 		}
 	}
 	return []byte("null")
@@ -781,7 +821,7 @@ func (g *Container) String() string {
 
 // StringIndent marshals an element to a JSON string formatted with a prefix and
 // indent string.
-func (g *Container) StringIndent(prefix string, indent string) string {
+func (g *Container) StringIndent(prefix, indent string) string {
 	return string(g.BytesIndent(prefix, indent))
 }
 
@@ -796,7 +836,7 @@ func EncodeOptHTMLEscape(doEscape bool) EncodeOpt {
 }
 
 // EncodeOptIndent sets the encoder to indent the JSON output.
-func EncodeOptIndent(prefix string, indent string) EncodeOpt {
+func EncodeOptIndent(prefix, indent string) EncodeOpt {
 	return func(e *json.Encoder) {
 		e.SetIndent(prefix, indent)
 	}
@@ -858,7 +898,7 @@ func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
 // ParseJSONFile reads a file and unmarshals the contents into a *Container.
 func ParseJSONFile(path string) (*Container, error) {
 	if len(path) > 0 {
-		cBytes, err := ioutil.ReadFile(path)
+		cBytes, err := os.ReadFile(path)
 		if err != nil {
 			return nil, err
 		}
diff --git a/gabs_test.go b/gabs_test.go
index 695522e..f8429e5 100644
--- a/gabs_test.go
+++ b/gabs_test.go
@@ -38,7 +38,7 @@ func TestBasic(t *testing.T) {
 		t.Errorf("Didn't find test2")
 	}
 
-	if result := val.Bytes(); string(result) != string(sample) {
+	if result := val.Bytes(); !bytes.Equal(result, sample) {
 		t.Errorf("Wrong []byte conversion: %s != %s", result, sample)
 	}
 }
@@ -124,12 +124,28 @@ func TestJSONPointer(t *testing.T) {
 		path  string
 		value string
 		err   string
+		input string
 	}
 	tests := []testCase{
 		{
 			path: "foo",
 			err:  "failed to resolve JSON pointer: path must begin with '/'",
 		},
+		{
+			path:  "",
+			value: `{"whole":{"document":"is this"}}`,
+			input: `{"whole":{"document":"is this"}}`,
+		},
+		{
+			path:  "/",
+			value: `{"":{"b":"value b"},"a":"value a"}`,
+			input: `{"":{"a":"value a","":{"b":"value b"}}}`,
+		},
+		{
+			path:  "//",
+			value: `{"b":"value b"}`,
+			input: `{"":{"a":"value a","":{"b":"value b"}}}`,
+		},
 		{
 			path: "/a/doesnotexist",
 			err:  "failed to resolve path segment '1': key 'doesnotexist' was not found",
@@ -158,10 +174,6 @@ func TestJSONPointer(t *testing.T) {
 			path:  "/what~0.a.~0pain",
 			value: `"ouch5"`,
 		},
-		{
-			path:  "/",
-			value: `{"can we access":"this?"}`,
-		},
 		{
 			path:  "//can we access",
 			value: `"this?"`,
@@ -192,13 +204,17 @@ func TestJSONPointer(t *testing.T) {
 		},
 	}
 
-	root, err := ParseJSON(bigSample)
-	if err != nil {
-		t.Fatalf("Failed to parse: %v", err)
-	}
-
 	for _, test := range tests {
 		t.Run(test.path, func(tt *testing.T) {
+			input := test.input
+			if input == "" {
+				input = string(bigSample)
+			}
+			root, err := ParseJSON([]byte(input))
+			if err != nil {
+				t.Fatalf("Failed to parse: %v", err)
+			}
+
 			var result *Container
 			result, err = root.JSONPointer(test.path)
 			if len(test.err) > 0 {
@@ -209,8 +225,7 @@ func TestJSONPointer(t *testing.T) {
 				}
 				return
 			} else if err != nil {
-				tt.Error(err)
-				return
+				tt.Fatal(err)
 			}
 			if exp, act := test.value, result.String(); exp != act {
 				tt.Errorf("Wrong result: %v != %v", act, exp)
@@ -692,6 +707,10 @@ func TestExamples(t *testing.T) {
 		}
 	}
 }`))
+	if err != nil {
+		t.Errorf("Error: %v", err)
+		return
+	}
 
 	var value float64
 	var ok bool
@@ -953,11 +972,11 @@ func TestModify(t *testing.T) {
 		t.Errorf("Didn't find test.value")
 	}
 
-	if out := val.String(); `{"test":{"value":45},"test2":20}` != out {
+	if out := val.String(); out != `{"test":{"value":45},"test2":20}` {
 		t.Errorf("Incorrectly serialized: %v", out)
 	}
 
-	if out := val.Search("test").String(); `{"value":45}` != out {
+	if out := val.Search("test").String(); out != `{"value":45}` {
 		t.Errorf("Incorrectly serialized: %v", out)
 	}
 }
@@ -1050,19 +1069,20 @@ func TestChildrenMap(t *testing.T) {
 	}
 
 	for key, val := range objectMap {
-		if "objectOne" == key {
+		switch key {
+		case "objectOne":
 			if val := val.S("num").Data().(float64); val != 1 {
 				t.Errorf("%v != %v", val, 1)
 			}
-		} else if "objectTwo" == key {
+		case "objectTwo":
 			if val := val.S("num").Data().(float64); val != 2 {
 				t.Errorf("%v != %v", val, 2)
 			}
-		} else if "objectThree" == key {
+		case "objectThree":
 			if val := val.S("num").Data().(float64); val != 3 {
 				t.Errorf("%v != %v", val, 3)
 			}
-		} else {
+		default:
 			t.Errorf("Unexpected key: %v", key)
 		}
 	}
@@ -1403,7 +1423,7 @@ func TestLargeSample(t *testing.T) {
 }
 
 func TestShorthand(t *testing.T) {
-	json, _ := ParseJSON([]byte(`{
+	container, _ := ParseJSON([]byte(`{
 		"outter":{
 			"inner":{
 				"value":5,
@@ -1418,24 +1438,24 @@ func TestShorthand(t *testing.T) {
 		}
 	}`))
 
-	missingValue := json.S("outter").S("doesntexist").S("alsodoesntexist").S("inner").S("value").Data()
+	missingValue := container.S("outter").S("doesntexist").S("alsodoesntexist").S("inner").S("value").Data()
 	if missingValue != nil {
 		t.Errorf("missing value was actually found: %v\n", missingValue)
 	}
 
-	realValue := json.S("outter").S("inner").S("value2").Data().(float64)
+	realValue := container.S("outter").S("inner").S("value2").Data().(float64)
 	if realValue != 10 {
 		t.Errorf("real value was incorrect: %v\n", realValue)
 	}
 
-	_, err := json.S("outter2").Set(json.S("outter").S("inner").Data(), "inner")
+	_, err := container.S("outter2").Set(container.S("outter").S("inner").Data(), "inner")
 	if err != nil {
 		t.Errorf("error setting outter2: %v\n", err)
 	}
 
 	compare := `{"outter":{"inner":{"value":5,"value2":10,"value3":11},"inner2":{}}` +
 		`,"outter2":{"inner":{"value":5,"value2":10,"value3":11}}}`
-	out := json.String()
+	out := container.String()
 	if out != compare {
 		t.Errorf("wrong serialized structure: %v\n", out)
 	}
@@ -1443,8 +1463,8 @@ func TestShorthand(t *testing.T) {
 	compare2 := `{"outter":{"inner":{"value":6,"value2":10,"value3":11},"inner2":{}}` +
 		`,"outter2":{"inner":{"value":6,"value2":10,"value3":11}}}`
 
-	json.S("outter").S("inner").Set(6, "value")
-	out = json.String()
+	container.S("outter").S("inner").Set(6, "value")
+	out = container.String()
 	if out != compare2 {
 		t.Errorf("wrong serialized structure: %v\n", out)
 	}
@@ -1475,14 +1495,14 @@ func TestInvalid(t *testing.T) {
 	}
 
 	invalidStr := validObj.S("Doesn't exist").String()
-	if "null" != invalidStr {
+	if invalidStr != "null" {
 		t.Errorf("expected 'null', received: %v", invalidStr)
 	}
 }
 
 func TestCreation(t *testing.T) {
-	json, _ := ParseJSON([]byte(`{}`))
-	inner, err := json.ObjectP("test.inner")
+	container, _ := ParseJSON([]byte(`{}`))
+	inner, err := container.ObjectP("test.inner")
 	if err != nil {
 		t.Errorf("Error: %v", err)
 		return
@@ -1498,7 +1518,7 @@ func TestCreation(t *testing.T) {
 
 	expected := `{"test":{"inner":{"array":["first element of the array",2,"three"],` +
 		`"first":10,"second":20}}}`
-	actual := json.String()
+	actual := container.String()
 	if actual != expected {
 		t.Errorf("received incorrect output from json object: %v\n", actual)
 	}
@@ -1558,9 +1578,13 @@ dynamic approach.
 */
 
 func BenchmarkStatic(b *testing.B) {
+	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		var jsonObj jsonStructure
-		json.Unmarshal(jsonContent, &jsonObj)
+		if err := json.Unmarshal(jsonContent, &jsonObj); err != nil {
+			b.Errorf("Error: %v", err)
+			return
+		}
 
 		if val := jsonObj.FirstOutter.SecondInner.NumberType; val != 12 {
 			b.Errorf("Wrong value of FirstOutter.SecondInner.NumberType: %v\n", val)
@@ -1580,6 +1604,7 @@ func BenchmarkStatic(b *testing.B) {
 }
 
 func BenchmarkDynamic(b *testing.B) {
+	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		jsonObj, err := ParseJSON(jsonContent)
 		if err != nil {
@@ -1612,10 +1637,10 @@ func TestBadIndexes(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
-	if act := jsonObj.Index(0).Data(); nil != act {
+	if act := jsonObj.Index(0).Data(); act != nil {
 		t.Errorf("Unexpected value returned: %v != %v", nil, act)
 	}
-	if act := jsonObj.S("array").Index(4).Data(); nil != act {
+	if act := jsonObj.S("array").Index(4).Data(); act != nil {
 		t.Errorf("Unexpected value returned: %v != %v", nil, act)
 	}
 }
@@ -1808,6 +1833,18 @@ func TestFlatten(t *testing.T) {
 			input:  `[["1"],["2","3"]]`,
 			output: `{"0.0":"1","1.0":"2","1.1":"3"}`,
 		},
+		{
+			input:  `{"foo":{"bar":null}}`,
+			output: `{"foo.bar":null}`,
+		},
+		{
+			input:  `{"foo":{"bar":{}}}`,
+			output: `{}`,
+		},
+		{
+			input:  `{"foo":{"bar":[]}}`,
+			output: `{}`,
+		},
 	}
 
 	for _, test := range tests {
@@ -1825,3 +1862,91 @@ func TestFlatten(t *testing.T) {
 		}
 	}
 }
+
+func TestFlattenIncludeEmpty(t *testing.T) {
+	type testCase struct {
+		input  string
+		output string
+	}
+	tests := []testCase{
+		{
+			input:  `{"foo":{"bar":"baz"}}`,
+			output: `{"foo.bar":"baz"}`,
+		},
+		{
+			input:  `{"foo":[{"bar":"1"},{"bar":"2"}]}`,
+			output: `{"foo.0.bar":"1","foo.1.bar":"2"}`,
+		},
+		{
+			input:  `[{"bar":"1"},{"bar":"2"}]`,
+			output: `{"0.bar":"1","1.bar":"2"}`,
+		},
+		{
+			input:  `[["1"],["2","3"]]`,
+			output: `{"0.0":"1","1.0":"2","1.1":"3"}`,
+		},
+		{
+			input:  `{"foo":{"bar":null}}`,
+			output: `{"foo.bar":null}`,
+		},
+		{
+			input:  `{"foo":{"bar":{}}}`,
+			output: `{"foo.bar":{}}`,
+		},
+		{
+			input:  `{"foo":{"bar":[]}}`,
+			output: `{"foo.bar":[]}`,
+		},
+	}
+
+	for _, test := range tests {
+		gObj, err := ParseJSON([]byte(test.input))
+		if err != nil {
+			t.Fatalf("Failed to parse '%v': %v", test.input, err)
+		}
+		var res map[string]interface{}
+		if res, err = gObj.FlattenIncludeEmpty(); err != nil {
+			t.Error(err)
+			continue
+		}
+		if exp, act := test.output, Wrap(res).String(); exp != act {
+			t.Errorf("Wrong result: %v != %v", act, exp)
+		}
+	}
+}
+
+func BenchmarkChildren(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		jsonObj, err := ParseJSON(bigSample)
+		if err != nil {
+			b.Errorf("Error parsing json: %v\n", err)
+		}
+
+		_ = jsonObj.Children()
+
+		FOSI := jsonObj.S("firstOutter", "secondInner")
+		_ = FOSI.Children()
+		SOSI := jsonObj.S("secondOutter", "secondInner")
+		_ = SOSI.Children()
+		SOTI := jsonObj.S("secondOutter", "thirdInner")
+		_ = SOTI.Children()
+	}
+}
+
+func BenchmarkWildcardSearch(b *testing.B) {
+	sample := []byte(`{"test":[{"value":10},{"value":20}]}`)
+
+	val, err := ParseJSON(sample)
+	if err != nil {
+		b.Fatalf("Failed to parse: %v", err)
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		val.Search([]string{"test", "*"}...)
+		val.Search([]string{"test", "*", "value"}...)
+	}
+}
diff --git a/go.mod b/go.mod
index c3d7ca4..bd75790 100644
--- a/go.mod
+++ b/go.mod
@@ -1 +1,3 @@
 module github.com/Jeffail/gabs/v2
+
+go 1.16

Debdiff

File lists identical (after any substitutions)

No differences were encountered in the control files

More details

Full run details