Skip to content

Commit

Permalink
[Feat] #1 Define struct of non-Scalar data types (#9)
Browse files Browse the repository at this point in the history
* add Sets data type

* add go.mod

* mod gitignore

* add errors

* * Exclude float32 from Sets support
* add IsCompatible func

* fix Sets#IsCompatible

* added List & Map type

* fix go.mod
  • Loading branch information
miyamo2 committed Mar 16, 2024
1 parent 4dc5837 commit c9443c2
Show file tree
Hide file tree
Showing 10 changed files with 1,174 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@

# Go workspace file
go.work

.idea
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/miyamo2/dynamgorm

go 1.21

require github.com/google/go-cmp v0.6.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
8 changes: 8 additions & 0 deletions types/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package types

import "errors"

var (
ErrCollectionAlreadyContainsItem = errors.New("collection already contains item")
ErrFailedToCast = errors.New("failed to cast")
)
79 changes: 79 additions & 0 deletions types/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package types

import (
"errors"
"fmt"
)

// List is a DynamoDB list type.
//
// See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
type List []interface{}

// GormDataType returns the data type for Gorm.
func (l *List) GormDataType() string {
return "dglist"
}

// Scan implements the sql.Scanner#Scan
func (l *List) Scan(value interface{}) error {
if len(*l) != 0 {
return ErrCollectionAlreadyContainsItem
}
sv, ok := value.([]interface{})
if !ok {
return errors.Join(ErrFailedToCast, fmt.Errorf("incompatible %T and %T", l, value))
}
*l = sv
return l.ResolveNestedDocument()
}

// ResolveNestedDocument resolves nested document type attribute.
func (l *List) ResolveNestedDocument() error {
for i, v := range *l {
if v, ok := v.(map[string]interface{}); ok {
m := Map{}
err := m.Scan(v)
if err != nil {
*l = nil
return err
}
(*l)[i] = m
continue
}
if s := newSets[int](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*l)[i] = s
continue
}
}
if s := newSets[float64](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*l)[i] = s
continue
}
}
if s := newSets[string](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*l)[i] = s
continue
}
}
if s := newSets[[]byte](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*l)[i] = s
continue
}
}
if v, ok := v.([]interface{}); ok {
il := List{}
err := il.Scan(v)
if err != nil {
*l = nil
return err
}
(*l)[i] = il
}
}
return nil
}
127 changes: 127 additions & 0 deletions types/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package types

import (
"errors"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestList_GormDataType(t *testing.T) {
l := &List{}
if got := l.GormDataType(); got != "dglist" {
t.Errorf("GormDataType() = %v, want %v", got, "dglist")
}
}

func TestList_Scan(t *testing.T) {
type testCase struct {
sut List
args interface{}
expectedState List
want error
}
tests := map[string]testCase{
"happy-path/empty-list": {
sut: List{},
args: []interface{}{},
expectedState: List{},
},
"happy-path/single-value": {
sut: List{},
args: []interface{}{1},
expectedState: List{1},
},
"happy-path/multiple-values": {
sut: List{},
args: []interface{}{1, "2"},
expectedState: List{1, "2"},
},
"happy-path/single-nested-map": {
sut: List{},
args: []interface{}{map[string]interface{}{"a": 1}},
expectedState: List{Map{"a": 1}},
},
"unhappy-path/sut-is-not-empty": {
sut: List{1, 2, 3},
args: []interface{}{4, 5, 6},
expectedState: List{1, 2, 3},
want: ErrCollectionAlreadyContainsItem,
},
"unhappy-path/non-slice-value": {
sut: List{},
args: "non-slice",
expectedState: List{},
want: ErrFailedToCast,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
err := tt.sut.Scan(tt.args)
if !errors.Is(err, tt.want) {
t.Errorf("Scan() error = %v, want %v", err, tt.want)
return
}
if diff := cmp.Diff(tt.expectedState, tt.sut); diff != "" {
t.Errorf("Scan() mismatch (-want +got):\n%s", diff)
return
}
})
}
}

func TestList_ResolveNestedCollections(t *testing.T) {
type testCase struct {
sut List
expectedState List
want error
}
tests := map[string]testCase{
"happy-path/empty-list": {
sut: List{},
expectedState: List{},
},
"happy-path/single-nested-map": {
sut: List{map[string]interface{}{"a": 1}},
expectedState: List{Map{"a": 1}},
},
"happy-path/multiple-nested-maps": {
sut: List{map[string]interface{}{"a": 1}, map[string]interface{}{"b": 2}},
expectedState: List{Map{"a": 1}, Map{"b": 2}},
},
"happy-path/nested-list": {
sut: List{[]interface{}{1, "b"}},
expectedState: List{List{1, "b"}},
},
"happy-path/nested-int-sets": {
sut: List{[]interface{}{int(1), 2, int(3)}},
expectedState: List{Sets[int]{1, 2, 3}},
},
"happy-path/nested-float-sets": {
sut: List{[]interface{}{float64(1.1), 2.1, float64(3.1)}},
expectedState: List{Sets[float64]{1.1, 2.1, 3.1}},
},
"happy-path/nested-string-sets": {
sut: List{[]interface{}{string("1"), string("2"), string("3")}},
expectedState: List{Sets[string]{"1", "2", "3"}},
},
"happy-path/nested-binary-sets": {
sut: List{[]interface{}{[]byte("1"), []byte("2"), []byte("3")}},
expectedState: List{Sets[[]byte]{[]byte("1"), []byte("2"), []byte("3")}},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
err := tt.sut.ResolveNestedDocument()
if !errors.Is(err, tt.want) {
t.Errorf("ResolveNestedDocument() error = %v, want %v", err, tt.want)
return

}
if diff := cmp.Diff(tt.expectedState, tt.sut); diff != "" {
t.Errorf("ResolveNestedDocument() mismatch (-want +got):\n%s", diff)
return
}
})
}
}
75 changes: 75 additions & 0 deletions types/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package types

// Map is a DynamoDB map type.
//
// See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
type Map map[string]interface{}

// GormDataType returns the data type for Gorm.
func (m *Map) GormDataType() string {
return "dgmap"
}

// Scan implements the sql.Scanner#Scan
func (m *Map) Scan(value interface{}) error {
if len(*m) != 0 {
return ErrCollectionAlreadyContainsItem
}
mv, ok := value.(map[string]interface{})
if !ok {
*m = nil
return ErrFailedToCast
}
*m = mv
return m.ResolveNestedDocument()
}

// ResolveNestedDocument resolves nested document type attribute.
func (m *Map) ResolveNestedDocument() error {
for k, v := range *m {
if v, ok := v.(map[string]interface{}); ok {
im := Map{}
err := im.Scan(v)
if err != nil {
*m = nil
return err
}
(*m)[k] = im
continue
}
if s := newSets[int](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*m)[k] = s
continue
}
}
if s := newSets[float64](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*m)[k] = s
continue
}
}
if s := newSets[string](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*m)[k] = s
continue
}
}
if s := newSets[[]byte](); s.IsCompatible(v) {
if err := s.Scan(v); err == nil {
(*m)[k] = s
continue
}
}
if v, ok := v.([]interface{}); ok {
l := List{}
err := l.Scan(v)
if err != nil {
*m = nil
return err
}
(*m)[k] = l
}
}
return nil
}
Loading

0 comments on commit c9443c2

Please sign in to comment.