Skip to content

feat(evaluator): add lower/upper string casing operators#1982

Open
anxkhn wants to merge 4 commits into
open-feature:mainfrom
anxkhn:loop/flagd__002
Open

feat(evaluator): add lower/upper string casing operators#1982
anxkhn wants to merge 4 commits into
open-feature:mainfrom
anxkhn:loop/flagd__002

Conversation

@anxkhn

@anxkhn anxkhn commented Jun 30, 2026

Copy link
Copy Markdown

This adds two single-argument JSONLogic operators, lower and upper, that fold a string to ASCII lower/upper case. Composed with the existing operators (==, in, starts_with, ...) they enable case-insensitive targeting without duplicating every comparison operator with an ignore-case variant, which is the shape requested in the issue:

{ "==": [{ "lower": [{ "var": "email" }] }, "user@example.com"] }

Casing is restricted to ASCII (A-Z/a-z) per @toddbaert's recommendation on the issue: strings.ToLower/ToUpper give simple 1:1 mapping in Go, but Java/Python/Rust/JS use full (and Java locale-sensitive) mappings, so a fold like ß -> SS would diverge across providers. ASCII-only is trivially identical everywhere and covers the real use case (emails, non-IDN domains). All non-ASCII bytes are left unchanged. This is documented as the intended scope.

Changes

  • core/pkg/evaluator/string_casing.go: lower/upper operators, mirroring the string_comparison.go (starts_with/ends_with) structure. Single arg accepted as bare string or 1-element array; non-string/invalid input logs and returns nil, matching the existing operators' error idiom.
  • core/pkg/evaluator/json.go: register both operators in NewResolver.
  • docs/schema/v0/targeting.json: stringCasingRule added to anyRule.
  • Docs: new custom-operation page, operator-table row, mkdocs nav entry.
  • core/pkg/evaluator/string_casing_test.go: table-driven tests (compose with ==/starts_with, ASCII-only proof, non-string fallback).

Closes #1916

Notes

  • Restricted to ASCII per the maintainer comment; intentionally does not use strings.ToLower/ToUpper.
  • make test-core (-race), golangci-lint v2.7.2, and markdownlint all pass locally; evaluator coverage ~87.6%.

Add two single-argument JSONLogic operators, `lower` and `upper`, that fold a
string to ASCII lower/upper case. Composed with existing operators (==, in,
starts_with, ...) they enable case-insensitive targeting without duplicating
every comparison operator, e.g.:

  { "==": [{ "lower": [{ "var": "email" }] }, "user@example.com"] }

Casing is restricted to ASCII (A-Z/a-z) so results are deterministic and
identical across all flagd providers; other bytes are unchanged. Includes
targeting schema, operator-table and custom-operation docs, and unit tests.

Closes open-feature#1916

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn anxkhn requested review from a team as code owners June 30, 2026 09:22
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 30, 2026
@netlify

netlify Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploy Preview for polite-licorice-3db33c canceled.

Name Link
🔨 Latest commit 6434e6f
🔍 Latest deploy log https://app.netlify.com/projects/polite-licorice-3db33c/deploys/6a465683fc03f10008e1614a

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0c54f2e-fbf6-4024-8c3c-eed5ff0c99d9

📥 Commits

Reviewing files that changed from the base of the PR and between ba808b4 and 6434e6f.

📒 Files selected for processing (1)
  • core/pkg/evaluator/string_casing_test.go
👮 Files not reviewed due to content moderation or server errors (1)
  • core/pkg/evaluator/string_casing_test.go

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding lower/upper string casing operators.
Description check ✅ Passed The description is directly related and accurately describes the casing operators and supporting changes.
Linked Issues check ✅ Passed The PR fulfills #1916 by adding composable lower/upper transforms for case-insensitive targeting.
Out of Scope Changes check ✅ Passed The changes appear scoped to the requested evaluator, schema, docs, and tests for casing operators.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/pkg/evaluator/string_casing_test.go`:
- Around line 243-256: The no-error test cases in string_casing_test.go are
short-circuiting the loop because the custom wantErr closures for the
bare-string and single-element-array cases return false, so the subsequent
assert.Equalf on wantProperty is never reached. Update those wantErr helpers in
the relevant test table entries to indicate success (or replace them with
assert.NoError-style checks) so the test continues past the error check and
actually verifies the returned string values for the string-casing evaluator
cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b9e9b08-7a3d-4bf6-b859-85607cd99883

📥 Commits

Reviewing files that changed from the base of the PR and between d99a3e3 and 8f8186f.

📒 Files selected for processing (7)
  • core/pkg/evaluator/json.go
  • core/pkg/evaluator/string_casing.go
  • core/pkg/evaluator/string_casing_test.go
  • docs/reference/custom-operations/string-casing-operation.md
  • docs/reference/flag-definitions.md
  • docs/schema/v0/targeting.json
  • mkdocs.yml

Comment thread core/pkg/evaluator/string_casing_test.go
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn

anxkhn commented Jul 1, 2026

Copy link
Copy Markdown
Author

Good catch. The two happy-path wantErr closures returned false, which tripped the if !tt.wantErr(...) { return } guard and skipped assert.Equalf, so the "a" outputs for the bare-string and single-element-array cases were never checked. I flipped them to return true (matching the error-case closures) so the loop now reaches the value assertion. Verified by temporarily corrupting wantProperty: it passed before the change and fails after, so both cases are genuinely asserted now.

anxkhn added 2 commits July 2, 2026 13:38
The lower and upper operators shared near-identical implementations: each
method repeated the parse/error-log/return-nil body, and lowerString and
upperString repeated the same ASCII-folding loop. SonarCloud flagged this as
duplicated code on the PR's new lines.

Factor the shared logic into helpers so the two operators differ only by their
case mapping:
- evaluateCasing(op, values, mapByte) holds the common parse-and-apply body.
- asciiCase(s, mapByte) holds the common per-byte rewrite loop.
- toASCIILower/toASCIIUpper isolate the only real difference (the letter range
  and fold direction).

Behavior and operator names are unchanged; existing tests pass.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Each lower/upper table case repeated the full model.Flag literal (key, state,
default variant, variants, and a multi-line targeting rule), and two of the
upper cases were byte-identical. SonarCloud flagged this as duplicated new code
(new_duplicated_lines_density above the 3% gate) on string_casing_test.go.

Extract a casingFlag(targeting) helper so each case supplies only its targeting
rule and collapse the targeting JSON to a single line. Test names, contexts, and
expectations are unchanged; go test -race and golangci-lint still pass.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support case-insensitive string matching in targeting rules

1 participant