diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 193ad76..bcaa4ff 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 + name: coverage-go${{ matrix.go-version }}${{ matrix.flags }} path: | - profile.out + profile-go${{ matrix.go-version }}${{ matrix.flags }}.out if-no-files-found: error retention-days: 1 @@ -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 diff --git a/insane.go b/insane.go index afc7f71..ecaaf93 100644 --- a/insane.go +++ b/insane.go @@ -1290,6 +1290,65 @@ 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. +// 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 @@ -1806,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 } diff --git a/insane_test.go b/insane_test.go index 6a19a32..6936cd0 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,49 @@ 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) +} + +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") +}