Skip to content

Commit

Permalink
create v2 package
Browse files Browse the repository at this point in the history
  • Loading branch information
tauraamui committed Jul 11, 2023
1 parent 041564a commit 56f97e8
Show file tree
Hide file tree
Showing 9 changed files with 1,184 additions and 0 deletions.
30 changes: 30 additions & 0 deletions v2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.DEFAULT_GOAL := default

.PHONY: test
test:
gotestsum ./...

.PHONY: test-verbose
test-verbose:
gotestsum --format standard-verbose ./...

.PHONY: coverage
coverage:
go test -coverpkg=./... -coverprofile=coverage.out ./... && go tool cover -func coverage.out && rm coverage.out

.PHONY: coverage-persist
coverage-persist:
go test -coverpkg=./... -coverprofile=coverage.out ./... && go tool cover -func coverage.out

.PHONY: install-gotestsum
install-gotestsum:
go get github.com/gotestyourself/gotestsum

.PHONY: install-linter
install-linter:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.1

.PHONY: lint
lint:
golangci-lint run

243 changes: 243 additions & 0 deletions v2/entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package kvs

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"

"github.com/dgraph-io/badger/v3"
"github.com/google/uuid"
)

type Entry struct {
TableName string
ColumnName string
OwnerUUID UUID
RowID uint32
Data []byte
}

func (e Entry) PrefixKey() []byte {
return []byte(fmt.Sprintf("%s.%s.%s", e.TableName, e.ColumnName, e.resolveOwnerID()))
}

func (e Entry) Key() []byte {
return []byte(fmt.Sprintf("%s.%s.%s.%d", e.TableName, e.ColumnName, e.resolveOwnerID(), e.RowID))
}

func (e Entry) resolveOwnerID() string {
if e.OwnerUUID == nil {
e.OwnerUUID = RootOwner{}
}
return e.OwnerUUID.String()
}

func Store(db KVDB, e Entry) error {
return db.conn.Update(func(txn *badger.Txn) error {
return txn.Set([]byte(e.Key()), e.Data)
})
}

func Get(db KVDB, e *Entry) error {
return db.conn.View(func(txn *badger.Txn) error {
lookupKey := e.Key()
item, err := txn.Get(lookupKey)
if err != nil {
return fmt.Errorf("%s: %s", strings.ToLower(err.Error()), lookupKey)
}

if err := item.Value(func(val []byte) error {
e.Data = val
return nil
}); err != nil {
return err
}

return nil
})
}

func ConvertToBlankEntries(tableName string, ownerID UUID, rowID uint32, x any) []Entry {
v := reflect.ValueOf(x)
return convertToEntries(tableName, ownerID, rowID, v, false)
}

func ConvertToEntries(tableName string, ownerID UUID, rowID uint32, x any) []Entry {
v := reflect.ValueOf(x)
return convertToEntries(tableName, ownerID, rowID, v, true)
}

type UUID interface {
String() string
}

type RootOwner struct{}

func (o RootOwner) String() string { return "root" }

func LoadEntry(s interface{}, entry Entry) error {
// convert the interface value to a reflect.Value so we can access its fields
val := reflect.ValueOf(s).Elem()

field, err := resolveFieldRef(val, entry.ColumnName)
if err != nil {
return err
}

// convert the entry's Data field to the type of the target field
if err := convertFromBytes(entry.Data, field.Addr().Interface()); err != nil {
return fmt.Errorf("failed to convert entry data to field type: %v", err)
}

return nil
}

func LoadID(s any, rowID uint32) error {
// convert the interface value to a reflect.Value so we can access its fields
val := reflect.ValueOf(s).Elem()

field, err := resolveFieldRef(val, "ID")
if err != nil {
return err
}

// convert the entry's Data field to the type of the target field
if err := assignUint32(rowID, field.Addr().Interface()); err != nil {
return fmt.Errorf("failed to convert entry data to field type: %v", err)
}

return nil
}

func resolveFieldRef(v reflect.Value, nameToMatch string) (reflect.Value, error) {
t := v.Type()

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)

if strings.EqualFold(field.Name, nameToMatch) {
return v.Field(i), nil
}
}

return reflect.Zero(reflect.TypeOf(v)), fmt.Errorf("struct does not have a field with name %q", nameToMatch)
}

func LoadEntries(s interface{}, entries []Entry) error {
for _, entry := range entries {
if err := LoadEntry(s, entry); err != nil {
return err
}
}

return nil
}

func convertToEntries(tableName string, ownerUUID UUID, rowID uint32, v reflect.Value, includeData bool) []Entry {
entries := []Entry{}

if v.Kind() == reflect.Pointer {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
vv := reflect.Indirect(v)
f := vv.Type().Field(i)

fOpts := resolveFieldOptions(f)
if fOpts.Ignore {
continue
}

e := Entry{
TableName: tableName,
ColumnName: strings.ToLower(f.Name),
OwnerUUID: ownerUUID,
RowID: rowID,
}

if includeData {
bd, err := convertToBytes(v.Field(i).Interface())
if err != nil {
return entries
}
e.Data = bd
}

entries = append(entries, e)
}

return entries
}

func convertToBytes(i interface{}) ([]byte, error) {
// Check the type of the interface.
switch v := i.(type) {
case []byte:
// Return the input as a []byte if it is already a []byte.
return v, nil
case string:
// Convert the string to a []byte and return it.
return []byte(v), nil
default:
// Use json.Marshal to convert the interface to a []byte.
return json.Marshal(v)
}
}

func assignUint32(data uint32, dest any) error {
// Check that the destination argument is a pointer.
if reflect.TypeOf(dest).Kind() != reflect.Ptr {
return fmt.Errorf("destination must be a pointer")
}

switch v := dest.(type) {
case *uint32:
*v = data
return nil
}

return errors.New("struct field ID is not of type uint32")
}

func convertFromBytes(data []byte, i interface{}) error {
// Check that the destination argument is a pointer.
if reflect.TypeOf(i).Kind() != reflect.Ptr {
return fmt.Errorf("destination must be a pointer")
}

// Check the type of the interface.
switch v := i.(type) {
case *[]byte:
// Set the value of the interface to the []byte if it is a pointer to a []byte.
*v = data
return nil
case *string:
// Convert the []byte to a string and set the value of the interface to the string.
*v = string(data)
return nil
case *UUID:
// Convert the []byte to a UUID instance and set the value of the interface to it.
uuidv, err := uuid.ParseBytes(data)
if err != nil {
return err
}
*v = uuidv
return nil
default:
// Use json.Unmarshal to convert the []byte to the interface.
return json.Unmarshal(data, v)
}
}

type mdbFieldOptions struct {
Ignore bool
}

func resolveFieldOptions(f reflect.StructField) mdbFieldOptions {
mdbTagValue := f.Tag.Get("mdb")
return mdbFieldOptions{
Ignore: strings.Contains(mdbTagValue, "ignore"),
}
}
63 changes: 63 additions & 0 deletions v2/entry_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package kvs

import (
"testing"

"github.com/matryer/is"
)

func TestConvertToBytes(t *testing.T) {
is := is.New(t)

// Test input and expected output.
tests := []struct {
input interface{}
expected []byte
}{
{[]byte{1, 2, 3}, []byte{1, 2, 3}},
{"hello", []byte("hello")},
{struct{ A int }{5}, []byte("{\"A\":5}")},
}

// Iterate over the tests and compare the output of convertToBytes to the expected output.
for _, test := range tests {
result, err := convertToBytes(test.input)
is.NoErr(err)
is.Equal(result, test.expected)
}
}

func TestConvertBytesFromBytes(t *testing.T) {
is := is.New(t)

input := []byte{1, 2, 3}
var destination []byte
err := convertFromBytes(input, &destination)
is.NoErr(err)
is.Equal(destination, input)

}

func TestConvertStringFromBytes(t *testing.T) {
is := is.New(t)

input := []byte("hello")
var destination string
err := convertFromBytes(input, &destination)
is.NoErr(err)
is.Equal(destination, string(input))
}

func TestConvertStructFromBytes(t *testing.T) {
is := is.New(t)

type TestStruct struct {
A int
B string
}
input := []byte("{\"A\":5,\"B\":\"hello\"}")
var destination TestStruct
err := convertFromBytes(input, &destination)
is.NoErr(err)
is.Equal(destination, TestStruct{A: 5, B: "hello"})
}
Loading

0 comments on commit 56f97e8

Please sign in to comment.