forked from dugancathal/dynago
-
Notifications
You must be signed in to change notification settings - Fork 0
/
types.go
316 lines (273 loc) · 8.07 KB
/
types.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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package dynago
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"time"
)
/*
BinarySet stores a set of binary blobs in dynamo.
While implemented as a list in Go, DynamoDB does not preserve ordering on set
types and so may come back in a different order on retrieval. Use dynago.List
if ordering is important.
*/
type BinarySet [][]byte
/*
List represents DynamoDB lists, which are functionally very similar to JSON
lists. Like JSON lists, these lists are heterogeneous, which means that the
elements of the list can be any valid value type, which includes other lists,
documents, numbers, strings, etc.
*/
type List []interface{}
/*
Return a copy of this list with all elements coerced as Documents.
It's very common to use lists in dynago where all elements in the list are
a Document. For that reason, this method is provided as a convenience to
get back your list as a list of documents.
If any element in the List is not a document, this will error.
As a convenience, even when it errors, a slice containing any elements
preceding the one which errored as documents will be given.
*/
func (l List) AsDocumentList() ([]Document, error) {
docs := make([]Document, len(l))
for i, listItem := range l {
if doc, ok := listItem.(Document); !ok {
return docs[:i], fmt.Errorf("item at index %d was not a Document", i)
} else {
docs[i] = doc
}
}
return docs, nil
}
/*
A Number.
DynamoDB returns numbers as a string representation because they have a single
high-precision number type that can take the place of integers, floats, and
decimals for the majority of types.
This method has helpers to get the value of this number as one of various
Golang numeric types.
*/
type Number string
// IntVal interprets this number as an integer in base-10.
// error is returned if this is not a valid number or is too large.
func (n Number) IntVal() (int, error) {
return strconv.Atoi(string(n))
}
// Int64Val interprets this number as an integer in base-10.
// error is returned if this string cannot be parsed as base 10 or is too large.
func (n Number) Int64Val() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
// Uint64Val interprets this number as an unsigned integer.
// error is returned if this is not a valid positive integer or cannot fit.
func (n Number) Uint64Val() (uint64, error) {
return strconv.ParseUint(string(n), 10, 64)
}
// FloatVal interprets this number as a floating point.
// error is returned if this number is not well-formed.
func (n Number) FloatVal() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
/*
NumberSet is an un-ordered set of numbers.
Sets in DynamoDB do not guarantee any ordering, so storing and retrieving a
NumberSet may not give you back the same order you put it in. The main
advantage of using sets in DynamoDB is using atomic updates with ADD and DELETE
in your UpdateExpression.
*/
type NumberSet []string
/*
Document is the core type for many dynamo operations on documents.
It is used to represent the root-level document, maps values, and
can also be used to supply expression parameters to queries.
*/
type Document map[string]interface{}
// MarshalJSON is used for encoding Document into wire representation.
func (d Document) MarshalJSON() ([]byte, error) {
output := make(map[string]interface{}, len(d))
for key, val := range d {
if v := reflect.ValueOf(val); !isEmptyValue(v) {
output[key] = wireEncode(val)
}
}
return json.Marshal(output)
}
// UnmarshalJSON is used for unmarshaling Document from the wire representation.
func (d *Document) UnmarshalJSON(buf []byte) error {
raw := make(map[string]interface{})
err := json.Unmarshal(buf, &raw)
if err != nil {
return err
}
if *d == nil {
*d = make(Document)
}
dd := *d
for key, val := range raw {
dd[key] = wireDecode(val)
}
return nil
}
/*
GetList gets the value at key as a List.
If value at key is nil, returns a nil list.
If value at key is not a List, will panic.
*/
func (d Document) GetList(key string) List {
if d[key] != nil {
return d[key].(List)
} else {
return nil
}
}
/*
GetString gets the value at key as a String.
If the value at key is nil, returns an empty string.
If the value at key is not nil or a string, will panic.
*/
func (d Document) GetString(key string) string {
if d[key] != nil {
return d[key].(string)
} else {
return ""
}
}
/*
GetNumber gets the value at key as a Number.
If the value at key is nil, returns a number containing the empty string.
If the value is not a Number or nil, will panic.
*/
func (d Document) GetNumber(key string) Number {
if d[key] != nil {
return d[key].(Number)
} else {
return Number("")
}
}
/*
GetStringSet gets the value specified by key a StringSet.
If value at key is nil; returns an empty StringSet.
If it exists but is not a StringSet, panics.
*/
func (d Document) GetStringSet(key string) StringSet {
if d[key] != nil {
return d[key].(StringSet)
} else {
return StringSet{}
}
}
/*
Helper to get a Time from a document.
If the value is omitted from the DB, or an empty string, then the return
is nil. If the value fails to parse as iso8601, then this method panics.
*/
func (d Document) GetTime(key string) (t *time.Time) {
val := d[key]
if val != nil {
s := val.(string)
parsed, err := time.ParseInLocation(iso8601compact, s, time.UTC)
if err != nil {
panic(err)
}
t = &parsed
}
return t
}
// AsParams makes Document satisfy the Params interface.
func (d Document) AsParams() (params []Param) {
for key, val := range d {
params = append(params, Param{key, val})
}
return
}
/*
GetBool gets the value specified by key as a boolean.
If the value does not exist in this Document, returns false.
If the value is the nil interface, also returns false.
If the value is a bool, returns the value of the bool.
If the value is a Number, returns true if value is non-zero.
For any other values, panics.
*/
func (d Document) GetBool(key string) bool {
if v := d[key]; v != nil {
switch val := v.(type) {
case bool:
return val
case Number:
if res, err := val.IntVal(); err != nil {
panic(err)
} else if res == 0 {
return false
} else {
return true
}
default:
panic(v)
}
} else {
return false
}
}
// HashKey is a shortcut to building keys used for various item operations.
func HashKey(name string, value interface{}) Document {
return Document{name: value}
}
// HashRangeKey is a shortcut for building keys used for various item operations.
func HashRangeKey(hashName string, hashVal interface{}, rangeName string, rangeVal interface{}) Document {
return Document{
hashName: hashVal,
rangeName: rangeVal,
}
}
// Param can be used as a single parameter.
type Param struct {
Key string
Value interface{}
}
// AsParsms allows a solo Param to also satisfy the Params interface
func (p Param) AsParams() []Param {
return []Param{p}
}
// P is a shortcut to create a single dynago Param.
// This is mainly for brevity especially with cross-package imports.
func P(key string, value interface{}) Params {
return Param{key, value}
}
/*
Params encapsulates anything which can be used as expression parameters for
dynamodb expressions.
DynamoDB item queries using expressions can be provided parameters in a number
of handy ways:
.Param(":k1", v1).Param(":k2", v2)
-or-
.Params(P(":k1", v1), P(":k2", v2))
-or-
.FilterExpression("...", P(":k1", v1), P(":k2", v2))
-or-
.FilterExpression("...", Document{":k1": v1, ":k2": v2})
Or any combination of Param, Document, or potentially other custom types which
provide the Params interface.
*/
type Params interface {
AsParams() []Param
}
/*
StringSet is an un-ordered collection of distinct strings.
Sets in DynamoDB do not guarantee any ordering, so storing and retrieving a
StringSet may not give you back the same order you put it in. The main
advantage of using sets in DynamoDB is using atomic updates with ADD and DELETE
in your UpdateExpression.
*/
type StringSet []string
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflect.Invalid:
return true
}
return false
}