@@ -41,6 +41,10 @@ const (
4141var (
4242 // ErrKeyNotFound indicates that the specified key was not found.
4343 ErrKeyNotFound = errors .New ("key not found" )
44+ // ErrKeyCollision indicates that during the case-insensitive search multiple keys matched
45+ ErrKeyCollision = errors .New ("key collision" )
46+ // ErrNotMapType indicates that the given value is not map type
47+ ErrNotMapType = errors .New ("value is not a map" )
4448)
4549
4650// EventMetadata contains fields and tags that can be added to an event via
@@ -172,6 +176,62 @@ func (m M) HasKey(key string) (bool, error) {
172176 return hasKey , err
173177}
174178
179+ // FindFold accepts a key and traverses the map trying to match every key segment
180+ // using `strings.FindFold` (case-insensitive match) and returns the actual
181+ // key of the map that matched the given key and the value stored under this key.
182+ // Returns `ErrKeyCollision` if multiple keys match the same request.
183+ // Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
184+ func (m M ) FindFold (key string ) (matchedKey string , value interface {}, err error ) {
185+ path := strings .Split (key , "." )
186+ // the initial value must be `true` for the first iteration to work
187+ found := true
188+ // start with the root
189+ current := m
190+ // allocate only once
191+ var mapType bool
192+
193+ for i , segment := range path {
194+ if ! found {
195+ return "" , nil , ErrKeyNotFound
196+ }
197+ found = false
198+
199+ // we have to go through the list of all key on each level to detect case-insensitive collisions
200+ for k := range current {
201+ if ! strings .EqualFold (segment , k ) {
202+ continue
203+ }
204+ // if already found on this level, it's a collision
205+ if found {
206+ return "" , nil , fmt .Errorf ("key collision on the same path %q, previous match - %q, another subkey - %q: %w" , key , matchedKey , k , ErrKeyCollision )
207+ }
208+
209+ // mark for collision detection
210+ found = true
211+
212+ // build the result with the currently matched segment
213+ matchedKey += k
214+ value = current [k ]
215+
216+ // if it's the last segment, we don't need to go deeper
217+ if i == len (path )- 1 {
218+ continue
219+ }
220+
221+ // if it's not the last segment we put the separator dot
222+ matchedKey += "."
223+
224+ // go one level deeper
225+ current , mapType = tryToMapStr (current [k ])
226+ if ! mapType {
227+ return "" , nil , fmt .Errorf ("cannot continue path %q (full: %q), next element is not a map: %w" , matchedKey , key , ErrNotMapType )
228+ }
229+ }
230+ }
231+
232+ return matchedKey , value , nil
233+ }
234+
175235// GetValue gets a value from the map. If the key does not exist then an error
176236// is returned.
177237func (m M ) GetValue (key string ) (interface {}, error ) {
@@ -266,10 +326,12 @@ func (m M) Format(f fmt.State, c rune) {
266326// Flatten flattens the given M and returns a flat M.
267327//
268328// Example:
269- // "hello": M{"world": "test" }
329+ //
330+ // "hello": M{"world": "test" }
270331//
271332// This is converted to:
272- // "hello.world": "test"
333+ //
334+ // "hello.world": "test"
273335//
274336// This can be useful for testing or logging.
275337func (m M ) Flatten () M {
@@ -299,10 +361,12 @@ func flatten(prefix string, in, out M) M {
299361// FlattenKeys flattens given MapStr keys and returns a containing array pointer
300362//
301363// Example:
302- // "hello": MapStr{"world": "test" }
364+ //
365+ // "hello": MapStr{"world": "test" }
303366//
304367// This is converted to:
305- // ["hello.world"]
368+ //
369+ // ["hello.world"]
306370func (m M ) FlattenKeys () * []string {
307371 out := make ([]string , 0 )
308372 flattenKeys ("" , m , & out )
0 commit comments