-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdig.go
More file actions
182 lines (177 loc) · 7.21 KB
/
dig.go
File metadata and controls
182 lines (177 loc) · 7.21 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
// Package dig provides safe nested data navigation for Go.
//
// It replaces chains of .(map[string]any) type assertions with a single
// function call. Every operation is nil-safe, never panics, and never
// modifies its input. All functions are safe for concurrent use, provided
// the caller does not mutate the input concurrently.
//
// Aliasing: when a returned value is a slice or map, it is the same
// underlying container stored in the input — not a copy. Mutating the
// returned container (e.g. appending to a returned []any, or writing
// keys into a returned map[string]any) also mutates the source. dig
// itself never modifies its input; isolation from caller mutation is
// the caller's responsibility. Copy the returned container if you need
// an independent value.
//
// A nil data argument with a non-empty path always fails: there is
// nothing to navigate into, so Dig returns (zero, false) and Has
// returns false. With an empty path, nil data is treated as a nil leaf:
// Dig[any](nil) and At(nil) return (nil, true), and Has(nil) returns
// true. This keeps the "nil leaf is a real value for interface T" rule
// consistent whether the nil sits at the top of the tree or at a leaf.
//
// For documentation and examples, see https://github.com/bold-minds/dig.
package dig
// Dig extracts a value of type T at the given path from nested data.
// It returns (zero, false) on any failure: a missing path element, a
// wrong intermediate type, an out-of-bounds index, or a leaf value whose
// type is not exactly T.
//
// Dig uses strict type matching at the leaf — no automatic conversion.
// For type coercion, chain with bold-minds/to:
//
// raw, _ := dig.At(data, "user", "age")
// age := to.Int(raw)
//
// A literal nil leaf value is treated as a successful result when T is
// an interface type (such as any): Dig returns (nil, true). For concrete
// T, a nil leaf returns (zero, false). Note that a typed nil pointer
// (e.g. (*Foo)(nil) stored in an any) is not a literal nil and matches
// T=*Foo normally, returning (nil, true).
//
// Supported source types along the path:
// - map[string]any (string keys)
// - map[any]any (see key whitelist below)
// - []any (non-negative int indices)
//
// For map[any]any, the path key must be one of the hashable primitive
// types commonly produced by JSON/YAML/CBOR/MessagePack unmarshalling:
// string, bool, all signed integer widths (int, int8, int16, int32,
// int64), all unsigned integer widths (uint, uint8, uint16, uint32,
// uint64), and the floating-point widths (float32, float64). Other
// key types — uintptr, complex64, complex128, structs, arrays, and
// pointers — are rejected with (zero, false) rather than used as a
// lookup key, even when the map literally contains such a key. This
// whitelist exists so that an unhashable path value (e.g. a slice)
// cannot trigger a runtime panic from Go's map lookup, preserving the
// never-panic guarantee without reflection. The narrow integer widths
// are included because decoders like CBOR and MessagePack routinely
// produce map[any]any with uint8 or int8 keys, and excluding them
// would be a foot-gun that the hashability rationale does not require.
//
// Typed containers like map[string]string or []string are not walked;
// use encoding/json or similar to unmarshal into any first.
func Dig[T any](data any, path ...any) (T, bool) {
var zero T
current, ok := walk(data, path)
if !ok {
return zero, false
}
// A nil leaf is a valid value when T is an interface type.
// any(zero) == nil is true exactly when T is an interface type: the
// zero value of a concrete type boxed in any carries a non-nil type
// descriptor, while the zero value of an interface type boxed in any
// is the nil interface itself. This lets us branch on "is T an
// interface?" at runtime without reflection.
if current == nil {
if any(zero) == nil {
return zero, true
}
return zero, false
}
// Non-nil leaves fall through to the generic type assertion, which
// handles interface T via the runtime's itab check: if T is an
// interface type and current satisfies it, the assertion succeeds;
// otherwise ok is false and we return (zero, false).
result, ok := current.(T)
if !ok {
return zero, false
}
return result, true
}
// DigOr extracts a value of type T at the given path, returning fallback
// on any failure. Equivalent to Dig[T] with the (zero, false) case
// replaced by the fallback value.
func DigOr[T any](data any, fallback T, path ...any) T {
if v, ok := Dig[T](data, path...); ok {
return v
}
return fallback
}
// Has reports whether the given path resolves to a value in data,
// regardless of that value's type. Returns false for nil data, missing
// keys, wrong intermediate types, and out-of-bounds indices.
func Has(data any, path ...any) bool {
_, ok := walk(data, path)
return ok
}
// At returns the raw value at the given path without type matching.
// Use when you need to inspect a value's actual type before handling it.
// Equivalent to Dig[any] but named for the outcome. A literal nil leaf
// returns (nil, true) — matching Dig[any] and Has.
func At(data any, path ...any) (any, bool) {
return walk(data, path)
}
// walk navigates a path through nested data structures. It returns the
// final value and true on success, or (nil, false) on any navigation
// failure: missing key, wrong intermediate type, or out-of-bounds index.
// Nil data with an empty path returns (nil, true) — the nil leaf case.
//
// Path element types are matched against the current node type:
// string keys navigate maps, non-negative int indices navigate slices.
func walk(data any, path []any) (any, bool) {
current := data
for _, key := range path {
// A nil intermediate has no children; any further navigation
// must fail. This also handles the nil-data + non-empty-path
// case: we enter the loop with current == nil and immediately
// fall through to the default branch below. With an empty
// path, the loop is skipped entirely and nil data is returned
// as a nil leaf (which Dig then handles per the T rules).
switch v := current.(type) {
case map[string]any:
strKey, ok := key.(string)
if !ok {
return nil, false
}
val, exists := v[strKey]
if !exists {
return nil, false
}
current = val
case map[any]any:
// Restrict keys to a whitelist of hashable primitive types.
// Using an arbitrary value as a map key would panic at runtime
// if the caller passed an unhashable value (e.g., a slice).
// This preserves the "never panics" guarantee without reflection.
//
// This list is the source of truth for the whitelist. Any
// change here must be mirrored in the package doc on Dig,
// TestDig_MapAnyAny_WhitelistedKeyTypes, and
// TestDig_MapAnyAny_NonWhitelistedKeyTypes — the positive and
// negative twin tests lock this boundary in place.
switch key.(type) {
case string, bool,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64:
default:
return nil, false
}
val, exists := v[key]
if !exists {
return nil, false
}
current = val
case []any:
idx, ok := key.(int)
if !ok || idx < 0 || idx >= len(v) {
return nil, false
}
current = v[idx]
default:
return nil, false
}
}
return current, true
}