Repository: yorkie-team/yorkie
Pull request #160 (2021-03-13 07:06:30)

Support for null values
<!--  Thanks for sending a pull request! -->

**What this PR does / why we need it**:

Support for null values.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes https://github.com/yorkie-team/yorkie-js-sdk/issues/156

**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


codecov[bot]: # [Codecov](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=h1) Report
> Merging [#160](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=desc) (afcd57f) into [main](https://codecov.io/gh/yorkie-team/yorkie/commit/b24df023ca422de2c925ad7a3e586fdbf5e5892f?el=desc) (b24df02) will **decrease** coverage by `0.29%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/yorkie-team/yorkie/pull/160/graphs/tree.svg?width=650&height=150&src=pr&token=P6COB1ZNIC)](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #160      +/-   ##
==========================================
- Coverage   57.48%   57.18%   -0.30%     
==========================================
  Files          37       37              
  Lines        3262     3279      +17     
==========================================
  Hits         1875     1875              
- Misses       1202     1218      +16     
- Partials      185      186       +1     
```


| [Impacted Files](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [api/converter/from\_pb.go](https://codecov.io/gh/yorkie-team/yorkie/pull/160/diff?src=pr&el=tree#diff-YXBpL2NvbnZlcnRlci9mcm9tX3BiLmdv) | `66.57% <0.00%> (-0.71%)` | :arrow_down: |
| [api/converter/to\_pb.go](https://codecov.io/gh/yorkie-team/yorkie/pull/160/diff?src=pr&el=tree#diff-YXBpL2NvbnZlcnRlci90b19wYi5nbw==) | `84.83% <0.00%> (-0.71%)` | :arrow_down: |
| [pkg/document/json/primitive.go](https://codecov.io/gh/yorkie-team/yorkie/pull/160/diff?src=pr&el=tree#diff-cGtnL2RvY3VtZW50L2pzb24vcHJpbWl0aXZlLmdv) | `25.78% <0.00%> (-2.43%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=footer). Last update [b24df02...afcd57f](https://codecov.io/gh/yorkie-team/yorkie/pull/160?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/api/converter/from_pb.go b/api/converter/from_pb.go
index 8b02de6e..2a283073 100644
--- a/api/converter/from_pb.go
+++ b/api/converter/from_pb.go
@@ -469,6 +469,8 @@ func fromElement(pbElement *api.JSONElementSimple) (json.Element, error) {
 			json.NewRGATreeList(),
 			createdAt,
 		), nil
+	case api.ValueType_NULL:
+		fallthrough
 	case api.ValueType_BOOLEAN:
 		fallthrough
 	case api.ValueType_INTEGER:
@@ -536,6 +538,8 @@ func fromElement(pbElement *api.JSONElementSimple) (json.Element, error) {
 
 func fromPrimitiveValueType(valueType api.ValueType) (json.ValueType, error) {
 	switch valueType {
+	case api.ValueType_NULL:
+		return json.Null, nil
 	case api.ValueType_BOOLEAN:
 		return json.Boolean, nil
 	case api.ValueType_INTEGER:
diff --git a/api/converter/to_pb.go b/api/converter/to_pb.go
index 6b72bb22..1d0e25a1 100644
--- a/api/converter/to_pb.go
+++ b/api/converter/to_pb.go
@@ -373,6 +373,8 @@ func toTimeTicket(ticket *time.Ticket) *api.TimeTicket {
 
 func toValueType(valueType json.ValueType) (api.ValueType, error) {
 	switch valueType {
+	case json.Null:
+		return api.ValueType_NULL, nil
 	case json.Boolean:
 		return api.ValueType_BOOLEAN, nil
 	case json.Integer:
diff --git a/pkg/document/json/primitive.go b/pkg/document/json/primitive.go
index 55abe7d5..b9c55656 100644
--- a/pkg/document/json/primitive.go
+++ b/pkg/document/json/primitive.go
@@ -43,6 +43,8 @@ const (
 // ValueFromBytes parses the given bytes into value.
 func ValueFromBytes(valueType ValueType, value []byte) interface{} {
 	switch valueType {
+	case Null:
+		return nil
 	case Boolean:
 		if value[0] == 1 {
 			return true
@@ -78,6 +80,14 @@ type Primitive struct {
 
 // NewPrimitive creates a new instance of Primitive.
 func NewPrimitive(value interface{}, createdAt *time.Ticket) *Primitive {
+	if value == nil {
+		return &Primitive{
+			valueType: Null,
+			value:     nil,
+			createdAt: createdAt,
+		}
+	}
+
 	switch val := value.(type) {
 	case bool:
 		return &Primitive{
@@ -135,6 +145,10 @@ func NewPrimitive(value interface{}, createdAt *time.Ticket) *Primitive {
 
 // Bytes creates an array representing the value.
 func (p *Primitive) Bytes() []byte {
+	if p.valueType == Null {
+		return nil
+	}
+
 	switch val := p.value.(type) {
 	case bool:
 		if val {
@@ -169,6 +183,8 @@ func (p *Primitive) Bytes() []byte {
 // Marshal returns the JSON encoding of the value.
 func (p *Primitive) Marshal() string {
 	switch p.valueType {
+	case Null:
+		return "null"
 	case Boolean:
 		return fmt.Sprintf("%t", p.value)
 	case Integer:
@@ -225,6 +241,11 @@ func (p *Primitive) Remove(removedAt *time.Ticket) bool {
 	return false
 }
 
+// Value returns the value of Primitive.
+func (p *Primitive) Value() interface{} {
+	return p.value
+}
+
 // ValueType returns the type of the value.
 func (p *Primitive) ValueType() ValueType {
 	return p.valueType
diff --git a/pkg/document/proxy/array_proxy.go b/pkg/document/proxy/array_proxy.go
index dbc4dc70..13395d88 100644
--- a/pkg/document/proxy/array_proxy.go
+++ b/pkg/document/proxy/array_proxy.go
@@ -40,6 +40,15 @@ func NewArrayProxy(ctx *change.Context, array *json.Array) *ArrayProxy {
 	}
 }
 
+// AddNull adds the null at the last.
+func (p *ArrayProxy) AddNull() *ArrayProxy {
+	p.addInternal(func(ticket *time.Ticket) json.Element {
+		return json.NewPrimitive(nil, ticket)
+	})
+
+	return p
+}
+
 // AddBool adds the given boolean at the last.
 func (p *ArrayProxy) AddBool(values ...bool) *ArrayProxy {
 	for _, value := range values {
diff --git a/pkg/document/proxy/object_proxy.go b/pkg/document/proxy/object_proxy.go
index 043a8201..976b81f8 100644
--- a/pkg/document/proxy/object_proxy.go
+++ b/pkg/document/proxy/object_proxy.go
@@ -93,6 +93,15 @@ func (p *ObjectProxy) SetNewCounter(k string, n interface{}) *CounterProxy {
 	return v.(*CounterProxy)
 }
 
+// SetNull sets the null for the given key.
+func (p *ObjectProxy) SetNull(k string) *ObjectProxy {
+	p.setInternal(k, func(ticket *time.Ticket) json.Element {
+		return json.NewPrimitive(nil, ticket)
+	})
+
+	return p
+}
+
 // SetBool sets the given boolean for the given key.
 func (p *ObjectProxy) SetBool(k string, v bool) *ObjectProxy {
 	p.setInternal(k, func(ticket *time.Ticket) json.Element {


Test patch:
diff --git a/api/converter/converter_test.go b/api/converter/converter_test.go
index 9b8758c7..adfda747 100644
--- a/api/converter/converter_test.go
+++ b/api/converter/converter_test.go
@@ -67,6 +67,7 @@ func TestConverter(t *testing.T) {
 		err := doc.Update(func(root *proxy.ObjectProxy) error {
 			// an object and primitive types
 			root.SetNewObject("k1").
+				SetNull("k1.0").
 				SetBool("k1.1", true).
 				SetInteger("k1.2", 2147483647).
 				SetLong("k1.3", 9223372036854775807).
@@ -78,6 +79,7 @@ func TestConverter(t *testing.T) {
 
 			// an array
 			root.SetNewArray("k2").
+				AddNull().
 				AddBool(true).
 				AddInteger(1).
 				AddLong(2).
diff --git a/pkg/document/json/object_test.go b/pkg/document/json/object_test.go
index 7e691769..545a0f4f 100644
--- a/pkg/document/json/object_test.go
+++ b/pkg/document/json/object_test.go
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2021 The Yorkie Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package json_test
 
 import (
diff --git a/pkg/document/json/primitive_test.go b/pkg/document/json/primitive_test.go
new file mode 100644
index 00000000..95596a76
--- /dev/null
+++ b/pkg/document/json/primitive_test.go
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Yorkie Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package json_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/yorkie-team/yorkie/pkg/document/json"
+	"github.com/yorkie-team/yorkie/pkg/document/time"
+)
+
+func TestPrimitive(t *testing.T) {
+	tests := []struct {
+		value     interface{}
+		valueType json.ValueType
+		marshal   string
+	}{
+		{nil, json.Null, "null"},
+		{false, json.Boolean, "false"},
+		{0, json.Integer, "0"},
+		{"0", json.String, `"0"`},
+		{[]byte{}, json.Bytes, `""`},
+	}
+
+	t.Run("creation test", func(t *testing.T) {
+		for _, test := range tests {
+			prim := json.NewPrimitive(test.value, time.InitialTicket)
+			assert.Equal(t, prim.ValueType(), prim.ValueType())
+			assert.Equal(t, prim.Value(), json.ValueFromBytes(prim.ValueType(), prim.Bytes()))
+			assert.Equal(t, prim.Marshal(), test.marshal)
+		}
+	})
+}