-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(config): support dynamic map keys with dots #8096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
05232af
0a2336e
732adeb
660cc66
6d295eb
e1cbee7
36b62c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,26 +2,62 @@ package common | |
|
|
||
| import ( | ||
| "fmt" | ||
| "regexp" | ||
| "strings" | ||
|
|
||
| "github.com/google/uuid" | ||
| ) | ||
|
|
||
| // Find dynamic map key names passed with Parent["foo"] notation | ||
| var bracketsRe = regexp.MustCompile(`\["([^\["\]]*)"\]`) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This regular expression does not support escape sequences. I wouldn't advice writing your own quoted string parser. It's hard. I would advice using a JS parser library for parsing the ENTIRE glob, e.g. https://pkg.go.dev/github.com/robertkrimen/otto/parser, and then work with the AST.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, you can use the Go language parser for the glob. It has a different set of escape sequences than JS, but is still better than rolling your own. |
||
|
|
||
| // Normalization for supporting arbitrary dynamic keys with dots: | ||
| // Gateway.PublicGateways["gw.example.com"].UseSubdomains | ||
| // Pinning.RemoteServices["pins.example.org"].Policies.MFS.Enable | ||
|
Comment on lines
+15
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this notation standard/used in other software or are we just making this up this form of escaping? Either way we'd need to document this in the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how keys in JSON objects are addressed on the web, and since we use JSON for config anyway, means we don't invent anything new, but follow existing JSON convention.: ( {"foo.bar":{"a": "buz"}} )["foo.bar"].a → "buz"I've added note about this notation to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return value is not documented accurately on a public method. What about dynamicKeys? |
||
| func ConfigKeyToLookupData(key string) (normalizedKey string, dynamicKeys map[string]string) { | ||
| bracketedKeys := bracketsRe.FindAllString(key, -1) | ||
| dynamicKeys = make(map[string]string, len(bracketedKeys)) | ||
| normalizedKey = key | ||
| for _, mapKeySegment := range bracketedKeys { | ||
| mapKey := strings.TrimPrefix(mapKeySegment, `["`) | ||
| mapKey = strings.TrimSuffix(mapKey, `"]`) | ||
lidel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| placeholder := uuid.New().String() | ||
| dynamicKeys[placeholder] = mapKey | ||
| normalizedKey = strings.Replace(normalizedKey, mapKeySegment, fmt.Sprintf(".%s", placeholder), 1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will convert a glob string like: ["abc"]["def"] into .UUID1.UUID2 |
||
| } | ||
| return normalizedKey, dynamicKeys | ||
| } | ||
|
|
||
| // Produces a part of config key with original map key names. | ||
| // Used only for better UX in error messages. | ||
| func buildSubKey(i int, parts []string, dynamicKeys map[string]string) string { | ||
| subkey := strings.Join(parts[:i], ".") | ||
| for placeholder, realKey := range dynamicKeys { | ||
| subkey = strings.Replace(subkey, fmt.Sprintf(".%s", placeholder), fmt.Sprintf(`["%s"]`, realKey), 1) | ||
| } | ||
| return subkey | ||
| } | ||
|
|
||
| func MapGetKV(v map[string]interface{}, key string) (interface{}, error) { | ||
| var ok bool | ||
| var mcursor map[string]interface{} | ||
| var cursor interface{} = v | ||
|
|
||
| parts := strings.Split(key, ".") | ||
| normalizedKey, dynamicKeys := ConfigKeyToLookupData(key) | ||
| parts := strings.Split(normalizedKey, ".") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fails of normalizedKey starts with a ".", which technically it can. |
||
| for i, part := range parts { | ||
| sofar := strings.Join(parts[:i], ".") | ||
| sofar := buildSubKey(i, parts, dynamicKeys) | ||
|
|
||
| mcursor, ok = cursor.(map[string]interface{}) | ||
| if !ok { | ||
| return nil, fmt.Errorf("%s key is not a map", sofar) | ||
| } | ||
|
|
||
| if dynamicPart, ok := dynamicKeys[part]; ok { | ||
| part = dynamicPart | ||
| } | ||
| cursor, ok = mcursor[part] | ||
| if !ok { | ||
| return nil, fmt.Errorf("%s key has no attributes", sofar) | ||
| return nil, fmt.Errorf("%s key has no attribute %s", sofar, part) | ||
| } | ||
| } | ||
| return cursor, nil | ||
|
|
@@ -32,13 +68,17 @@ func MapSetKV(v map[string]interface{}, key string, value interface{}) error { | |
| var mcursor map[string]interface{} | ||
| var cursor interface{} = v | ||
|
|
||
| parts := strings.Split(key, ".") | ||
| normalizedKey, dynamicKeys := ConfigKeyToLookupData(key) | ||
| parts := strings.Split(normalizedKey, ".") | ||
| for i, part := range parts { | ||
| mcursor, ok = cursor.(map[string]interface{}) | ||
| if !ok { | ||
| sofar := strings.Join(parts[:i], ".") | ||
| sofar := buildSubKey(i, parts, dynamicKeys) | ||
| return fmt.Errorf("%s key is not a map", sofar) | ||
| } | ||
| if dynamicPart, ok := dynamicKeys[part]; ok { | ||
| part = dynamicPart | ||
| } | ||
|
|
||
| // last part? set here | ||
| if i == (len(parts) - 1) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strings.EqualFold (below) no longer works, since now it is applied to a rewritten key which has UUIDs.