-
Notifications
You must be signed in to change notification settings - Fork 33
feat(core): accept shorthand enum names in policy API requests #3401
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
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ee80534
feat(core): accept shorthand enum names in policy API requests
marythought f421f4d
fix(core): address review feedback on enum normalization
marythought ebc9139
chore(core): add numeric enum passthrough tests
marythought 9668d0d
chore(core): add e2e BDD tests for shorthand enum names
marythought 2fa0ca8
chore(core): add oversized body test for enum normalization middleware
marythought c70f747
fix(core): address review feedback on BDD shorthand enum tests
marythought 57921d8
feat(core): export enumnormalize, add parent-scoped rules and WithHTT…
marythought 6f9cdc2
fix(core): fix CreateAttribute request body in BDD shorthand test
marythought 5e1ce05
fix(core): address CodeRabbit review feedback
marythought db769f6
refactor(core): make maxBodyBytes optional in NewMiddleware
marythought File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| package enumnormalize | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "io" | ||
| "strings" | ||
| ) | ||
|
|
||
| // EnumFieldRule maps a JSON field name to the prefix that protobuf requires. | ||
| // When the middleware encounters a string value in a matching field that does | ||
| // not already carry the prefix, it prepends the prefix so that protojson | ||
| // recognises the canonical enum name. | ||
| type EnumFieldRule struct { | ||
| // JSONField is the protojson camelCase field name (e.g. "operator", "booleanOperator"). | ||
| JSONField string | ||
| // Prefix is the proto enum type prefix including trailing underscore | ||
| // (e.g. "SUBJECT_MAPPING_OPERATOR_ENUM_"). | ||
| Prefix string | ||
| // ParentField optionally scopes this rule to only match when JSONField | ||
| // appears inside an object that is a direct child of a key named | ||
| // ParentField (at any depth). This disambiguates cases where multiple | ||
| // enum types share the same field name (e.g. "type") but live under | ||
| // different parent keys (e.g. "contentExtractors" vs "tagProcessors"). | ||
| // When empty, the rule matches JSONField at any position (original behavior). | ||
| ParentField string | ||
| } | ||
|
|
||
| // ruleLookup stores pre-built lookup tables for fast matching. | ||
| type ruleLookup struct { | ||
| // global maps field name → prefix for rules with no ParentField. | ||
| global map[string]string | ||
| // scoped maps parentField → (field name → prefix) for parent-scoped rules. | ||
| scoped map[string]map[string]string | ||
| } | ||
|
|
||
| // buildRuleLookup creates a ruleLookup from a set of rules. | ||
| func buildRuleLookup(rules []EnumFieldRule) ruleLookup { | ||
| rl := ruleLookup{ | ||
| global: make(map[string]string), | ||
| scoped: make(map[string]map[string]string), | ||
| } | ||
| for _, r := range rules { | ||
| if r.ParentField == "" { | ||
| rl.global[r.JSONField] = r.Prefix | ||
| } else { | ||
| if rl.scoped[r.ParentField] == nil { | ||
| rl.scoped[r.ParentField] = make(map[string]string) | ||
| } | ||
| rl.scoped[r.ParentField][r.JSONField] = r.Prefix | ||
| } | ||
| } | ||
| return rl | ||
| } | ||
|
|
||
| // normalizeJSON rewrites shorthand enum string values in body according to | ||
| // the configured rules. Values that already carry the full prefix, numeric | ||
| // values, and fields not covered by any rule pass through unchanged. | ||
| func normalizeJSON(body []byte, rl ruleLookup) ([]byte, error) { | ||
| if len(body) == 0 || (len(rl.global) == 0 && len(rl.scoped) == 0) { | ||
| return body, nil | ||
| } | ||
|
|
||
| // Use json.Decoder with UseNumber to preserve numeric precision | ||
| // (avoids float64 conversion of large int64 values). | ||
| decoder := json.NewDecoder(bytes.NewReader(body)) | ||
| decoder.UseNumber() | ||
|
|
||
| var parsed any | ||
| if err := decoder.Decode(&parsed); err != nil { | ||
| // Not valid JSON — pass through and let ConnectRPC surface the error. | ||
| return body, nil //nolint:nilerr // intentional: invalid JSON is not our error to report | ||
| } | ||
|
|
||
| // Ensure the entire body is a single JSON value. If there are trailing | ||
| // tokens (e.g. `{"a":1}{"b":2}`), return the original body so ConnectRPC | ||
| // can reject the malformed input rather than silently dropping the tail. | ||
| var trailing any | ||
| if err := decoder.Decode(&trailing); err != io.EOF { | ||
| return body, nil | ||
| } | ||
|
|
||
| normalizeValue(parsed, rl, "") | ||
|
|
||
| return json.Marshal(parsed) | ||
| } | ||
|
|
||
| // normalizeValue recursively walks a decoded JSON value, normalizing string | ||
| // enum fields according to the lookup rules. parentKey tracks the key under | ||
| // which the current value was found, enabling parent-scoped rules. | ||
| func normalizeValue(v any, rl ruleLookup, parentKey string) { | ||
| switch val := v.(type) { | ||
| case map[string]any: | ||
| for key, child := range val { | ||
| // Check global rules (no parent scope) | ||
| if prefix, ok := rl.global[key]; ok { | ||
| if s, isStr := child.(string); isStr { | ||
| val[key] = applyPrefix(s, prefix) | ||
| } | ||
| } | ||
| // Check parent-scoped rules | ||
| if scopedFields, hasParent := rl.scoped[parentKey]; hasParent { | ||
| if scopedPrefix, hasField := scopedFields[key]; hasField { | ||
| if s, isStr := child.(string); isStr { | ||
| val[key] = applyPrefix(s, scopedPrefix) | ||
| } | ||
| } | ||
| } | ||
| normalizeValue(child, rl, key) | ||
| } | ||
| case []any: | ||
| // Array elements inherit the parent key so that scoped rules work | ||
| // through arrays (e.g. "contentExtractors": [{"type": "..."}]). | ||
| for _, item := range val { | ||
| normalizeValue(item, rl, parentKey) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // applyPrefix prepends prefix to value if it is not already present | ||
| // (case-insensitive check). The value is upper-cased before comparison and | ||
| // before prepending so that "in" and "IN" both resolve correctly. | ||
| func applyPrefix(value, prefix string) string { | ||
| upper := strings.ToUpper(value) | ||
| if strings.HasPrefix(upper, strings.ToUpper(prefix)) { | ||
| return upper | ||
| } | ||
| return prefix + upper | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.