diff --git a/ast/node.go b/ast/node.go index 87b3b6f72..5b96b4b70 100644 --- a/ast/node.go +++ b/ast/node.go @@ -619,8 +619,8 @@ func (self *Node) SetAnyByIndex(index int, val interface{}) (bool, error) { // UnsetByIndex REOMVE (soft) the node of given index. // -// Deprecated: this will change index of elements, which is a dangerous action. -// Use Unset() for object or Pop() for array instead +// WARN: this will change address of elements, which is a dangerous action. +// Use Unset() for object or Pop() for array instead. func (self *Node) UnsetByIndex(index int) (bool, error) { if err := self.checkRaw(); err != nil { return false, err @@ -696,14 +696,27 @@ func (self *Node) Pop() error { if err != nil { return err } - - s.Pop() - self.l-- + // remove first unset node + for i := s.Len()-1; i >= 0; i-- { + if s.At(i).Exists() { + if i == s.Len()-1 { + s.Pop() + } else { + // not the last one, just set it to unset + s.Set(i, Node{}) + } + self.l-- + break + } + } return nil } -// Move moves the child at src index to dst index. -func (self *Node) Move(src, dst int) error { +// Move moves the child at src index to dst index, +// meanwhile slides sliblings from src+1 to dst. +// +// WARN: this will change address of elements, which is a dangerous action. +func (self *Node) Move(dst, src int) error { if err := self.should(types.V_ARRAY, "an array"); err != nil { return err } @@ -713,6 +726,31 @@ func (self *Node) Move(src, dst int) error { return err } + // check if any unset node exists + if l := s.Len(); self.len() != l { + println("unset", dst, src) + di, si := dst, src + // find real pos of src and dst + for i := 0; i < l; i++ { + if s.At(i).Exists() { + di-- + si-- + } + if di == -1 { + dst = i + di-- + } + if si == -1 { + src = i + si-- + } + if di == -2 && si == -2 { + break + } + } + println("unset2", dst, src) + } + s.MoveOne(src, dst) return nil } @@ -1409,7 +1447,6 @@ func (self *Node) removePair(i int) { *last = Pair{} // NOTICE: should be consistent with linkedPair.Len() self.l-- - if self. } func (self *Node) toGenericArray() ([]interface{}, error) { diff --git a/ast/node_test.go b/ast/node_test.go index ba35fd3a0..f143ce18a 100644 --- a/ast/node_test.go +++ b/ast/node_test.go @@ -769,125 +769,124 @@ func TestIndex(t *testing.T) { } func TestUnset(t *testing.T) { - root, derr := NewParser(_TwitterJson).Parse() - if derr != 0 { - t.Fatalf("decode failed: %v", derr.Error()) - } - entities := root.GetByPath("statuses", 0, "entities") - if !entities.Exists() || entities.Check() != nil { - t.Fatal(entities.Check().Error()) - } - exist, err := entities.Unset("urls") - if !exist || err != nil { - t.Fatal() - } - e := entities.Get("urls") - if e.Exists() { - t.Fatal() - } - if entities.len() != 2 { - t.Fatal(entities.len()) - } - - es, err := entities.Interface() - require.NoError(t, err) - require.Equal(t, map[string]interface{}{ - "hashtags": []interface{}{ - map[string]interface{}{ - "text": "freebandnames", - "indices": []interface{}{ - float64(20), float64(34), - }, - }, - }, - "user_mentions": []interface{}{}, - },es) - - out, err := entities.MarshalJSON() - require.NoError(t, err) - println(string(out)) - buf := bytes.NewBuffer(nil) - require.NoError(t, json.Compact(buf, out)) - require.Equal(t, -`{"hashtags":[{"text":"freebandnames","indices":[20,34]}],"user_mentions":[]}`, buf.String()) - - entities.Set("urls", NewString("a")) - e = entities.Get("urls") - x, _ := e.String() - if !e.Exists() || x != "a" { - t.Fatal() - } - - out, err = entities.MarshalJSON() - require.NoError(t, err) - buf = bytes.NewBuffer(nil) - json.Compact(buf, out) - require.Equal(t, -`{"hashtags":[{"text":"freebandnames","indices":[20,34]}],"user_mentions":[],"urls":"a"}`, buf.String()) - - // reload entities - *entities = NewRaw(string(out)) - - hashtags := entities.Get("hashtags").Index(0) - hashtags.Set("text2", newRawNode(`{}`, types.V_OBJECT)) - exist, err = hashtags.Unset("indices") // NOTICE: Unset() won't change node.Len() here - if !exist || err != nil || hashtags.len() != 2 { - t.Fatal(hashtags.len()) - } - y, _ := hashtags.Get("text").String() - if y != "freebandnames" { - t.Fatal() - } - if hashtags.Get("text2").Type() != V_OBJECT { - t.Fatal() - } - - entities.Load() - exist, err = entities.UnsetByIndex(entities.len()-1) - if !exist || err != nil { - t.Fatal() - } - if entities.len() != 2 { - t.Fatal(entities.len()) - } - e = entities.Index(entities.len()) - if e.Exists() { - t.Fatal() - } - - - ums := entities.Get("user_mentions") - ums.Add(NewNull()) - ums.Add(NewBool(false)) - ums.Add(NewBool(true)) - if ums.len() != 3 { - t.Fatal() - } - exist, err = ums.UnsetByIndex(1) - if !exist || err != nil { - t.Fatal() - } - require.Equal(t, 2, ums.len()) - umses, err := ums.Interface() - require.NoError(t, err) - require.Equal(t, []interface{}{interface{}(nil), true}, umses) - - v1, _ := ums.Index(0).Interface() - v2, _ := ums.Index(1).Interface() // NOTICE: unseted index 1 still can be find here - v3, _ := ums.Index(2).Interface() - if v1 != nil { - t.Fatal() - } - if v2 != true { - t.Fatal() - } - if v3 != nil { - t.Fatal() - } - out, err = entities.MarshalJSON() - require.NoError(t, err) - require.Equal(t, -`{"hashtags":[{"text":"freebandnames","text2":{}}],"user_mentions":[null,true]}`, string(out)) + root, derr := NewParser(_TwitterJson).Parse() + if derr != 0 { + t.Fatalf("decode failed: %v", derr.Error()) + } + entities := root.GetByPath("statuses", 0, "entities") + if !entities.Exists() || entities.Check() != nil { + t.Fatal(entities.Check().Error()) + } + exist, err := entities.Unset("urls") + if !exist || err != nil { + t.Fatal() + } + e := entities.Get("urls") + if e.Exists() { + t.Fatal() + } + if entities.len() != 2 { + t.Fatal(entities.len()) + } + + es, err := entities.Interface() + require.NoError(t, err) + require.Equal(t, map[string]interface{}{ + "hashtags": []interface{}{ + map[string]interface{}{ + "text": "freebandnames", + "indices": []interface{}{ + float64(20), float64(34), + }, + }, + }, + "user_mentions": []interface{}{}, + }, es) + + out, err := entities.MarshalJSON() + require.NoError(t, err) + println(string(out)) + buf := bytes.NewBuffer(nil) + require.NoError(t, json.Compact(buf, out)) + require.Equal(t, + `{"hashtags":[{"text":"freebandnames","indices":[20,34]}],"user_mentions":[]}`, buf.String()) + + entities.Set("urls", NewString("a")) + e = entities.Get("urls") + x, _ := e.String() + if !e.Exists() || x != "a" { + t.Fatal() + } + + out, err = entities.MarshalJSON() + require.NoError(t, err) + buf = bytes.NewBuffer(nil) + json.Compact(buf, out) + require.Equal(t, + `{"hashtags":[{"text":"freebandnames","indices":[20,34]}],"user_mentions":[],"urls":"a"}`, buf.String()) + + // reload entities + *entities = NewRaw(string(out)) + + hashtags := entities.Get("hashtags").Index(0) + hashtags.Set("text2", newRawNode(`{}`, types.V_OBJECT)) + exist, err = hashtags.Unset("indices") // NOTICE: Unset() won't change node.Len() here + if !exist || err != nil || hashtags.len() != 2 { + t.Fatal(hashtags.len()) + } + y, _ := hashtags.Get("text").String() + if y != "freebandnames" { + t.Fatal() + } + if hashtags.Get("text2").Type() != V_OBJECT { + t.Fatal() + } + + entities.Load() + exist, err = entities.UnsetByIndex(entities.len() - 1) + if !exist || err != nil { + t.Fatal() + } + if entities.len() != 2 { + t.Fatal(entities.len()) + } + e = entities.Index(entities.len()) + if e.Exists() { + t.Fatal() + } + + ums := entities.Get("user_mentions") + ums.Add(NewNull()) + ums.Add(NewBool(false)) + ums.Add(NewBool(true)) + if ums.len() != 3 { + t.Fatal() + } + exist, err = ums.UnsetByIndex(1) + if !exist || err != nil { + t.Fatal() + } + require.Equal(t, 2, ums.len()) + umses, err := ums.Interface() + require.NoError(t, err) + require.Equal(t, []interface{}{interface{}(nil), true}, umses) + + v1, _ := ums.Index(0).Interface() + v2, _ := ums.Index(1).Interface() // NOTICE: unseted index 1 still can be find here + v3, _ := ums.Index(2).Interface() + if v1 != nil { + t.Fatal() + } + if v2 != true { + t.Fatal() + } + if v3 != nil { + t.Fatal() + } + out, err = entities.MarshalJSON() + require.NoError(t, err) + require.Equal(t, + `{"hashtags":[{"text":"freebandnames","text2":{}}],"user_mentions":[null,true]}`, string(out)) } @@ -1816,55 +1815,145 @@ func BenchmarkMapAdd(b *testing.B) { } func TestNode_Move(t *testing.T) { + var us = NewRaw(`["a","1","b","c"]`) + if ex, e := us.UnsetByIndex(1); !ex || e != nil { + t.Fail() + } + var us2 = NewRaw(`["a","b","c","1"]`) + if ex, e := us2.UnsetByIndex(3); !ex || e != nil { + t.Fail() + } tests := []struct { name string - in Node - src int - dst int - out Node + in Node + src int + dst int + out Node wantErr bool }{ { - name: "over index", - in: NewArray([]Node{}), - src: 0, - dst: 1, - out: NewArray([]Node{}), - wantErr: false, - }, - { - name: "equal index", - in: NewArray([]Node{NewBool(true)}), - src: 0, - dst: 0, - out: NewArray([]Node{NewBool(true)}), - wantErr: false, - }, - { - name: "forward", - in: NewArray([]Node{NewString("a"), NewString("b"), NewString("c")}), - src: 0, - dst: 2, - out: NewArray([]Node{NewString("b"), NewString("c"), NewString("a")}), - wantErr: false, - }, - { - name: "backward", - in: NewArray([]Node{NewString("a"), NewString("b"), NewString("c")}), - src: 2, - dst: 0, - out: NewArray([]Node{NewString("c"), NewString("a"), NewString("b")}), - wantErr: false, - }, + name: "over index", + in: NewArray([]Node{}), + src: 0, + dst: 1, + out: NewArray([]Node{}), + wantErr: false, + }, + { + name: "equal index", + in: NewArray([]Node{NewBool(true)}), + src: 0, + dst: 0, + out: NewArray([]Node{NewBool(true)}), + wantErr: false, + }, + { + name: "forward", + in: NewArray([]Node{NewString("a"), NewString("b"), NewString("c")}), + src: 0, + dst: 2, + out: NewArray([]Node{NewString("b"), NewString("c"), NewString("a")}), + wantErr: false, + }, + { + name: "backward", + in: NewArray([]Node{NewString("a"), NewString("b"), NewString("c")}), + src: 2, + dst: 0, + out: NewArray([]Node{NewString("c"), NewString("a"), NewString("b")}), + wantErr: false, + }, + { + name: "lazy", + in: NewRaw(`["a","b","c"]`), + src: 2, + dst: 0, + out: NewArray([]Node{NewString("c"), NewString("a"), NewString("b")}), + wantErr: false, + }, + { + name: "unset back", + in: us, + src: 2, + dst: 0, + out: NewArray([]Node{NewString("c"), NewString("a"), NewString("b")}), + wantErr: false, + }, + { + name: "unset forward", + in: us2, + src: 0, + dst: 2, + out: NewArray([]Node{NewString("b"), NewString("c"), NewString("a")}), + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.in.Move(tt.src, tt.dst) - require.NoError(t, err) - ej, _ := tt.out.MarshalJSON() - aj, _ := tt.in.MarshalJSON() - require.Equal(t, string(ej), string(aj)) + err := tt.in.Move(tt.dst, tt.src) + require.NoError(t, err) + ej, _ := tt.out.MarshalJSON() + aj, _ := tt.in.MarshalJSON() + require.Equal(t, string(ej), string(aj)) }) } } + +func TestNode_Pop(t *testing.T) { + var us = NewRaw(`[1,2,3]`) + if ex, e := us.UnsetByIndex(0); !ex || e != nil { + t.Fail() + } + var us2 = NewRaw(`[1,2,3]`) + if ex, e := us2.UnsetByIndex(2); !ex || e != nil { + t.Fail() + } + tests := []struct { + name string + in Node + out Node + wantErr bool + }{ + { + name: "empty", + in: NewArray([]Node{}), + out: NewArray([]Node{}), + wantErr: false, + }, + { + name: "one", + in: NewArray([]Node{NewString("a")}), + out: NewArray([]Node{}), + wantErr: false, + }, + { + name: "raw", + in: NewRaw(`[1]`), + out: NewArray([]Node{}), + wantErr: false, + }, + { + name: "unset head", + in: us, + out: NewRaw(`[2]`), + wantErr: false, + }, + { + name: "unset tail", + in: us2, + out: NewRaw(`[1]`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.in.Pop(); (err != nil) != tt.wantErr { + t.Errorf("Node.Pop() error = %v, wantErr %v", err, tt.wantErr) + } + ej, _ := tt.out.MarshalJSON() + aj, _ := tt.in.MarshalJSON() + require.Equal(t, string(ej), string(aj)) + }) + } +}