Repository: googleforgames/open-saves
Pull request #259 (2021-05-21 22:51:01)

feat: Add ChunkRef struct
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://github.com/googleforgames/open-saves/blob/main/docs/contributing.md and developer guide https://github.com/googleforgames/open-saves/blob/main/docs/development.md
2. Please label this pull request according to what type of issue you are addressing.
3. Ensure you have added or ran the appropriate tests for your PR.
-->

**What type of PR is this?**
/kind feat

**What this PR does / Why we need it**:
Add a new struct ChunkRef to manage chunked blob objects. Server implementations will be added later.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Closes #<issue number>`, or `Closes (paste link of issue)`.
-->
Closes #

**Special notes for your reviewer**:



Patch:
diff --git a/internal/pkg/metadb/blobref/chunkref/chunkref.go b/internal/pkg/metadb/blobref/chunkref/chunkref.go
new file mode 100644
index 0000000..6013abd
--- /dev/null
+++ b/internal/pkg/metadb/blobref/chunkref/chunkref.go
@@ -0,0 +1,113 @@
+// Copyright 2021 Google LLC
+//
+// 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 chunkref
+
+import (
+	"bytes"
+	"encoding/gob"
+
+	"cloud.google.com/go/datastore"
+	"github.com/google/uuid"
+	"github.com/googleforgames/open-saves/internal/pkg/cache"
+	"github.com/googleforgames/open-saves/internal/pkg/metadb/timestamps"
+)
+
+type ChunkRef struct {
+	Key     uuid.UUID `datastore:"-"`
+	BlobRef uuid.UUID `datastore:"-"`
+
+	Number     int32
+	Size       int32
+	Timestamps timestamps.Timestamps
+}
+
+// Assert ChunkRef implements both PropertyLoadSave and KeyLoader.
+var _ datastore.PropertyLoadSaver = new(ChunkRef)
+var _ datastore.KeyLoader = new(ChunkRef)
+
+// Assert ChunkRef implements Cacheable
+var _ cache.Cacheable = new(ChunkRef)
+
+func (c *ChunkRef) LoadKey(k *datastore.Key) error {
+	if uuidKey, err := uuid.Parse(k.Name); err == nil {
+		c.Key = uuidKey
+	} else {
+		return err
+	}
+	if k.Parent != nil {
+		if uuidParent, err := uuid.Parse(k.Parent.Name); err == nil {
+			c.BlobRef = uuidParent
+		} else {
+			return err
+		}
+	}
+	return nil
+}
+
+// Save and Load replicates the default behaviors, however, they are required
+// for the KeyLoader interface.
+
+func (c *ChunkRef) Load(ps []datastore.Property) error {
+	return datastore.LoadStruct(c, ps)
+}
+
+func (c *ChunkRef) Save() ([]datastore.Property, error) {
+	return datastore.SaveStruct(c)
+}
+
+func (c *ChunkRef) ObjectPath() string {
+	return c.Key.String()
+}
+
+// New creates a new ChunkRef instance with the input parameters.
+func New(blobRef uuid.UUID, number, size int32) *ChunkRef {
+	return &ChunkRef{
+		Key:     uuid.New(),
+		BlobRef: blobRef,
+		Number:  number,
+		Size:    size,
+	}
+}
+
+// CacheKey returns a cache key string to store in the cache.
+// It returns a string representation of the uuid key.
+func CacheKey(u uuid.UUID) string {
+	return u.String()
+}
+
+// Cacheable implementations.
+
+// CacheKey returns a cache key string to store in the cache.
+// It returns a string representation of the uuid key.
+func (c ChunkRef) CacheKey() string {
+	return CacheKey(c.Key)
+}
+
+// EncodeBytes returns a serialized byte slice of the object.
+func (c *ChunkRef) EncodeBytes() ([]byte, error) {
+	b := new(bytes.Buffer)
+	e := gob.NewEncoder(b)
+	if err := e.Encode(c); err != nil {
+		return nil, err
+	}
+	return b.Bytes(), nil
+}
+
+// DecodeBytes deserializes the byte slice given by by.
+func (c *ChunkRef) DecodeBytes(by []byte) error {
+	b := bytes.NewBuffer(by)
+	d := gob.NewDecoder(b)
+	return d.Decode(c)
+}


Test patch:
diff --git a/internal/pkg/metadb/blobref/chunkref/chunkref_test.go b/internal/pkg/metadb/blobref/chunkref/chunkref_test.go
new file mode 100644
index 0000000..1c429d0
--- /dev/null
+++ b/internal/pkg/metadb/blobref/chunkref/chunkref_test.go
@@ -0,0 +1,94 @@
+// Copyright 2021 Google LLC
+//
+// 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 chunkref
+
+import (
+	"testing"
+
+	"cloud.google.com/go/datastore"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestChunkRef_New(t *testing.T) {
+	blobuuid := uuid.New()
+	c := New(blobuuid, 42, 123450)
+	assert.NotEqual(t, uuid.Nil, c.Key)
+	assert.Equal(t, blobuuid, c.BlobRef)
+	assert.Equal(t, int32(42), c.Number)
+	assert.Equal(t, int32(123450), c.Size)
+}
+
+func TestChunkRef_ObjectPath(t *testing.T) {
+	c := New(uuid.Nil, 0, 0)
+	assert.Equal(t, c.Key.String(), c.ObjectPath())
+}
+
+func TestChunkRef_SaveLoad(t *testing.T) {
+	c := New(uuid.New(), 42, 24)
+	ps, err := c.Save()
+	if err != nil {
+		t.Fatalf("Save returned error: %v", err)
+	}
+	assert.Len(t, ps, 3)
+	loaded := new(ChunkRef)
+	if err := loaded.Load(ps); err != nil {
+		t.Fatalf("Load returned error: %v", err)
+	}
+	// Reset Key and BlobRef as they are not loaded by Load()
+	c.Key = uuid.Nil
+	c.BlobRef = uuid.Nil
+	assert.Equal(t, c, loaded)
+}
+
+func TestChunkRef_LoadKey(t *testing.T) {
+	testUUID := uuid.New()
+	c := new(ChunkRef)
+	if err := c.LoadKey(datastore.NameKey("chunk", testUUID.String(), nil)); assert.NoError(t, err) {
+		assert.Equal(t, testUUID, c.Key)
+		assert.Equal(t, uuid.Nil, c.BlobRef)
+	}
+
+	c = new(ChunkRef)
+	testBlobUUID := uuid.New()
+	key := datastore.NameKey("chunk", testUUID.String(),
+		datastore.NameKey("blobref", testBlobUUID.String(), nil))
+	if err := c.LoadKey(key); assert.NoError(t, err) {
+		assert.Equal(t, testUUID, c.Key)
+		assert.Equal(t, testBlobUUID, c.BlobRef)
+	}
+}
+
+func TestChunkRef_CacheKey(t *testing.T) {
+	testUUID := uuid.New()
+	assert.Equal(t, testUUID.String(), CacheKey(testUUID))
+	c := New(uuid.Nil, 0, 0)
+	assert.Equal(t, c.Key.String(), c.CacheKey())
+}
+
+func TestChunkRef_EncodeDecodeBytes(t *testing.T) {
+	c := New(uuid.New(), 42, 24)
+	encoded, err := c.EncodeBytes()
+	if err != nil {
+		t.Fatalf("EncodeBytes failed with error: %v", err)
+	}
+	assert.NotEmpty(t, encoded)
+
+	decoded := new(ChunkRef)
+	if err := decoded.DecodeBytes(encoded); err != nil {
+		t.Fatalf("DecodeBytes failed with error: %v", err)
+	}
+	assert.Equal(t, c, decoded)
+}