@@ -181,55 +181,145 @@ func (m M) HasKey(key string) (bool, error) {
181181// key of the map that matched the given key and the value stored under this key.
182182// Returns `ErrKeyCollision` if multiple keys match the same request.
183183// 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 , "." )
184+ // Returns `ErrKeyNotFound` when the path does not exist
185+ func (m M ) FindFold (path string ) (matchedKey string , value interface {}, err error ) {
186+ segmentCount := strings .Count (path , "." ) + 1
187+ err = m .Traverse (path , CaseInsensitiveMode , func (level M , key string ) error {
188+ segmentCount --
189+ matchedKey += key
190+ if segmentCount != 0 {
191+ matchedKey += "."
192+ return nil
193+ }
194+
195+ value = level [key ]
196+ return nil
197+ })
198+ if err != nil {
199+ return "" , nil , err
200+ }
201+ return matchedKey , value , nil
202+ }
203+
204+ type AlterFunc func (string ) (string , error )
205+
206+ // AlterPath walks the given `path` and replaces matching keys using the value returned by `alterFunc`.
207+ // `mode` sets the behavior how the given path is matched throughout the levels.
208+ // Returns `ErrKeyCollision` if multiple keys match the same request (when `mode` is `CaseInsensitiveMode`).
209+ // Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
210+ // Returns `ErrKeyNotFound` when the path does not exist
211+ func (m M ) AlterPath (path string , mode TraversalMode , alterFunc AlterFunc ) (err error ) {
212+ return m .Traverse (path , mode , func (level M , key string ) error {
213+ val := level [key ]
214+ newKey , err := alterFunc (key )
215+ if err != nil {
216+ return fmt .Errorf ("failed to apply a change to %q: %w" , key , err )
217+ }
218+ if newKey == "" {
219+ return fmt .Errorf ("replacement key for %q cannot be empty" , key )
220+ }
221+ _ , exists := level [newKey ]
222+ if exists {
223+ return fmt .Errorf ("replacement key %q already exists: %w" , newKey , ErrKeyCollision )
224+ }
225+ delete (level , key )
226+ level [newKey ] = val
227+
228+ return nil
229+ })
230+ }
231+
232+ // TraversalMode used for traversing the map through multiple levels.
233+ type TraversalMode int
234+
235+ const (
236+ // The key match is strictly case-sensitive
237+ CaseSensitiveMode = iota
238+ // The key match is performed with `strings.EqualFold`
239+ CaseInsensitiveMode = iota
240+ )
241+
242+ type TraversalVisitor func (M , string ) error
243+
244+ // Traverse walks the given nested `path` in the map and invokes the `visitor` function on each level passing
245+ // the current-level map and the current key.
246+ // `mode` sets the behavior how the given path is matched throughout the levels.
247+ // The `visitor` function is allowed to make changes in the level or collect data.
248+ // Returns `ErrKeyCollision` if multiple keys match the same request (when `mode` is `CaseInsensitiveMode`).
249+ // Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
250+ // Returns `ErrKeyNotFound` when the path does not exist
251+ func (m M ) Traverse (path string , mode TraversalMode , visitor TraversalVisitor ) (err error ) {
252+ segments := strings .Split (path , "." )
253+ var match func (string , string ) bool
254+
255+ switch mode {
256+ case CaseInsensitiveMode :
257+ match = strings .EqualFold
258+ case CaseSensitiveMode :
259+ match = func (a , b string ) bool { return a == b }
260+ }
261+
186262 // the initial value must be `true` for the first iteration to work
187263 found := true
188264 // start with the root
189265 current := m
190266 // allocate only once
191- var mapType bool
267+ var (
268+ mapType bool
269+ next interface {}
270+ )
192271
193- for i , segment := range path {
272+ for i , segment := range segments {
194273 if ! found {
195- return "" , nil , ErrKeyNotFound
274+ return ErrKeyNotFound
196275 }
197276 found = false
198277
199278 // we have to go through the list of all key on each level to detect case-insensitive collisions
200279 for k := range current {
201- if ! strings . EqualFold (segment , k ) {
280+ if ! match (segment , k ) {
202281 continue
203282 }
283+
204284 // if already found on this level, it's a collision
205285 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 )
286+ return fmt .Errorf ("multiple keys match %q on the same level of the path %q: %w" , k , path , ErrKeyCollision )
207287 }
208288
209289 // mark for collision detection
210290 found = true
211291
212- // build the result with the currently matched segment
213- matchedKey += k
214- value = current [k ]
292+ // we need to save this in case the visitor makes changes in keys
293+ next = current [k ]
294+ err = visitor (current , k )
295+ if err != nil {
296+ return fmt .Errorf ("error visiting key %q of the path %q: %w" , k , path , err )
297+ }
215298
216- // if it's the last segment, we don't need to go deeper
217- if i == len (path )- 1 {
299+ // if it's the last segment, we don't need to go deeper, skipping...
300+ if i == len (segments )- 1 {
218301 continue
219302 }
220303
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 ])
304+ // try to go one level deeper
305+ current , mapType = tryToMapStr (next )
226306 if ! mapType {
227- return "" , nil , fmt .Errorf ("cannot continue path %q (full: %q), next element is not a map: %w" , matchedKey , key , ErrNotMapType )
307+ return fmt .Errorf ("cannot continue path %q, next value %q is not a map: %w" , path , k , ErrNotMapType )
308+ }
309+
310+ // if it's a case-sensitive key match, we don't have to care about collision detection
311+ // and we can simply stop iterating here.
312+ if mode == CaseSensitiveMode {
313+ break
228314 }
229315 }
230316 }
231317
232- return matchedKey , value , nil
318+ if ! found {
319+ return ErrKeyNotFound
320+ }
321+
322+ return nil
233323}
234324
235325// GetValue gets a value from the map. If the key does not exist then an error
0 commit comments