-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjsptr.go
More file actions
480 lines (415 loc) · 11.5 KB
/
Copy pathjsptr.go
File metadata and controls
480 lines (415 loc) · 11.5 KB
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
package jsptr
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/lestrrat-go/blackmagic"
"github.com/valyala/fastjson"
)
// Source is an interface for abstracting different data sources
type Source interface {
RetrieveJSONPointer(dst any, ptrspec string) error
}
// Pointer represents a compiled JSON pointer
type Pointer struct {
pattern string
tokens []string
}
// New creates a new JSON pointer from a path specification
func New(pathspec string) (*Pointer, error) {
if pathspec == "" {
return &Pointer{pattern: "", tokens: nil}, nil
}
if !strings.HasPrefix(pathspec, "/") {
return nil, fmt.Errorf("JSON pointer must start with '/'")
}
// Split the path into tokens, skipping the empty first element
parts := strings.Split(pathspec, "/")[1:]
// Unescape each token
tokens := make([]string, len(parts))
for i, part := range parts {
tokens[i] = unescapeToken(part)
}
return &Pointer{
pattern: pathspec,
tokens: tokens,
}, nil
}
// Pattern returns the original path specification
func (p *Pointer) Pattern() string {
return p.pattern
}
// Retrieve retrieves the value at the JSON pointer location
func (p *Pointer) Retrieve(dst any, target any) error {
// Create appropriate source based on target type
source, err := createSource(target)
if err != nil {
return err
}
return source.RetrieveJSONPointer(dst, p.pattern)
}
// unescapeToken unescapes JSON pointer tokens
func unescapeToken(token string) string {
// JSON pointer escaping: ~1 -> /, ~0 -> ~
token = strings.ReplaceAll(token, "~1", "/")
token = strings.ReplaceAll(token, "~0", "~")
return token
}
// createSource creates an appropriate source for the given target
func createSource(target any) (Source, error) {
// First check if target already implements Source interface
if source, ok := target.(Source); ok {
return source, nil
}
// Use reflection to properly detect types
rv := reflect.ValueOf(target)
if !rv.IsValid() {
return scalarSource{data: target}, nil
}
// Handle specific types first
switch v := target.(type) {
case []byte:
return createJSONSource(v)
case string:
return createJSONSource([]byte(v))
case map[string]any:
return mapSource{data: v}, nil
}
// Use reflection for more general type checking
switch rv.Kind() {
case reflect.Slice, reflect.Array:
// Convert to []any for uniform handling
length := rv.Len()
slice := make([]any, length)
for i := range length {
slice[i] = rv.Index(i).Interface()
}
return sliceSource{data: slice}, nil
case reflect.Map:
// Only handle string-keyed maps
if rv.Type().Key().Kind() == reflect.String {
// Convert to map[string]any for uniform handling
result := make(map[string]any)
for _, key := range rv.MapKeys() {
keyStr := key.String()
value := rv.MapIndex(key).Interface()
result[keyStr] = value
}
return mapSource{data: result}, nil
}
// Non-string-keyed maps cannot be accessed with JSON pointer
return nil, fmt.Errorf("cannot use JSON pointer with non-string-keyed map type %s", rv.Type())
case reflect.Struct:
return structSource{data: target}, nil
case reflect.Ptr:
// For pointers, recurse with the pointed-to value
if rv.IsNil() {
return scalarSource{data: target}, nil
}
return createSource(rv.Elem().Interface())
default:
// Scalars (int, bool, float64, etc.)
return scalarSource{data: target}, nil
}
}
// createJSONSource creates a jsonSource with pre-parsed JSON data
func createJSONSource(data []byte) (Source, error) {
var p fastjson.Parser
parsed, err := p.ParseBytes(data)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return jsonSource{data: data, parsed: parsed}, nil
}
// scalarSource handles scalar values (int, bool, float64, etc.)
type scalarSource struct {
data any
}
func (s scalarSource) RetrieveJSONPointer(dst any, ptrspec string) error {
// Scalars can only be retrieved with empty pointer
if ptrspec != "" {
return fmt.Errorf("cannot index into scalar value %T with pointer '%s'", s.data, ptrspec)
}
return blackmagic.AssignIfCompatible(dst, s.data)
}
// jsonSource handles JSON byte data
type jsonSource struct {
data []byte
parsed *fastjson.Value
}
func (s jsonSource) RetrieveJSONPointer(dst any, ptrspec string) error {
// Use cached parsed JSON data
v := s.parsed
// Handle empty pointer - return the parsed data directly
if ptrspec == "" {
return s.assignFromValue(dst, v)
}
// Parse the pointer and navigate to the value
ptr, err := New(ptrspec)
if err != nil {
return err
}
// Navigate through the JSON using the pointer tokens
current := v
for _, token := range ptr.tokens {
switch current.Type() {
case fastjson.TypeObject:
current = current.Get(token)
if current == nil {
return fmt.Errorf("property '%s' not found", token)
}
case fastjson.TypeArray:
index, err := strconv.Atoi(token)
if err != nil {
return fmt.Errorf("invalid array index '%s'", token)
}
arr, err := current.Array()
if err != nil {
return fmt.Errorf("failed to get array: %w", err)
}
if index < 0 || index >= len(arr) {
return fmt.Errorf("array index %d out of bounds", index)
}
current = arr[index]
default:
return fmt.Errorf("cannot index into %s with '%s'", current.Type(), token)
}
}
return s.assignFromValue(dst, current)
}
// assignFromValue converts a fastjson.Value to a Go value and assigns it to dst
func (s jsonSource) assignFromValue(dst any, v *fastjson.Value) error {
if v == nil {
return blackmagic.AssignIfCompatible(dst, nil)
}
switch v.Type() {
case fastjson.TypeNull:
return blackmagic.AssignIfCompatible(dst, nil)
case fastjson.TypeString:
str, err := v.StringBytes()
if err != nil {
return fmt.Errorf("failed to get string value: %w", err)
}
return blackmagic.AssignIfCompatible(dst, string(str))
case fastjson.TypeNumber:
return blackmagic.AssignIfCompatible(dst, v.GetFloat64())
case fastjson.TypeTrue:
return blackmagic.AssignIfCompatible(dst, true)
case fastjson.TypeFalse:
return blackmagic.AssignIfCompatible(dst, false)
case fastjson.TypeArray:
arr, err := v.Array()
if err != nil {
return fmt.Errorf("failed to get array: %w", err)
}
result := make([]any, len(arr))
for i, item := range arr {
var temp any
if err := s.assignFromValue(&temp, item); err != nil {
return fmt.Errorf("failed to convert array item %d: %w", i, err)
}
result[i] = temp
}
return blackmagic.AssignIfCompatible(dst, result)
case fastjson.TypeObject:
obj, err := v.Object()
if err != nil {
return fmt.Errorf("failed to get object: %w", err)
}
result := make(map[string]any)
obj.Visit(func(key []byte, val *fastjson.Value) {
var temp any
if err := s.assignFromValue(&temp, val); err == nil {
result[string(key)] = temp
}
})
return blackmagic.AssignIfCompatible(dst, result)
default:
return fmt.Errorf("unsupported JSON type: %s", v.Type())
}
}
// mapSource handles map[string]any data
type mapSource struct {
data map[string]any
}
func (s mapSource) RetrieveJSONPointer(dst any, ptrspec string) error {
// Handle empty pointer - return the data directly
if ptrspec == "" {
return blackmagic.AssignIfCompatible(dst, s.data)
}
ptr, err := New(ptrspec)
if err != nil {
return err
}
current := any(s.data)
for _, token := range ptr.tokens {
switch curr := current.(type) {
case map[string]any:
val, exists := curr[token]
if !exists {
return fmt.Errorf("property '%s' not found", token)
}
current = val
case []any:
index, err := strconv.Atoi(token)
if err != nil {
return fmt.Errorf("invalid array index '%s'", token)
}
if index < 0 || index >= len(curr) {
return fmt.Errorf("array index %d out of bounds", index)
}
current = curr[index]
default:
return fmt.Errorf("cannot index into %T with '%s'", current, token)
}
}
return blackmagic.AssignIfCompatible(dst, current)
}
// sliceSource handles []any data
type sliceSource struct {
data []any
}
func (s sliceSource) RetrieveJSONPointer(dst any, ptrspec string) error {
// Handle empty pointer - return the data directly
if ptrspec == "" {
return blackmagic.AssignIfCompatible(dst, s.data)
}
ptr, err := New(ptrspec)
if err != nil {
return err
}
// First token must be an array index
index, err := strconv.Atoi(ptr.tokens[0])
if err != nil {
return fmt.Errorf("invalid array index '%s'", ptr.tokens[0])
}
if index < 0 || index >= len(s.data) {
return fmt.Errorf("array index %d out of bounds", index)
}
// If only one token, return the element
if len(ptr.tokens) == 1 {
return blackmagic.AssignIfCompatible(dst, s.data[index])
}
// Create new pointer for remaining tokens
remainingPath := "/" + strings.Join(ptr.tokens[1:], "/")
source, err := createSource(s.data[index])
if err != nil {
return err
}
return source.RetrieveJSONPointer(dst, remainingPath)
}
// structSource handles struct data with JSON tag caching
type structSource struct {
data any
}
// Cache for struct field information
var (
structCache = make(map[reflect.Type]*structInfo)
cacheMutex sync.RWMutex
)
type structInfo struct {
fields map[string]*fieldInfo
}
type fieldInfo struct {
index []int
jsonName string
}
func (s structSource) RetrieveJSONPointer(dst any, ptrspec string) error {
// Handle empty pointer - return the data directly
if ptrspec == "" {
return blackmagic.AssignIfCompatible(dst, s.data)
}
ptr, err := New(ptrspec)
if err != nil {
return err
}
current := s.data
for _, token := range ptr.tokens {
current, err = s.getField(current, token)
if err != nil {
return err
}
}
return blackmagic.AssignIfCompatible(dst, current)
}
func (s structSource) getField(obj any, fieldName string) (any, error) {
val := reflect.ValueOf(obj)
// Handle pointers
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, fmt.Errorf("cannot access field of nil pointer")
}
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot access field '%s' of non-struct type %T", fieldName, obj)
}
info := getStructInfo(val.Type())
fieldInfo, exists := info.fields[fieldName]
if !exists {
return nil, fmt.Errorf("field '%s' not found in struct %T", fieldName, obj)
}
fieldVal := val.FieldByIndex(fieldInfo.index)
return fieldVal.Interface(), nil
}
func getStructInfo(t reflect.Type) *structInfo {
cacheMutex.RLock()
if info, exists := structCache[t]; exists {
cacheMutex.RUnlock()
return info
}
cacheMutex.RUnlock()
cacheMutex.Lock()
defer cacheMutex.Unlock()
// Double-check after acquiring write lock
if info, exists := structCache[t]; exists {
return info
}
info := &structInfo{
fields: make(map[string]*fieldInfo),
}
// Process all fields, including embedded ones
processFields(t, nil, info)
structCache[t] = info
return info
}
func processFields(t reflect.Type, index []int, info *structInfo) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldIndex := append(index, i)
// Handle embedded fields
if field.Anonymous {
fieldType := field.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
if fieldType.Kind() == reflect.Struct {
processFields(fieldType, fieldIndex, info)
}
continue
}
// Skip unexported fields
if !field.IsExported() {
continue
}
// Get JSON tag
jsonTag := field.Tag.Get("json")
if jsonTag == "-" {
continue
}
// Parse JSON tag
jsonName := field.Name
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" {
jsonName = parts[0]
}
}
info.fields[jsonName] = &fieldInfo{
index: fieldIndex,
jsonName: jsonName,
}
}
}