|
1 | 1 | package datastore |
2 | 2 |
|
3 | | -import ( |
4 | | - "errors" |
5 | | - "fmt" |
6 | | - "reflect" |
7 | | - "strconv" |
8 | | - "strings" |
9 | | - "time" |
10 | | -) |
11 | | - |
12 | | -// encodeEntity converts a Go struct to a Datastore entity. |
13 | | -func encodeEntity(key *Key, src any) (map[string]any, error) { |
14 | | - v := reflect.ValueOf(src) |
15 | | - if v.Kind() == reflect.Ptr { |
16 | | - v = v.Elem() |
17 | | - } |
18 | | - |
19 | | - if v.Kind() != reflect.Struct { |
20 | | - return nil, errors.New("src must be a struct or pointer to struct") |
21 | | - } |
22 | | - |
23 | | - t := v.Type() |
24 | | - properties := make(map[string]any) |
25 | | - |
26 | | - for i := range v.NumField() { |
27 | | - field := t.Field(i) |
28 | | - value := v.Field(i) |
29 | | - |
30 | | - // Skip unexported fields |
31 | | - if !field.IsExported() { |
32 | | - continue |
33 | | - } |
34 | | - |
35 | | - // Get field name from datastore tag or use field name |
36 | | - name := field.Name |
37 | | - noIndex := false |
38 | | - |
39 | | - if tag := field.Tag.Get("datastore"); tag != "" { |
40 | | - parts := strings.Split(tag, ",") |
41 | | - if parts[0] != "" && parts[0] != "-" { |
42 | | - name = parts[0] |
43 | | - } |
44 | | - if len(parts) > 1 && parts[1] == "noindex" { |
45 | | - noIndex = true |
46 | | - } |
47 | | - if parts[0] == "-" { |
48 | | - continue |
49 | | - } |
50 | | - } |
51 | | - |
52 | | - prop, err := encodeValue(value.Interface()) |
53 | | - if err != nil { |
54 | | - return nil, fmt.Errorf("field %s: %w", field.Name, err) |
55 | | - } |
56 | | - |
57 | | - if noIndex { |
58 | | - if m, ok := prop.(map[string]any); ok { |
59 | | - m["excludeFromIndexes"] = true |
60 | | - } |
61 | | - } |
62 | | - |
63 | | - properties[name] = prop |
64 | | - } |
65 | | - |
66 | | - return map[string]any{ |
67 | | - "key": keyToJSON(key), |
68 | | - "properties": properties, |
69 | | - }, nil |
70 | | -} |
71 | | - |
72 | | -// encodeValue converts a Go value to a Datastore property value. |
73 | | -func encodeValue(v any) (any, error) { |
74 | | - if v == nil { |
75 | | - return map[string]any{"nullValue": nil}, nil |
76 | | - } |
77 | | - |
78 | | - switch val := v.(type) { |
79 | | - case string: |
80 | | - return map[string]any{"stringValue": val}, nil |
81 | | - case int: |
82 | | - return map[string]any{"integerValue": strconv.Itoa(val)}, nil |
83 | | - case int64: |
84 | | - return map[string]any{"integerValue": strconv.FormatInt(val, 10)}, nil |
85 | | - case int32: |
86 | | - return map[string]any{"integerValue": strconv.Itoa(int(val))}, nil |
87 | | - case bool: |
88 | | - return map[string]any{"booleanValue": val}, nil |
89 | | - case float64: |
90 | | - return map[string]any{"doubleValue": val}, nil |
91 | | - case time.Time: |
92 | | - return map[string]any{"timestampValue": val.Format(time.RFC3339Nano)}, nil |
93 | | - case []string: |
94 | | - values := make([]map[string]any, len(val)) |
95 | | - for i, s := range val { |
96 | | - values[i] = map[string]any{"stringValue": s} |
97 | | - } |
98 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
99 | | - case []int64: |
100 | | - values := make([]map[string]any, len(val)) |
101 | | - for i, n := range val { |
102 | | - values[i] = map[string]any{"integerValue": strconv.FormatInt(n, 10)} |
103 | | - } |
104 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
105 | | - case []int: |
106 | | - values := make([]map[string]any, len(val)) |
107 | | - for i, n := range val { |
108 | | - values[i] = map[string]any{"integerValue": strconv.Itoa(n)} |
109 | | - } |
110 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
111 | | - case []float64: |
112 | | - values := make([]map[string]any, len(val)) |
113 | | - for i, f := range val { |
114 | | - values[i] = map[string]any{"doubleValue": f} |
115 | | - } |
116 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
117 | | - case []bool: |
118 | | - values := make([]map[string]any, len(val)) |
119 | | - for i, b := range val { |
120 | | - values[i] = map[string]any{"booleanValue": b} |
121 | | - } |
122 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
123 | | - default: |
124 | | - // Try to handle slices/arrays via reflection |
125 | | - rv := reflect.ValueOf(v) |
126 | | - if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { |
127 | | - length := rv.Len() |
128 | | - values := make([]map[string]any, length) |
129 | | - for i := range length { |
130 | | - elem := rv.Index(i).Interface() |
131 | | - encodedElem, err := encodeValue(elem) |
132 | | - if err != nil { |
133 | | - return nil, fmt.Errorf("failed to encode array element %d: %w", i, err) |
134 | | - } |
135 | | - // encodedElem is already a map[string]any with the type wrapper |
136 | | - m, ok := encodedElem.(map[string]any) |
137 | | - if !ok { |
138 | | - return nil, fmt.Errorf("unexpected encoded value type for element %d", i) |
139 | | - } |
140 | | - values[i] = m |
141 | | - } |
142 | | - return map[string]any{"arrayValue": map[string]any{"values": values}}, nil |
143 | | - } |
144 | | - return nil, fmt.Errorf("unsupported type: %T", v) |
145 | | - } |
146 | | -} |
147 | | - |
148 | | -// decodeEntity converts a Datastore entity to a Go struct. |
149 | | -func decodeEntity(entity map[string]any, dst any) error { |
150 | | - v := reflect.ValueOf(dst) |
151 | | - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { |
152 | | - return errors.New("dst must be a pointer to struct") |
153 | | - } |
154 | | - |
155 | | - v = v.Elem() |
156 | | - t := v.Type() |
157 | | - |
158 | | - properties, ok := entity["properties"].(map[string]any) |
159 | | - if !ok { |
160 | | - return errors.New("invalid entity format") |
161 | | - } |
162 | | - |
163 | | - for i := range v.NumField() { |
164 | | - field := t.Field(i) |
165 | | - value := v.Field(i) |
166 | | - |
167 | | - if !field.IsExported() { |
168 | | - continue |
169 | | - } |
170 | | - |
171 | | - // Get field name from datastore tag |
172 | | - name := field.Name |
173 | | - if tag := field.Tag.Get("datastore"); tag != "" { |
174 | | - parts := strings.Split(tag, ",") |
175 | | - if parts[0] != "" && parts[0] != "-" { |
176 | | - name = parts[0] |
177 | | - } |
178 | | - if parts[0] == "-" { |
179 | | - continue |
180 | | - } |
181 | | - } |
| 3 | +import "errors" |
182 | 4 |
|
183 | | - prop, ok := properties[name] |
184 | | - if !ok { |
185 | | - continue // Field not in entity |
186 | | - } |
187 | | - |
188 | | - propMap, ok := prop.(map[string]any) |
189 | | - if !ok { |
190 | | - continue |
191 | | - } |
192 | | - |
193 | | - if err := decodeValue(propMap, value); err != nil { |
194 | | - return fmt.Errorf("field %s: %w", field.Name, err) |
195 | | - } |
196 | | - } |
197 | | - |
198 | | - return nil |
199 | | -} |
200 | | - |
201 | | -// decodeValue decodes a Datastore property value into a Go reflect.Value. |
202 | | -func decodeValue(prop map[string]any, dst reflect.Value) error { |
203 | | - // Handle each type |
204 | | - if val, ok := prop["stringValue"]; ok { |
205 | | - if dst.Kind() == reflect.String { |
206 | | - if s, ok := val.(string); ok { |
207 | | - dst.SetString(s) |
208 | | - return nil |
209 | | - } |
210 | | - } |
211 | | - } |
212 | | - |
213 | | - if val, ok := prop["integerValue"]; ok { |
214 | | - var intVal int64 |
215 | | - switch v := val.(type) { |
216 | | - case string: |
217 | | - if _, err := fmt.Sscanf(v, "%d", &intVal); err != nil { |
218 | | - return fmt.Errorf("invalid integer format: %w", err) |
219 | | - } |
220 | | - case float64: |
221 | | - intVal = int64(v) |
222 | | - } |
223 | | - |
224 | | - switch dst.Kind() { |
225 | | - case reflect.Int, reflect.Int64, reflect.Int32: |
226 | | - dst.SetInt(intVal) |
227 | | - return nil |
228 | | - default: |
229 | | - return fmt.Errorf("unsupported integer type: %v", dst.Kind()) |
230 | | - } |
231 | | - } |
232 | | - |
233 | | - if val, ok := prop["booleanValue"]; ok { |
234 | | - if dst.Kind() == reflect.Bool { |
235 | | - if b, ok := val.(bool); ok { |
236 | | - dst.SetBool(b) |
237 | | - return nil |
238 | | - } |
239 | | - } |
240 | | - } |
241 | | - |
242 | | - if val, ok := prop["doubleValue"]; ok { |
243 | | - if dst.Kind() == reflect.Float64 { |
244 | | - if f, ok := val.(float64); ok { |
245 | | - dst.SetFloat(f) |
246 | | - return nil |
247 | | - } |
248 | | - } |
249 | | - } |
250 | | - |
251 | | - if val, ok := prop["timestampValue"]; ok { |
252 | | - if dst.Type() == reflect.TypeOf(time.Time{}) { |
253 | | - if s, ok := val.(string); ok { |
254 | | - t, err := time.Parse(time.RFC3339Nano, s) |
255 | | - if err != nil { |
256 | | - return err |
257 | | - } |
258 | | - dst.Set(reflect.ValueOf(t)) |
259 | | - return nil |
260 | | - } |
261 | | - } |
262 | | - } |
263 | | - |
264 | | - if val, ok := prop["arrayValue"]; ok { |
265 | | - if dst.Kind() != reflect.Slice { |
266 | | - return fmt.Errorf("cannot decode array into non-slice type: %s", dst.Type()) |
267 | | - } |
268 | | - |
269 | | - arrayMap, ok := val.(map[string]any) |
270 | | - if !ok { |
271 | | - return errors.New("invalid arrayValue format") |
272 | | - } |
273 | | - |
274 | | - valuesAny, ok := arrayMap["values"] |
275 | | - if !ok { |
276 | | - // Empty array |
277 | | - dst.Set(reflect.MakeSlice(dst.Type(), 0, 0)) |
278 | | - return nil |
279 | | - } |
280 | | - |
281 | | - values, ok := valuesAny.([]any) |
282 | | - if !ok { |
283 | | - return errors.New("invalid arrayValue.values format") |
284 | | - } |
285 | | - |
286 | | - // Create slice with appropriate capacity |
287 | | - slice := reflect.MakeSlice(dst.Type(), len(values), len(values)) |
288 | | - |
289 | | - // Decode each element |
290 | | - for i, elemAny := range values { |
291 | | - elemMap, ok := elemAny.(map[string]any) |
292 | | - if !ok { |
293 | | - return fmt.Errorf("invalid array element %d format", i) |
294 | | - } |
295 | | - |
296 | | - elemValue := slice.Index(i) |
297 | | - if err := decodeValue(elemMap, elemValue); err != nil { |
298 | | - return fmt.Errorf("failed to decode array element %d: %w", i, err) |
299 | | - } |
300 | | - } |
301 | | - |
302 | | - dst.Set(slice) |
303 | | - return nil |
304 | | - } |
305 | | - |
306 | | - if _, ok := prop["nullValue"]; ok { |
307 | | - // Set to zero value |
308 | | - dst.Set(reflect.Zero(dst.Type())) |
309 | | - return nil |
310 | | - } |
311 | | - |
312 | | - return fmt.Errorf("unsupported property type for %s", dst.Type()) |
313 | | -} |
| 5 | +// Entity encoding/decoding errors. |
| 6 | +var ( |
| 7 | + errNotStruct = errors.New("src must be a struct or pointer to struct") |
| 8 | + errNotStructPtr = errors.New("dst must be a pointer to struct") |
| 9 | + errInvalidEntity = errors.New("invalid entity format") |
| 10 | +) |
0 commit comments