Repository: yorkie-team/yorkie
Pull request #448 (2023-01-16 05:32:48)

Omit empty attrs in the response of Text.Marshal
<!--  Thanks for sending a pull request! -->

**What this PR does / why we need it**:

Omit empty attrs in the response of Text.Marshal

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Address https://github.com/yorkie-team/yorkie/issues/435#issuecomment-1371732400

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note

```

**Additional documentation**:

<!--
This section can be blank if this pull request does not require a release note.

Please use the following format for linking documentation:
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

**Checklist**:
- [x] Added relevant tests or not required
- [x] Didn't break anything



Patch:
diff --git a/pkg/document/crdt/text.go b/pkg/document/crdt/text.go
index 40140646..dac3f6e3 100644
--- a/pkg/document/crdt/text.go
+++ b/pkg/document/crdt/text.go
@@ -63,6 +63,10 @@ func (t *TextValue) String() string {
 
 // Marshal returns the JSON encoding of this text.
 func (t *TextValue) Marshal() string {
+	if len(t.attrs.Elements()) == 0 {
+		return fmt.Sprintf(`{"val":"%s"}`, EscapeString(t.value))
+	}
+
 	return fmt.Sprintf(
 		`{"attrs":%s,"val":"%s"}`,
 		t.attrs.Marshal(),


Test patch:
diff --git a/api/converter/converter_test.go b/api/converter/converter_test.go
index 7d824aee..e9711afb 100644
--- a/api/converter/converter_test.go
+++ b/api/converter/converter_test.go
@@ -45,21 +45,21 @@ func TestConverter(t *testing.T) {
 			return nil
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"A"}]}`, doc.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"A"}]}`, doc.Marshal())
 
 		err = doc.Update(func(root *json.Object) error {
 			root.SetNewText("k1").Edit(0, 0, "B")
 			return nil
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"B"}]}`, doc.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"B"}]}`, doc.Marshal())
 
 		bytes, err := converter.ObjectToBytes(doc.RootObject())
 		assert.NoError(t, err)
 
 		obj, err = converter.BytesToObject(bytes)
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"B"}]}`, obj.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"B"}]}`, obj.Marshal())
 	})
 
 	t.Run("snapshot test", func(t *testing.T) {
diff --git a/pkg/document/crdt/root_test.go b/pkg/document/crdt/root_test.go
index f207778b..f4d72f05 100644
--- a/pkg/document/crdt/root_test.go
+++ b/pkg/document/crdt/root_test.go
@@ -128,35 +128,35 @@ func TestRoot(t *testing.T) {
 	t.Run("garbage collection for rich text test", func(t *testing.T) {
 		root := helper.TestRoot()
 		ctx := helper.TextChangeContext(root)
-		richText := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
+		text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
 
-		fromPos, toPos := richText.CreateRange(0, 0)
-		richText.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
-		registerTextElementWithGarbage(fromPos, toPos, root, richText)
-		assert.Equal(t, `[{"attrs":{},"val":"Hello World"}]`, richText.Marshal())
+		fromPos, toPos := text.CreateRange(0, 0)
+		text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
+		registerTextElementWithGarbage(fromPos, toPos, root, text)
+		assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
 		assert.Equal(t, 0, root.GarbageLen())
 
-		fromPos, toPos = richText.CreateRange(6, 11)
-		richText.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
-		registerTextElementWithGarbage(fromPos, toPos, root, richText)
-		assert.Equal(t, `[{"attrs":{},"val":"Hello "},{"attrs":{},"val":"Yorkie"}]`, richText.Marshal())
+		fromPos, toPos = text.CreateRange(6, 11)
+		text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
+		registerTextElementWithGarbage(fromPos, toPos, root, text)
+		assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
 		assert.Equal(t, 1, root.GarbageLen())
 
-		fromPos, toPos = richText.CreateRange(0, 6)
-		richText.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
-		registerTextElementWithGarbage(fromPos, toPos, root, richText)
-		assert.Equal(t, `[{"attrs":{},"val":"Yorkie"}]`, richText.Marshal())
+		fromPos, toPos = text.CreateRange(0, 6)
+		text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
+		registerTextElementWithGarbage(fromPos, toPos, root, text)
+		assert.Equal(t, `[{"val":"Yorkie"}]`, text.Marshal())
 		assert.Equal(t, 2, root.GarbageLen())
 
 		// It contains code marked tombstone.
 		// After calling the garbage collector, the node will be removed.
-		nodeLen := len(richText.Nodes())
+		nodeLen := len(text.Nodes())
 		assert.Equal(t, 3, nodeLen)
 
 		assert.Equal(t, 2, root.GarbageCollect(time.MaxTicket))
 		assert.Equal(t, 0, root.GarbageLen())
 
-		nodeLen = len(richText.Nodes())
+		nodeLen = len(text.Nodes())
 		assert.Equal(t, 1, nodeLen)
 	})
 
diff --git a/pkg/document/crdt/text_test.go b/pkg/document/crdt/text_test.go
index 12753ff7..04fa873c 100644
--- a/pkg/document/crdt/text_test.go
+++ b/pkg/document/crdt/text_test.go
@@ -33,11 +33,11 @@ func TestText(t *testing.T) {
 
 		fromPos, toPos := text.CreateRange(0, 0)
 		text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
-		assert.Equal(t, `[{"attrs":{},"val":"Hello World"}]`, text.Marshal())
+		assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
 
 		fromPos, toPos = text.CreateRange(6, 11)
 		text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
-		assert.Equal(t, `[{"attrs":{},"val":"Hello "},{"attrs":{},"val":"Yorkie"}]`, text.Marshal())
+		assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
 	})
 
 	t.Run("UTF-16 code units test", func(t *testing.T) {
@@ -70,17 +70,17 @@ func TestText(t *testing.T) {
 
 		fromPos, toPos := text.CreateRange(0, 0)
 		text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
-		assert.Equal(t, `[{"attrs":{},"val":"Hello World"}]`, text.Marshal())
+		assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
 
 		fromPos, toPos = text.CreateRange(6, 11)
 		text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
-		assert.Equal(t, `[{"attrs":{},"val":"Hello "},{"attrs":{},"val":"Yorkie"}]`, text.Marshal())
+		assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
 
 		fromPos, toPos = text.CreateRange(0, 1)
 		text.Style(fromPos, toPos, map[string]string{"b": "1"}, ctx.IssueTimeTicket())
 		assert.Equal(
 			t,
-			`[{"attrs":{"b":"1"},"val":"H"},{"attrs":{},"val":"ello "},{"attrs":{},"val":"Yorkie"}]`,
+			`[{"attrs":{"b":"1"},"val":"H"},{"val":"ello "},{"val":"Yorkie"}]`,
 			text.Marshal(),
 		)
 	})
diff --git a/pkg/document/document_test.go b/pkg/document/document_test.go
index 2d47c491..815a1670 100644
--- a/pkg/document/document_test.go
+++ b/pkg/document/document_test.go
@@ -206,11 +206,11 @@ func TestDocument(t *testing.T) {
 			root.SetNewText("k1").
 				Edit(0, 0, "ABCD").
 				Edit(1, 3, "12")
-			assert.Equal(t, `{"k1":[{"attrs":{},"val":"A"},{"attrs":{},"val":"12"},{"attrs":{},"val":"D"}]}`, root.Marshal())
+			assert.Equal(t, `{"k1":[{"val":"A"},{"val":"12"},{"val":"D"}]}`, root.Marshal())
 			return nil
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"A"},{"attrs":{},"val":"12"},{"attrs":{},"val":"D"}]}`, doc.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"A"},{"val":"12"},{"val":"D"}]}`, doc.Marshal())
 
 		err = doc.Update(func(root *json.Object) error {
 			text := root.GetText("k1")
@@ -249,11 +249,11 @@ func TestDocument(t *testing.T) {
 				Edit(0, 1, "하").
 				Edit(1, 1, "느").
 				Edit(1, 2, "늘")
-			assert.Equal(t, `{"k1":[{"attrs":{},"val":"하"},{"attrs":{},"val":"늘"}]}`, root.Marshal())
+			assert.Equal(t, `{"k1":[{"val":"하"},{"val":"늘"}]}`, root.Marshal())
 			return nil
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"하"},{"attrs":{},"val":"늘"}]}`, doc.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"하"},{"val":"늘"}]}`, doc.Marshal())
 	})
 
 	t.Run("rich text test", func(t *testing.T) {
@@ -270,7 +270,7 @@ func TestDocument(t *testing.T) {
 			return nil
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"Hello world"}]}`, doc.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"Hello world"}]}`, doc.Marshal())
 
 		err = doc.Update(func(root *json.Object) error {
 			text := root.GetText("k1")
@@ -284,7 +284,7 @@ func TestDocument(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(
 			t,
-			`{"k1":[{"attrs":{"b":"1"},"val":"Hello"},{"attrs":{},"val":" world"}]}`,
+			`{"k1":[{"attrs":{"b":"1"},"val":"Hello"},{"val":" world"}]}`,
 			doc.Marshal(),
 		)
 
@@ -308,7 +308,7 @@ func TestDocument(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(
 			t,
-			`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{},"val":" world"}]}`,
+			`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"val":" world"}]}`,
 			doc.Marshal(),
 		)
 
@@ -326,7 +326,7 @@ func TestDocument(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(
 			t,
-			`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{},"val":" Yorkie"}]}`,
+			`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"val":" Yorkie"}]}`,
 			doc.Marshal(),
 		)
 
@@ -345,7 +345,7 @@ func TestDocument(t *testing.T) {
 		assert.Equal(
 			t,
 			`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},`+
-				`{"attrs":{"list":"true"},"val":"\\n"},{"attrs":{},"val":" Yorkie"}]}`,
+				`{"attrs":{"list":"true"},"val":"\\n"},{"val":" Yorkie"}]}`,
 			doc.Marshal(),
 		)
 	})
diff --git a/test/bench/document_bench_test.go b/test/bench/document_bench_test.go
index 5b422be0..20dae0af 100644
--- a/test/bench/document_bench_test.go
+++ b/test/bench/document_bench_test.go
@@ -184,11 +184,11 @@ func BenchmarkDocument(b *testing.B) {
 				root.SetNewText("k1").
 					Edit(0, 0, "ABCD").
 					Edit(1, 3, "12")
-				assert.Equal(b, `{"k1":[{"attrs":{},"val":"A"},{"attrs":{},"val":"12"},{"attrs":{},"val":"D"}]}`, root.Marshal())
+				assert.Equal(b, `{"k1":[{"val":"A"},{"val":"12"},{"val":"D"}]}`, root.Marshal())
 				return nil
 			})
 			assert.NoError(b, err)
-			assert.Equal(b, `{"k1":[{"attrs":{},"val":"A"},{"attrs":{},"val":"12"},{"attrs":{},"val":"D"}]}`, doc.Marshal())
+			assert.Equal(b, `{"k1":[{"val":"A"},{"val":"12"},{"val":"D"}]}`, doc.Marshal())
 
 			err = doc.Update(func(root *json.Object) error {
 				text := root.GetText("k1")
@@ -229,11 +229,11 @@ func BenchmarkDocument(b *testing.B) {
 					Edit(0, 1, "하").
 					Edit(1, 1, "느").
 					Edit(1, 2, "늘")
-				assert.Equal(b, `{"k1":[{"attrs":{},"val":"하"},{"attrs":{},"val":"늘"}]}`, root.Marshal())
+				assert.Equal(b, `{"k1":[{"val":"하"},{"val":"늘"}]}`, root.Marshal())
 				return nil
 			})
 			assert.NoError(b, err)
-			assert.Equal(b, `{"k1":[{"attrs":{},"val":"하"},{"attrs":{},"val":"늘"}]}`, doc.Marshal())
+			assert.Equal(b, `{"k1":[{"val":"하"},{"val":"늘"}]}`, doc.Marshal())
 		}
 	})
 
@@ -252,7 +252,7 @@ func BenchmarkDocument(b *testing.B) {
 				return nil
 			})
 			assert.NoError(b, err)
-			assert.Equal(b, `{"k1":[{"attrs":{},"val":"Hello world"}]}`, doc.Marshal())
+			assert.Equal(b, `{"k1":[{"val":"Hello world"}]}`, doc.Marshal())
 
 			err = doc.Update(func(root *json.Object) error {
 				text := root.GetText("k1")
@@ -266,7 +266,7 @@ func BenchmarkDocument(b *testing.B) {
 			assert.NoError(b, err)
 			assert.Equal(
 				b,
-				`{"k1":[{"attrs":{"b":"1"},"val":"Hello"},{"attrs":{},"val":" world"}]}`,
+				`{"k1":[{"attrs":{"b":"1"},"val":"Hello"},{"val":" world"}]}`,
 				doc.Marshal(),
 			)
 
@@ -290,7 +290,7 @@ func BenchmarkDocument(b *testing.B) {
 			assert.NoError(b, err)
 			assert.Equal(
 				b,
-				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{},"val":" world"}]}`,
+				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"val":" world"}]}`,
 				doc.Marshal(),
 			)
 
@@ -308,7 +308,7 @@ func BenchmarkDocument(b *testing.B) {
 			assert.NoError(b, err)
 			assert.Equal(
 				b,
-				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{},"val":" Yorkie"}]}`,
+				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"val":" Yorkie"}]}`,
 				doc.Marshal(),
 			)
 
@@ -325,7 +325,7 @@ func BenchmarkDocument(b *testing.B) {
 			assert.NoError(b, err)
 			assert.Equal(
 				b,
-				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{"list":"true"},"val":"\n"},{"attrs":{},"val":" Yorkie"}]}`,
+				`{"k1":[{"attrs":{"b":"1"},"val":"Hel"},{"attrs":{"b":"1","i":"1"},"val":"lo"},{"attrs":{"list":"true"},"val":"\n"},{"val":" Yorkie"}]}`,
 				doc.Marshal(),
 			)
 		}
@@ -462,14 +462,6 @@ func BenchmarkDocument(b *testing.B) {
 		benchmarkCounter(10000, b)
 	})
 
-	b.Run("rich text 100", func(b *testing.B) {
-		benchmarkRichText(100, b)
-	})
-
-	b.Run("rich text 1000", func(b *testing.B) {
-		benchmarkRichText(1000, b)
-	})
-
 	b.Run("object 1000", func(b *testing.B) {
 		benchmarkObject(1000, b)
 	})
@@ -634,21 +626,6 @@ func benchmarkCounter(cnt int, b *testing.B) {
 	}
 }
 
-func benchmarkRichText(cnt int, b *testing.B) {
-	for i := 0; i < b.N; i++ {
-		doc := document.New("d1")
-
-		err := doc.Update(func(root *json.Object) error {
-			text := root.SetNewText("k1")
-			for c := 0; c < cnt; c++ {
-				text.Edit(0, 0, "a", nil)
-			}
-			return nil
-		})
-		assert.NoError(b, err)
-	}
-}
-
 func benchmarkObject(cnt int, b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		doc := document.New("d1")
diff --git a/test/integration/snapshot_test.go b/test/integration/snapshot_test.go
index 54fee134..d9986819 100644
--- a/test/integration/snapshot_test.go
+++ b/test/integration/snapshot_test.go
@@ -130,7 +130,7 @@ func TestSnapshot(t *testing.T) {
 		err = c2.Attach(ctx, d2)
 		assert.NoError(t, err)
 
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"하"},{"attrs":{},"val":"늘"},{"attrs":{},"val":"구"},{"attrs":{},"val":"름"}]}`, d1.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"하"},{"val":"늘"},{"val":"구"},{"val":"름"}]}`, d1.Marshal())
 		assert.Equal(t, d1.Marshal(), d2.Marshal())
 	})
 
diff --git a/test/integration/text_test.go b/test/integration/text_test.go
index 52584967..e6cf04d7 100644
--- a/test/integration/text_test.go
+++ b/test/integration/text_test.go
@@ -160,21 +160,21 @@ func TestText(t *testing.T) {
 		assert.NoError(t, err)
 
 		syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}})
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"123"},{"attrs":{},"val":"456"},{"attrs":{},"val":"789"}]}`, d2.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"123"},{"val":"456"},{"val":"789"}]}`, d2.Marshal())
 
 		err = d1.Update(func(root *json.Object) error {
 			root.GetText("k1").Edit(1, 7, "")
 			return nil
 		}, "delete block by c1")
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"1"},{"attrs":{},"val":"89"}]}`, d1.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"1"},{"val":"89"}]}`, d1.Marshal())
 
 		err = d2.Update(func(root *json.Object) error {
 			root.GetText("k1").Edit(2, 5, "")
 			return nil
 		}, "delete block by c2")
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"12"},{"attrs":{},"val":"6"},{"attrs":{},"val":"789"}]}`, d2.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"12"},{"val":"6"},{"val":"789"}]}`, d2.Marshal())
 
 		syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}})
 	})
@@ -207,7 +207,7 @@ func TestText(t *testing.T) {
 		assert.NoError(t, err)
 
 		syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}})
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"0"},{"attrs":{},"val":"0"},{"attrs":{},"val":"0"}]}`, d2.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"0"},{"val":"0"},{"val":"0"}]}`, d2.Marshal())
 
 		err = d1.Update(func(root *json.Object) error {
 			root.GetText("k1").Edit(1, 2, "1")
@@ -216,7 +216,7 @@ func TestText(t *testing.T) {
 			return nil
 		}, "newly create then delete by c1")
 		assert.NoError(t, err)
-		assert.Equal(t, `{"k1":[{"attrs":{},"val":"0"},{"attrs":{},"val":"0"}]}`, d1.Marshal())
+		assert.Equal(t, `{"k1":[{"val":"0"},{"val":"0"}]}`, d1.Marshal())
 
 		err = d2.Update(func(root *json.Object) error {
 			root.GetText("k1").Edit(0, 3, "")