From addef721fe11a8737f1fe0cae9b2798b75c9e82e Mon Sep 17 00:00:00 2001 From: Oleg Don Date: Mon, 20 Oct 2025 16:37:42 +0500 Subject: [PATCH 1/5] Add copy node function --- insane.go | 38 ++++++++++++++++++++++++++++++++++++++ insane_test.go | 25 +++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/insane.go b/insane.go index afc7f71..9d26a08 100644 --- a/insane.go +++ b/insane.go @@ -1290,6 +1290,44 @@ func (n *Node) MutateToStrict() *StrictNode { return &StrictNode{n} } +// CopyFromNode copies all data from the src node to the current node. +// `root` must be a root of the current node to utilize the same node pool. +// If the `src` node is a node tree, the whole tree will be copied recursively. +// This function is designed to use for copying data from node of one JSON tree +// to another so they don't mutate each others data and utilize their own node +// pools so the garbage collector can clean released nodes correctly. +func (n *Node) CopyFromNode(root *Root, src *Node) *Node { + if n == nil || src == nil { + return nil + } + + n.bits = src.bits + n.data = strings.Clone(src.data) + n.next = n.getNode(root) + n.next.parent = n.parent + n.next.CopyFromNode(root, src.next) + + if len(src.nodes) > 0 { + n.nodes = make([]*Node, 0, len(src.nodes)) + for _, child := range src.nodes { + newChild := n.getNode(root) + newChild.parent = n + n.nodes = append(n.nodes, newChild.CopyFromNode(root, child)) + } + } + + if src.fields != nil { + srcFields := *src.fields + newFields := make(map[string]int, len(srcFields)) + for k, v := range srcFields { + newFields[k] = v + } + n.fields = &newFields + } + + return n +} + func (n *Node) DigField(path ...string) *Node { if n == nil || len(path) == 0 { return nil diff --git a/insane_test.go b/insane_test.go index 6a19a32..49734d0 100644 --- a/insane_test.go +++ b/insane_test.go @@ -204,7 +204,6 @@ func TestDecodeErr(t *testing.T) { {json: `falsenull`, err: ErrUnexpectedJSONEnding}, {json: `null:`, err: ErrUnexpectedJSONEnding}, - // ok {json: `0`, err: nil}, {json: `1.0`, err: nil}, @@ -374,7 +373,7 @@ func TestAddElement(t *testing.T) { for index := 0; index < test.count; index++ { root.AddElement() l := len(root.AsArray()) - assert.True(t, root.Dig(strconv.Itoa(l - 1)).IsNull(), "wrong node type") + assert.True(t, root.Dig(strconv.Itoa(l-1)).IsNull(), "wrong node type") } assert.Equal(t, test.result, root.EncodeToString(), "wrong encoding") Release(root) @@ -1054,3 +1053,25 @@ func TestIndex(t *testing.T) { assert.Equal(t, index, node.getIndex(), "wrong index") } + +func TestCopyFromNode(t *testing.T) { + json := `{"first":["s1","s2","s3"],"second":[{"s4":true},{"s5":false}]}` + root1, err := DecodeString(json) + assert.NoError(t, err, "error while decoding") + assert.NotNil(t, root1, "node shouldn't be nil") + + root2 := Spawn() + clonedRoot := root2.CopyFromNode(root2, root1.Node) + assert.NotNil(t, clonedRoot, "node shouldn't be nil") + + array1 := root1.Dig("first").AsArray() + array2 := root2.Dig("first").AsArray() + assert.Equal(t, len(array1), len(array2)) + for i := range array1 { + assert.Equal(t, array1[i].AsString(), array2[i].AsString()) + } + + origVal := strings.Clone(array1[0].data) + array2[0].data = "newData" + assert.NotEqual(t, array1[0].AsString(), array2[0].AsString(), "values must be different, first value must be %q, but it was changed to %q", origVal, array1[0].data) +} From b0827c2e62be60c5aa27f4a5bc2821cde6cbfb3b Mon Sep 17 00:00:00 2001 From: Oleg Don Date: Mon, 20 Oct 2025 16:45:34 +0500 Subject: [PATCH 2/5] Bump upload artifact action to v4 --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 193ad76..630228f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,14 +32,14 @@ jobs: - name: Test env: GOFLAGS: ${{ matrix.flags }} - run: go test -coverprofile=profile.out -covermode=atomic -v -coverpkg=./... ./... + run: go test -coverprofile=profile-go${{ matrix.go-version }}${{ matrix.flags }}.out -covermode=atomic -v -coverpkg=./... ./... - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage path: | - profile.out + profile-go${{ matrix.go-version }}${{ matrix.flags }}.out if-no-files-found: error retention-days: 1 From 22ac733527f57107bd336f0148a3b7cc27386a1b Mon Sep 17 00:00:00 2001 From: Oleg Don Date: Mon, 20 Oct 2025 16:59:30 +0500 Subject: [PATCH 3/5] Fix upload artifact action paths --- .github/workflows/ci.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 630228f..bcaa4ff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: coverage + name: coverage-go${{ matrix.go-version }}${{ matrix.flags }} path: | profile-go${{ matrix.go-version }}${{ matrix.flags }}.out if-no-files-found: error @@ -52,9 +52,10 @@ jobs: uses: actions/checkout@v4 - name: Download artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage + pattern: coverage-* + merge-multiple: true - name: Send coverage uses: codecov/codecov-action@v3 From bd6913edc51ea2b8e280edb9db97b045a5134e1b Mon Sep 17 00:00:00 2001 From: Oleg Don Date: Mon, 22 Dec 2025 15:06:47 +0500 Subject: [PATCH 4/5] Add (*Node).ConvertToRoot function --- insane.go | 21 +++++++++++++++++++++ insane_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/insane.go b/insane.go index 9d26a08..01bacd8 100644 --- a/insane.go +++ b/insane.go @@ -1290,6 +1290,27 @@ func (n *Node) MutateToStrict() *StrictNode { return &StrictNode{n} } +// ConvertToRoot **experimental** **not safe** function. It converts the node to `Root` and +// removes it from its parent tree. The root parameter must the root of the current node, +// so the converted root node utilizes the same decoder and nodePool. The node's parent is +// becoming `nil` so it behaves like normal root node (e.g. on `(*Node).Suicide`). +// +// This function can be used when it is needed to use JSON subtree as a whole tree with +// the root on the subtree top node. For example when treating array elements as separate +// JSON trees. +// +// This function is unsafe because it shares the same decoder as its parent tree while the +// decoder itself points to the parent root and that can lead to some unexpected behaviour. +func (n *Node) ConvertToRoot(root *Root) *Root { + // remove node from its parent tree + n.Suicide() + n.parent = nil + return &Root{ + n, + root.decoder, + } +} + // CopyFromNode copies all data from the src node to the current node. // `root` must be a root of the current node to utilize the same node pool. // If the `src` node is a node tree, the whole tree will be copied recursively. diff --git a/insane_test.go b/insane_test.go index 49734d0..6936cd0 100644 --- a/insane_test.go +++ b/insane_test.go @@ -1075,3 +1075,27 @@ func TestCopyFromNode(t *testing.T) { array2[0].data = "newData" assert.NotEqual(t, array1[0].AsString(), array2[0].AsString(), "values must be different, first value must be %q, but it was changed to %q", origVal, array1[0].data) } + +func TestConvertToRoot(t *testing.T) { + json := `[{"a":"1"},{"b":"2"},{"c":"3"}]` + root1, err := DecodeString(json) + assert.NoError(t, err, "error while decoding") + assert.NotNil(t, root1, "root shouldn't be nil") + assert.True(t, len(root1.AsArray()) == 3, "root must be an array with 3 elements") + + node2 := root1.Dig("2") + assert.NotNil(t, node2, "node shouldn't be nil") + assert.True(t, node2.IsObject(), "node must be an object") + + root1FieldVal := node2.Dig("c") + assert.NotNil(t, root1FieldVal, "node must not be nil") + + root2 := node2.ConvertToRoot(root1) + assert.NotNil(t, root2, "root shouldn't be nil") + // after ConvertToRoot the node is deleted from its parent tree + assert.Nil(t, root1.Dig("2"), "node must be nil") + + root2FieldVal := root2.Dig("c") + assert.NotNil(t, root2FieldVal, "node must not be nil") + assert.Equal(t, root1FieldVal.AsString(), root2FieldVal.AsString(), "values must be equal") +} From 89194e0cbc3a907097e061f574aa6d0071208e33 Mon Sep 17 00:00:00 2001 From: Oleg Don Date: Fri, 26 Dec 2025 13:56:15 +0500 Subject: [PATCH 5/5] Prealloc slice of Node in initPool and expandPool --- insane.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/insane.go b/insane.go index 01bacd8..ecaaf93 100644 --- a/insane.go +++ b/insane.go @@ -1865,17 +1865,25 @@ func (n *Node) getIndex() int { // ******************** // func (d *decoder) initPool() { - d.nodePool = make([]*Node, StartNodePoolSize, StartNodePoolSize) + s := make([]Node, StartNodePoolSize) + d.nodePool = make([]*Node, StartNodePoolSize) for i := 0; i < StartNodePoolSize; i++ { - d.nodePool[i] = &Node{} + d.nodePool[i] = &s[i] } } func (d *decoder) expandPool() []*Node { c := cap(d.nodePool) + newSlice := make([]*Node, 0, c+c) + newSlice = append(newSlice, d.nodePool...) + s := make([]Node, c) for i := 0; i < c; i++ { - d.nodePool = append(d.nodePool, &Node{}) + newSlice = append(newSlice, &s[i]) } + for i := range d.nodePool { + d.nodePool[i] = nil + } + d.nodePool = newSlice return d.nodePool }