Skip to content

Conversation

techouse
Copy link
Owner

@techouse techouse commented Aug 13, 2025

This pull request standardizes the handling of map keys throughout the query string decoder and related utilities to always use string keys, even for numeric indices. This change ensures consistency in the decoded output and prevents issues that could arise from mixing integer and string keys in maps. The update also affects test cases and fixtures to align with this new behavior.

Core decoder and utility logic:

  • Updated Decoder.kt and Utils.kt to always create maps with string keys instead of integer keys, including for numeric indices and when converting lists to maps. This affects how parsed query strings are represented internally. [1] [2] [3] [4] [5]

Test fixtures and cases:

  • Modified all relevant test cases and fixtures in DecodeSpec.kt, ExampleSpec.kt, QsParserSpec.kt, and EmptyTestCases.kt so that expected decoded maps use string keys for indices, ensuring tests match the new decoder output. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13]

Code style and imports:

  • Simplified imports in Encoder.kt by using wildcard imports for models and updating standard library imports for consistency.

Summary by CodeRabbit

  • Bug Fixes
    • Decoding now uses string keys for numeric indices in maps (e.g., "0", "1"), ensuring consistent map outputs.
    • Lists are only created for bracketed indices within the list limit; otherwise, results are maps with string keys (empty index becomes "0").
  • Refactor
    • Internal map handling unified to use string-keyed maps when deriving from iterables.
  • Tests
    • Updated expectations across decoding and merge tests to reflect stringified numeric keys.

No changes to public APIs.

@techouse techouse self-assigned this Aug 13, 2025
@techouse techouse added the bug Something isn't working label Aug 13, 2025
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

The decoder and utils now consistently produce string-keyed maps for numeric indices when not forming lists. Decoder logic distinguishes bracketed numeric indices within limits to build lists; otherwise, it stores values under string keys. Utils.merge aligns by constructing string-keyed maps from Iterable inputs. Tests updated accordingly. Encoder imports consolidated.

Changes

Cohort / File(s) Summary
Decoder: string-keyed maps and list handling
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Decoder.kt
Non-list branches now use LinkedHashMap<String, Any?>; refined detection of bracketed numeric indices vs pure numeric keys; lists created only when bracketed index within limit; else paths store leaves under decodedRoot as string keys.
Utils: merge to string-keyed maps from Iterable
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Utils.kt
Iterable-derived map targets now use String keys (indices stringified) across merge paths; assignments and size-based keys converted to strings; control flow unchanged.
Encoder imports cleanup
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Encoder.kt
Replaced specific imports with wildcards (models., java.util.); no logic changes.
Tests: expectations updated to string keys
qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/fixtures/data/EmptyTestCases.kt, .../unit/DecodeSpec.kt, .../unit/ExampleSpec.kt, .../unit/QsParserSpec.kt, .../unit/UtilsSpec.kt
Adjusted expected map keys from numeric to string for indices throughout decoding and merge scenarios; no test logic changes.

Sequence Diagram(s)

sequenceDiagram
  participant Input as Query String
  participant Decoder
  participant Builder as Obj Builder
  participant Output as Decoded Result

  Input->>Decoder: key=value (e.g., a[1]=b, a.b=c)
  Decoder->>Decoder: Parse key segments
  Decoder->>Decoder: Detect bracketed index?
  alt Bracketed index within listLimit and lists enabled
    Decoder->>Builder: Ensure List size idx+1
    Builder->>Builder: Place value at list[idx]
  else Otherwise
    Decoder->>Builder: Use Map<String, Any?> path
    Builder->>Builder: Put value under string key
  end
  Builder->>Output: Return assembled structure
Loading
sequenceDiagram
  participant Utils as Utils.merge
  participant L as Left Target
  participant R as Right Source
  participant M as Merge Result

  L->>Utils: merge(left, right)
  alt Iterable involved
    Utils->>Utils: Build Map<String, Any?> from indices
    Utils->>M: Assign entries with stringified keys
  else Map vs Map or scalar
    Utils->>M: Standard merge logic
  end
  Utils-->>L: Return merged result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my whiskers at keys that sing,
“0” and “1” now tied with string.
Lists line up when brackets chime,
Maps keep order, crisp in time.
I thump approval—merge is neat,
Carrots for code that’s clean and sweet. 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/revert-numbers-as-strings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@techouse techouse changed the title 🐛 ensure consistent string key usage for maps and correct numeric key handling in Decoder 🚸 ensure consistent string key usage for maps and correct numeric key handling in Decoder Aug 13, 2025
Copy link

codecov bot commented Aug 13, 2025

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.08%. Comparing base (ca12138) to head (267b53c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...in/io/github/techouse/qskotlin/internal/Decoder.kt 85.71% 0 Missing and 2 partials ⚠️
...tlin/io/github/techouse/qskotlin/internal/Utils.kt 91.66% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main      #10      +/-   ##
============================================
- Coverage     88.22%   88.08%   -0.15%     
  Complexity      353      353              
============================================
  Files            14       14              
  Lines          1036     1032       -4     
  Branches        244      241       -3     
============================================
- Hits            914      909       -5     
  Misses           52       52              
- Partials         70       71       +1     
Flag Coverage Δ
java-17 88.08% <88.46%> (-0.15%) ⬇️
java-21 88.08% <88.46%> (-0.15%) ⬇️
jvm 88.08% <88.46%> (-0.15%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Utils.kt (1)

184-193: Make map-to-map merges string-keyed to avoid “1” vs 1 key duplication

In the map+map merge path, keys from source are currently used as-is. If any numeric keys are Int (not String), this can produce mixed keys for equivalent indices (e.g., "1" and 1). Given the PR goal of standardizing on string keys everywhere, coerce both target and source keys to strings and use a String-keyed merge target.

Apply this diff:

-        @Suppress("UNCHECKED_CAST")
-        val mergeTarget: MutableMap<Any, Any?> =
-            when {
-                target is Iterable<*> && source !is Iterable<*> ->
-                    target
-                        .withIndex()
-                        .associate { it.index.toString() to it.value }
-                        .filterValues { it !is Undefined }
-                        .toMutableMap()
-                else -> (target as Map<Any, Any?>).toMutableMap()
-            }
+        @Suppress("UNCHECKED_CAST")
+        val mergeTarget: MutableMap<String, Any?> =
+            when {
+                target is Iterable<*> && source !is Iterable<*> ->
+                    target
+                        .withIndex()
+                        .associate { it.index.toString() to it.value }
+                        .filterValues { it !is Undefined }
+                        .toMutableMap()
+                else -> (target as Map<*, *>)
+                    .mapKeys { (k, _) -> k.toString() }
+                    .toMutableMap()
+            }
 
-        @Suppress("UNCHECKED_CAST")
-        (source as Map<Any, Any?>).forEach { (key, value) ->
-            mergeTarget[key] =
-                if (mergeTarget.containsKey(key)) {
-                    merge(mergeTarget[key], value, options)
-                } else {
-                    value
-                }
-        }
+        @Suppress("UNCHECKED_CAST")
+        (source as Map<*, *>).forEach { (key, value) ->
+            val k = key.toString()
+            mergeTarget[k] =
+                if (mergeTarget.containsKey(k)) {
+                    merge(mergeTarget[k], value, options)
+                } else {
+                    value
+                }
+        }

Notes:

  • Keeps existing behavior for list-to-map coercion.
  • Ensures both target and source map keys are consistently strings in this path, preventing subtle duplication bugs.

Also applies to: 196-203

🧹 Nitpick comments (2)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Encoder.kt (1)

12-12: Avoid star import from java.util to prevent name clashes with Kotlin collections

Importing java.util.* can introduce ambiguous references with Kotlin’s Map/List types and makes the file’s dependencies less explicit. Only WeakHashMap is used here.

Apply this diff:

-import java.util.*
+import java.util.WeakHashMap
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Decoder.kt (1)

212-216: Simplify numeric-index detection by using toIntOrNull()

Current detection uses an extra isPureNumeric check. You can simplify and keep the same behavior (including leading-zero safeguards via string comparison) by relying on toIntOrNull().

Apply:

-                val isPureNumeric = decodedRoot.isNotEmpty() && decodedRoot.all { it.isDigit() }
-                val idx: Int? = if (isPureNumeric) decodedRoot.toInt() else null
-                val isBracketedNumeric =
-                    idx != null && root != decodedRoot && idx.toString() == decodedRoot
+                val idx: Int? = decodedRoot.toIntOrNull()
+                val isBracketedNumeric =
+                    idx != null && root != decodedRoot && idx.toString() == decodedRoot
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca12138 and 267b53c.

📒 Files selected for processing (8)
  • qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Decoder.kt (1 hunks)
  • qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Encoder.kt (1 hunks)
  • qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Utils.kt (4 hunks)
  • qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/fixtures/data/EmptyTestCases.kt (2 hunks)
  • qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/DecodeSpec.kt (9 hunks)
  • qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/ExampleSpec.kt (1 hunks)
  • qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/QsParserSpec.kt (9 hunks)
  • qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/UtilsSpec.kt (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/QsParserSpec.kt (1)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/qs.kt (1)
  • decode (26-73)
qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/DecodeSpec.kt (2)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Utils.kt (1)
  • decode (452-473)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/qs.kt (1)
  • decode (26-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Android AAR (AGP) + unit tests
  • GitHub Check: Ensure compatibility with qs
  • GitHub Check: JVM tests (Java 21)
  • GitHub Check: JVM tests (Java 17)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (37)
qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Encoder.kt (1)

7-7: Wildcard import from models is acceptable; no functional impact

Consolidating multiple imports under io.github.techouse.qskotlin.models.* keeps things readable and has no behavioral change. Good.

qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Utils.kt (5)

40-45: Correct: iterable-to-map indices now stringified

Converting Iterable indices to string keys aligns with the PR objective and prevents mixed-type keys downstream.


50-55: Correct: appending elements with string keys

Using i.toString() for newly appended elements preserves the string-key invariant when merging Iterable into a sparse target.


54-65: Result shape handling is sensible

  • parseLists=false returns a Map (filtering Undefined)
  • Set/List targets return corresponding collection values
  • Otherwise returns a List

Behavior is consistent and predictable.


119-126: Correct: when merging Iterable into a Map, use string keys

Ensures newly added entries are keyed consistently as strings.


152-162: Good: when target is Iterable and source is Map, convert target to string-keyed map and string-coerce source keys

This is required to guarantee uniform string-key maps during merges.

qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/fixtures/data/EmptyTestCases.kt (1)

181-182: Fixture updates match string-index expectations

Switching inner map keys from numeric to string ("0"/"1") aligns fixtures with the new decoder behavior.

Also applies to: 186-187, 210-211

qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/ExampleSpec.kt (1)

182-183: Tests correctly reflect string-keyed indices when lists are not formed

  • High indices and listLimit=0 produce maps with string keys
  • parseLists=false yields string indices starting at "0"
  • Mixed numeric and non-numeric indices merge into a map with consistent string keys

Also applies to: 186-193, 196-197

qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/UtilsSpec.kt (3)

372-374: Intentional mixed-key result is acceptable for externally provided numeric-keyed maps

The pre-existing Int key (0) is retained, while new indices from the list are stringified ("1"). This mirrors Utils.merge behavior and avoids destructive key type coercion on user-provided maps.


441-443: Updated expectation to string indices is correct

List indices are stringified when merged into an object, matching Utils changes.


524-529: Stringifying indices when mixing list/object under same key looks good

Both scenarios (object into list, and list into object) now produce string-keyed maps under "foo", ensuring consistency.

Also applies to: 537-542

qs-kotlin/src/main/kotlin/io/github/techouse/qskotlin/internal/Decoder.kt (5)

201-203: Consistently using string-keyed maps in non-list branches — aligns with PR objective

Switching to LinkedHashMap<String, Any?> here ensures we never leak integer keys into map structures. This is the right move for consistency across the decoder and Utils.merge.


210-211: Localized dot-decoding for keys looks correct

Decoding "%2E" to "." in decodedRoot based on getDecodeDotInKeys keeps behavior aligned with options while avoiding over-decoding. Good.


218-224: Correct: when lists are disabled or listLimit < 0, always return a string-keyed map

Using "0" when decodedRoot is empty ensures "[]" becomes a map entry "0" -> leaf, which matches updated tests and the PR’s consistency goal.


233-239: Else-path correctly stores leaves under string keys

Numeric-looking keys that are not valid list indices (e.g., out of limit, negative, leading zeros) fall back to string-keyed maps as intended. This matches the updated test expectations across the suite.


226-231: No changes needed: Undefined.Companion() correctly invokes the sentinel
The companion object’s operator fun invoke() returns the single Instance, so calling Undefined.Companion() yields the same sentinel as Undefined(). The fill–and later compaction (is Undefined)–logic remains correct and all existing tests pass.

  • Decoder.kt:228 (MutableList<Any?>(idx + 1) { Undefined.Companion() })

Likely an incorrect or invalid review comment.

qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/DecodeSpec.kt (11)

77-77: Expectation updated to string key for numeric top-level key — correct

"0" should be a string key now; this mirrors the decoder’s consistent string-key policy.


307-317: Correct: string keys for indices when arrays are not formed

  • listLimit = 0 -> a[1] becomes {"1": "c"}
  • parseLists = false -> indexed entries map to string keys

This aligns with the new decoder behavior.


320-326: Boundary handling at listLimit: correct fallback to string-keyed map

Index 21 with listLimit 20 falls back to map key "21" as a string, both explicitly and with defaults — matches intended behavior.


352-363: Arrays-to-objects transformation now uses string indices — good

All numeric indexes inside transformed maps are asserted as strings ("0", "1"), ensuring consistency throughout the data model.


389-396: Dot-notation arrays-to-objects: indices asserted as strings — good

These expectations correctly reflect the new string-keyed behavior under allowDots = true.


407-410: Pruning with large indices preserves numeric indices as strings — correct

Keys "2" and "99999999" are strings, aligning with the normal form for map keys.


484-489: No-parent parsing: empty bracket index becomes key "0" (string) — correct

Maintains uniformity for synthetic indices under both empty value and strictNullHandling.


726-731: Map produced from mixed list/object input uses stringified index — correct

The expected map uses "0" (string) for the array element; consistent with Utils.merge changes.


520-534: List-limit handling: explicit indices and negative indices now stringified in maps — correct

All updated assertions ("0", "1", "-1") are correct given the new decoding rules.


536-541: parseLists=false path: indices are string keys — correct

Disabling list parsing consistently yields string-keyed maps for indices.


1035-1045: Converting long lists into maps uses string keys — matches Decoder behavior

When list length exceeds the limit, string-keyed maps are asserted; this matches the Decoder branch that falls back to maps.

qs-kotlin/src/test/kotlin/io/github/techouse/qskotlin/unit/QsParserSpec.kt (10)

24-24: Top-level numeric key expected as string — correct

This aligns with the decoder’s normalization to string keys.


169-176: Array index with listLimit = 0 falls back to object with string key — correct

"1" becomes a map key instead of an array index under listLimit = 0; expectation is right.


184-191: Limit-bound indices fallback to string-keyed objects — correct

At 21 > 20, both tests properly expect a string-keyed object ("21" -> "a").


233-247: Arrays-to-objects conversion uses string keys — correct and consistent

All numeric indices are asserted as strings, covering multiple ordering scenarios.


279-290: Dot-notation arrays-to-objects: numeric indices as strings — correct

These cover the allowDots path and maintain consistent key typing.


301-303: Undefined pruning scenario asserts stringified numeric keys — correct

"2" and "99999999" remain strings as expected.


419-424: Missing parent parsing assigns "0" (string) — correct

Matches the decoder’s "[]" -> "0" normalization.


456-464: Negative listLimit: indices are always mapped to string keys — correct

"0" and "-1" assertions reflect the “list parsing off” behavior for negative limits.


469-473: parseLists=false path explicitly asserts string-keyed indices — correct

Both multi-index and bracket-only cases are covered.


1013-1015: Utils.merge expected now asserts string keys for iterable-derived targets — correct

"0"/"1" are string keys, consistent with the underlying Utils changes.

@techouse techouse merged commit 031439a into main Aug 13, 2025
13 of 14 checks passed
@techouse techouse deleted the fix/revert-numbers-as-strings branch August 13, 2025 13:13
@coderabbitai coderabbitai bot mentioned this pull request Oct 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant