-
Notifications
You must be signed in to change notification settings - Fork 0
/
dynago.go
208 lines (189 loc) · 5.26 KB
/
dynago.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package dynago
import (
"fmt"
"reflect"
"sync"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
"github.com/twharmon/slices"
)
// DynagoAPI provides an interface to enable mocking the
// dynago.Dynago service client's API operations. This make unit
// testing your code easier.
type DynagoAPI interface {
DeleteItem(Keyer) *DeleteItem
PutItem(Keyer) *PutItem
GetItem(Keyer) *GetItem
Query(interface{}) *Query
Scan(interface{}) *Scan
UpdateItem(Keyer) *UpdateItem
ConditionCheck(Keyer) *ConditionCheck
TransactionWriteItems() *TransactionWriteItems
Marshal(interface{}) (map[string]*dynamodb.AttributeValue, error)
Unmarshal(map[string]*dynamodb.AttributeValue, interface{}) error
}
var _ DynagoAPI = (*Dynago)(nil)
type Keyer interface {
PrimaryKeys() []string
}
// Dynago provides the API operation methods for making requests to
// Amazon DynamoDB. Dynago methods are safe to use concurrently.
type Dynago struct {
config *Config
cache map[string]map[int]*field
cacheMtx sync.Mutex
ddb dynamodbiface.DynamoDBAPI
}
// Config is used to customize struct tag names.
type Config struct {
// AttrTagName specifies which tag is used for a DynamoDB
// item attribute name. Defaults to "attr".
AttrTagName string
// FmtTagName specifies which tag is used to format the attribute
// value. This is only used for String types. Defaults to "fmt".
FmtTagName string
// TypeTagName specifies which tag is used for DynamoDB type.
// Defaults to "type".
TypeTagName string
// LayoutTagName specifies which tag is used for formatting time
// values. Defaults to "layout".
LayoutTagName string
// AttrsToCopyTagName specifies which tag is used to determine
// which other attributes should have same value. Defaults to
// "copy".
AttrsToCopyTagName string
// AdditionalAttrs can be added for each dynamodb item.
AdditionalAttrs func(map[string]*dynamodb.AttributeValue, reflect.Value)
// DefaultTableName is the default table queried when not
// specified.
DefaultTableName string
// DefaultConsistentRead is the default read consistency model.
DefaultConsistentRead bool
}
// New creates a new Dynago client. An optional config can be passed
// in second argument.
func New(ddb dynamodbiface.DynamoDBAPI, config ...*Config) *Dynago {
d := Dynago{
cache: make(map[string]map[int]*field),
config: &Config{},
}
if len(config) > 0 {
d.config = config[0]
}
if d.config.AttrTagName == "" {
d.config.AttrTagName = "attr"
}
if d.config.AttrsToCopyTagName == "" {
d.config.AttrsToCopyTagName = "copy"
}
if d.config.FmtTagName == "" {
d.config.FmtTagName = "fmt"
}
if d.config.TypeTagName == "" {
d.config.TypeTagName = "type"
}
if d.config.LayoutTagName == "" {
d.config.LayoutTagName = "layout"
}
d.ddb = ddb
return &d
}
// Unmarshal converts a DynamoDB item into a Go struct.
func (d *Dynago) Unmarshal(item map[string]*dynamodb.AttributeValue, v interface{}) error {
ty, val := tyVal(v)
cache, err := d.cachedStruct(ty)
if err != nil {
return fmt.Errorf("d.cachedStruct: %w", err)
}
for i := 0; i < ty.NumField(); i++ {
if cache[i].attrName == "-" || ty.Field(i).Anonymous {
continue
}
if err := cache[i].unmarshal(item, val); err != nil {
return err
}
}
return nil
}
// Marshal converts a Go struct into a DynamoDB item.
func (d *Dynago) Marshal(v interface{}) (map[string]*dynamodb.AttributeValue, error) {
m := make(map[string]*dynamodb.AttributeValue)
ty, val := tyVal(v)
cache, err := d.cachedStruct(ty)
if err != nil {
return nil, fmt.Errorf("d.cachedStruct: %w", err)
}
_, isTopLevel := v.(Keyer)
for i := 0; i < ty.NumField(); i++ {
if cache[i].attrName == "-" {
continue
}
attrVal, err := cache[i].attrVal(val)
if err != nil {
return nil, fmt.Errorf("cache.attrVal: %w", err)
}
if attrVal == nil {
continue
}
m[cache[i].attrName] = attrVal
for _, cp := range cache[i].attrsToCopy {
m[cp] = attrVal
}
}
if isTopLevel && d.config.AdditionalAttrs != nil {
d.config.AdditionalAttrs(m, val)
}
return m, nil
}
func (d *Dynago) key(v Keyer) (map[string]*dynamodb.AttributeValue, error) {
m := make(map[string]*dynamodb.AttributeValue)
ty, val := tyVal(v)
cache, err := d.cachedStruct(ty)
if err != nil {
return nil, fmt.Errorf("d.cachedStruct: %w", err)
}
primKeys := v.PrimaryKeys()
for _, primKey := range primKeys {
for i := 0; i < ty.NumField(); i++ {
if cache[i].attrName == primKey || slices.Contains(cache[i].attrsToCopy, primKey) {
av, err := cache[i].attrVal(val)
if err != nil {
return nil, fmt.Errorf("cache.attrVal: %w", err)
}
m[primKey] = av
}
}
}
if d.config.AdditionalAttrs != nil {
d.config.AdditionalAttrs(m, val)
for k := range m {
isKey := false
for _, pk := range primKeys {
if pk == k {
isKey = true
break
}
}
if !isKey {
delete(m, k)
}
}
}
return m, nil
}
func (d *Dynago) cachedStruct(ty reflect.Type) (map[int]*field, error) {
key := ty.String()
d.cacheMtx.Lock()
defer d.cacheMtx.Unlock()
if d.cache[key] == nil {
d.cache[key] = make(map[int]*field)
for i := 0; i < ty.NumField(); i++ {
cfg, err := d.field(ty.Field(i), i)
if err != nil {
return nil, fmt.Errorf("d.field")
}
d.cache[key][i] = cfg
}
}
return d.cache[key], nil
}