Skip to content

Commit af39a0a

Browse files
authored
feat: impl sets, map, list and typed-list (#3)
1 parent 79b0941 commit af39a0a

11 files changed

+2752
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# sqldav
2-
Implementation of `database/sql/driver.Valuer` to convert slice | map to DynamoDB AttributeValue.
2+
3+
map | slice 🔄 "sql.Scanner"/"driver.Valuer" 🔄 DynamoDB AttributeValue. (And its Tooltip).

attribute_value.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package sqldav
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
7+
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
8+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
9+
"github.com/iancoleman/strcase"
10+
"reflect"
11+
"regexp"
12+
"strings"
13+
)
14+
15+
// ErrNestedStructHasIncompatibleAttributes occurs when the nested struct has incompatible attributes.
16+
var ErrNestedStructHasIncompatibleAttributes = errors.New("nested struct has incompatible attributes")
17+
18+
// AssignMapValueToReflectValue assigns the map type value to the reflect.Value
19+
func AssignMapValueToReflectValue(rt reflect.Type, rv reflect.Value, mv map[string]interface{}) error {
20+
for i := 0; i < rt.NumField(); i++ {
21+
tf := rt.Field(i)
22+
vf := func() reflect.Value {
23+
if rv.Kind() == reflect.Pointer {
24+
return rv.Elem().Field(i)
25+
}
26+
return rv.Field(i)
27+
}()
28+
name := getColumnNameFromStructField(tf)
29+
a, ok := mv[name]
30+
if !ok {
31+
continue
32+
}
33+
err := assignInterfaceValueToReflectValue(tf.Type, vf, a)
34+
if err != nil {
35+
return err
36+
}
37+
}
38+
return nil
39+
}
40+
41+
// ErrDocumentAttributeValueIsIncompatible occurs when an incompatible conversion to following:
42+
// - *types.AttributeValueMemberL
43+
// - *types.AttributeValueMemberM
44+
// - *types.AttributeValueMemberSS
45+
// - *types.AttributeValueMemberNS
46+
// - *types.AttributeValueMemberBS
47+
var ErrDocumentAttributeValueIsIncompatible = fmt.Errorf("document-attribute-value is incompatible")
48+
49+
// DocumentAttributeMember represents Document Attribute Member.
50+
type DocumentAttributeMember interface {
51+
*types.AttributeValueMemberL | *types.AttributeValueMemberM | *types.AttributeValueMemberSS | *types.AttributeValueMemberNS | *types.AttributeValueMemberBS
52+
}
53+
54+
// ToDocumentAttributeValue converts given interface to a DocumentAttributeMember.
55+
//
56+
// NOTE: this function returns a typed-nil if the conversion is incompatible.
57+
// therefore, nil-check is not guaranteed to work.
58+
func ToDocumentAttributeValue[T DocumentAttributeMember](value interface{}) (T, error) {
59+
v, err := toAttibuteValue(value)
60+
if err != nil {
61+
return nil, err
62+
}
63+
if v, ok := v.(T); ok {
64+
return v, nil
65+
}
66+
return nil, ErrDocumentAttributeValueIsIncompatible
67+
}
68+
69+
// reXORMColumnName matches column name from xorm tag
70+
var reXORMColumnName = regexp.MustCompile(`'(.*?)'`)
71+
72+
// getColumnNameFromStructField returns the column name from the struct field
73+
func getColumnNameFromStructField(sf reflect.StructField) string {
74+
tag := sf.Tag
75+
for _, value := range strings.Split(tag.Get("gorm"), ";") {
76+
if value == "" {
77+
continue
78+
}
79+
kv := strings.Split(value, ":")
80+
if len(kv) != 2 {
81+
continue
82+
}
83+
switch kv[0] {
84+
case "column":
85+
return kv[1]
86+
}
87+
}
88+
if name := tag.Get("db"); name != "" {
89+
return name
90+
}
91+
if xt := tag.Get("xorm"); xt != "" {
92+
matches := reXORMColumnName.FindAllString(xt, 1)
93+
if len(matches) > 0 {
94+
return matches[1]
95+
}
96+
}
97+
return strcase.ToSnake(sf.Name)
98+
}
99+
100+
// assignInterfaceValueToReflectValue assigns the value to the reflect.Value
101+
func assignInterfaceValueToReflectValue(rt reflect.Type, rv reflect.Value, value interface{}) error {
102+
if rv.CanAddr() {
103+
switch sc := rv.Addr().Interface().(type) {
104+
case sql.Scanner:
105+
return sc.Scan(value)
106+
}
107+
} else {
108+
switch sc := rv.Interface().(type) {
109+
case sql.Scanner:
110+
return sc.Scan(value)
111+
}
112+
}
113+
switch rt.Kind() {
114+
case reflect.String:
115+
str, ok := value.(string)
116+
if !ok {
117+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
118+
fmt.Errorf("incompatible string and %T", value))
119+
}
120+
rv.SetString(str)
121+
case reflect.Int:
122+
f64, ok := value.(float64)
123+
if !ok {
124+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
125+
fmt.Errorf("incompatible int and %T", value))
126+
}
127+
rv.Set(reflect.ValueOf(int(f64)))
128+
case reflect.Bool:
129+
b, ok := value.(bool)
130+
if !ok {
131+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
132+
fmt.Errorf("incompatible bool and %T", value))
133+
}
134+
rv.SetBool(b)
135+
case reflect.Float64:
136+
f64, ok := value.(float64)
137+
if !ok {
138+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
139+
fmt.Errorf("incompatible float64 and %T", value))
140+
}
141+
rv.SetFloat(f64)
142+
case reflect.Slice:
143+
if rt.Elem().Kind() != reflect.Uint8 {
144+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
145+
fmt.Errorf("incompatible []byte and %T", value))
146+
}
147+
b, ok := value.([]byte)
148+
if !ok {
149+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
150+
fmt.Errorf("incompatible []byte and %T", value))
151+
}
152+
rv.SetBytes(b)
153+
case reflect.Struct:
154+
mv, ok := value.(map[string]interface{})
155+
if !ok {
156+
return errors.Join(ErrNestedStructHasIncompatibleAttributes,
157+
fmt.Errorf("incompatible struct and %T", value))
158+
}
159+
err := AssignMapValueToReflectValue(rt, rv, mv)
160+
if err != nil {
161+
return err
162+
}
163+
case reflect.Pointer:
164+
if value == nil {
165+
return nil
166+
}
167+
rv.Set(reflect.New(rt.Elem()))
168+
// NOTE: even return error, it will not be returned to the caller.
169+
// Only expect the attribute to be nil.
170+
assignInterfaceValueToReflectValue(rt.Elem(), rv.Elem(), value)
171+
}
172+
return nil
173+
}
174+
175+
// toAttibuteValue converts the value to a types.AttributeValue
176+
func toAttibuteValue(value interface{}) (types.AttributeValue, error) {
177+
switch value := value.(type) {
178+
case List:
179+
avs := make([]types.AttributeValue, 0, len(value))
180+
for _, v := range value {
181+
av, err := toAttibuteValue(v)
182+
if err != nil {
183+
return nil, err
184+
}
185+
avs = append(avs, av)
186+
}
187+
return &types.AttributeValueMemberL{Value: avs}, nil
188+
case Map:
189+
avm := make(map[string]types.AttributeValue)
190+
for k, v := range value {
191+
av, err := toAttibuteValue(v)
192+
if err != nil {
193+
return nil, err
194+
}
195+
avm[k] = av
196+
}
197+
return &types.AttributeValueMemberM{Value: avm}, nil
198+
case Set[string]:
199+
return &types.AttributeValueMemberSS{Value: value}, nil
200+
case Set[int]:
201+
ss := make([]string, 0, len(value))
202+
for _, v := range value {
203+
ss = append(ss, fmt.Sprintf("%v", v))
204+
}
205+
return &types.AttributeValueMemberNS{Value: ss}, nil
206+
case Set[float64]:
207+
ss := make([]string, 0, len(value))
208+
for _, v := range value {
209+
ss = append(ss, fmt.Sprintf("%v", v))
210+
}
211+
return &types.AttributeValueMemberNS{Value: ss}, nil
212+
case Set[[]byte]:
213+
return &types.AttributeValueMemberBS{Value: value}, nil
214+
default:
215+
rv := reflect.ValueOf(value)
216+
switch rv.Kind() {
217+
case reflect.Struct:
218+
avm := make(map[string]types.AttributeValue)
219+
for i := 0; i < rv.NumField(); i++ {
220+
fv := rv.Field(i)
221+
ft := rv.Type().Field(i)
222+
if fv.CanInterface() {
223+
av, err := toAttibuteValue(fv.Interface())
224+
if err != nil {
225+
return nil, err
226+
}
227+
avm[getColumnNameFromStructField(ft)] = av
228+
} else if fv.CanAddr() {
229+
av, err := toAttibuteValue(fv.Addr().Interface())
230+
if err != nil {
231+
return nil, err
232+
}
233+
avm[getColumnNameFromStructField(ft)] = av
234+
}
235+
}
236+
return &types.AttributeValueMemberM{Value: avm}, nil
237+
case reflect.Ptr:
238+
if rv.IsNil() {
239+
return &types.AttributeValueMemberNULL{}, nil
240+
}
241+
if !rv.CanAddr() {
242+
return attributevalue.Marshal(value)
243+
}
244+
return toAttibuteValue(rv.Addr().Interface())
245+
}
246+
return attributevalue.Marshal(value)
247+
}
248+
}

0 commit comments

Comments
 (0)