Skip to content

✨ add key-aware decoding to the query string parser#35

Merged
techouse merged 8 commits intomainfrom
fix/decode-dot-in-keys
Aug 20, 2025
Merged

✨ add key-aware decoding to the query string parser#35
techouse merged 8 commits intomainfrom
fix/decode-dot-in-keys

Conversation

@techouse
Copy link
Owner

@techouse techouse commented Aug 19, 2025

This pull request introduces a new DecodeKind enum to enable key-aware decoding in the query string parser, and refactors the decoding logic and options to support more flexible custom decoder signatures. It also improves percent-decoding for dots in keys, adds comprehensive tests for the new behaviors, and clarifies option handling. These changes make it easier for consumers to write custom decoders that distinguish between keys and values, and ensure backward compatibility with existing decoder signatures.

Key-aware decoding and API enhancements

  • Added the DecodeKind enum (key, value) to distinguish decoding context for keys vs. values, and updated the decoder logic throughout the codebase to pass this context. (lib/src/enums/decode_kind.dart, lib/qs_dart.dart, lib/src/extensions/decode.dart, lib/src/models/decode_options.dart, lib/src/qs.dart) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
  • Refactored the DecodeOptions.decoder API to support multiple function signatures, including legacy and new key-aware variants, with robust dynamic fallback for callable objects. (lib/src/models/decode_options.dart) [1] [2] [3]

Decoding logic improvements

  • Updated decoding in decode.dart to pass DecodeKind.key or DecodeKind.value for each token, ensuring custom decoders receive context. (lib/src/extensions/decode.dart) [1] [2]
  • Improved percent-decoding for dots in keys by handling both %2E and %2e consistently. (lib/src/extensions/decode.dart)

Option handling and guardrails

  • Fixed logic for allowDots default value to ensure correct option precedence, and improved guardrail for disabling list parsing only for string input. (lib/src/models/decode_options.dart, lib/src/qs.dart) [1] [2]

Testing and validation

  • Added extensive unit tests for key-aware decoding, decoder API variants, option isolation, and dynamic fallback behaviors, including edge cases and legacy compatibility. (test/unit/decode_test.dart)

Miscellaneous

  • Updated the Makefile to use the correct test target name (test instead of tests). (Makefile)

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

coderabbitai bot commented Aug 19, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds DecodeKind enum and integrates key/value-aware decoding into the pipeline. Updates DecodeOptions to support flexible decoder signatures with kind and charset, adjusts decode control flow to pass DecodeKind, broadens dot-decoding in keys, gates list-guard by input type, expands public exports, updates tests, and fixes Makefile target.

Changes

Cohort / File(s) Summary
Build tooling
Makefile
sure target now calls make test instead of make tests.
Public exports
lib/qs_dart.dart, lib/src/qs.dart
Re-exported DecodeKind and additional modules; exposed decode/encode via methods.dart; public surface expanded.
New enum
lib/src/enums/decode_kind.dart
Added DecodeKind enum with values key and value; documented usage.
Decode pipeline + options
lib/src/extensions/decode.dart, lib/src/models/decode_options.dart
Decoder invocations now pass DecodeKind (key/value). DecodeOptions accepts flexible decoder shapes (Function?) with typed typedefs (Decoder, Decoder1–3) and a resolving method. Key-dot decoding now handles %2E and %2e.
QS decode guard
lib/src/qs.dart
Memory guard for list parsing applies only when input is String; DecodeKind re-exported.
Tests
test/unit/decode_test.dart
Added comprehensive tests for DecodeKind-aware decoding, decoder shape fallbacks, isolation, charset sentinel interactions, and list parsing behaviour; introduced helper callables.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant QS as QS.decode
  participant P as Parser (extensions/decode)
  participant DO as DecodeOptions
  participant UD as Utils.decode (fallback)

  C->>QS: decode(input, options)
  alt input is String
    QS->>P: parse query string
  else input is Map/other
    QS->>P: use pre-tokenised input
  end

  rect rgba(200,240,255,0.3)
  note over P,DO: Key parsing path
  P->>DO: decoder(keyPart, charset, kind=key)
  alt Custom decoder matches
    DO-->>P: decoded key
  else Fallbacks fail
    DO->>UD: decode(value, charset)
    UD-->>DO: decoded key
    DO-->>P: decoded key
  end
  end

  rect rgba(220,255,220,0.3)
  note over P,DO: Value parsing path
  P->>DO: decoder(valPart, charset, kind=value)
  alt Custom decoder matches
    DO-->>P: decoded value
  else Fallbacks fail
    DO->>UD: decode(value, charset)
    UD-->>DO: decoded value
    DO-->>P: decoded value
  end
  end

  P-->>QS: assembled map
  QS-->>C: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble keys and values too,
With kind in paw, I sort them through.
Dots unwrapped, both e and E,
Options dance quite flexibly.
Lists behave when strings arrive—
Hippity-hop, our tests all thrive! 🐇✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/decode-dot-in-keys

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 🐛 fix decode dot in keys ✨ add key-aware decoding to the query string parser Aug 19, 2025
@techouse techouse added enhancement New feature or request and removed bug Something isn't working labels Aug 19, 2025
@codacy-production
Copy link

codacy-production bot commented Aug 19, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-0.49% (target: -1.00%) 82.14%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (79f0659) 815 777 95.34%
Head commit (3af13c1) 834 (+19) 791 (+14) 94.84% (-0.49%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#35) 28 23 82.14%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@codecov
Copy link

codecov bot commented Aug 19, 2025

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.84%. Comparing base (79f0659) to head (3af13c1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/models/decode_options.dart 76.19% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #35      +/-   ##
==========================================
- Coverage   95.33%   94.84%   -0.50%     
==========================================
  Files          14       14              
  Lines         815      834      +19     
==========================================
+ Hits          777      791      +14     
- Misses         38       43       +5     

☔ 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

🧹 Nitpick comments (5)
Makefile (1)

40-40: Use $(MAKE) when invoking sub-makes

This preserves MAKEFLAGS (e.g., -j, --silent) and is the recommended pattern in Makefiles.

Apply this diff:

-	make check_style && make test
+	$(MAKE) check_style && $(MAKE) test
lib/src/enums/decode_kind.dart (1)

15-20: Doc example: import via the public API

Since DecodeKind is re-exported, prefer showing imports from the public surface to guide users.

Apply this diff to the doc snippet:

-/// ```dart
-/// import 'decode_kind.dart';
-///
-/// DecodeKind k = DecodeKind.key; // decode a key/segment
-/// DecodeKind v = DecodeKind.value; // decode a value
-/// ```
+/// ```dart
+/// import 'package:qs_dart/qs.dart'; // public re-export
+///
+/// final DecodeKind k = DecodeKind.key;   // decode a key/segment
+/// final DecodeKind v = DecodeKind.value; // decode a value
+/// ```
lib/src/extensions/decode.dart (1)

260-266: Case-insensitive dot decoding: simplify with a single regex

Minor readability/perf nit: replace the double replaceAll with a single case-insensitive pattern.

Apply this diff:

-        final String decodedRoot = options.decodeDotInKeys
-            ? cleanRoot.replaceAll('%2E', '.').replaceAll('%2e', '.')
-            : cleanRoot;
+        final String decodedRoot = options.decodeDotInKeys
+            ? cleanRoot.replaceAll(RegExp(r'%2E', caseSensitive: false), '.')
+            : cleanRoot;
lib/src/models/decode_options.dart (2)

72-74: Simplify defaulting logic for allowDots; remove redundant || false

The current expression is correct but unnecessarily complex and could be misread due to operator precedence. Make it explicit and concise.

Apply this diff:

-  })  : allowDots = allowDots ?? decodeDotInKeys == true || false,
+  })  : allowDots = allowDots ?? (decodeDotInKeys ?? false),

165-213: Do not swallow user decoder exceptions in the dynamic path; catch only call-shape errors

The dynamic ladder currently catches all exceptions, which risks hiding genuine errors thrown by user-provided decoders. Restrict catches to NoSuchMethodError/TypeError (mismatched named args/required named param) and let other exceptions propagate. Behaviour in the strongly-typed branches already propagates exceptions.

Apply this diff to narrow the catches:

-    // Dynamic callable or class with `call` method
-    try {
-      // Try full shape (value, {charset, kind})
-      return (d as dynamic)(value, charset: charset, kind: kind);
-    } catch (_) {
-      try {
-        // Try (value, {charset})
-        return (d as dynamic)(value, charset: charset);
-      } catch (_) {
-        try {
-          // Try (value, {kind})
-          return (d as dynamic)(value, kind: kind);
-        } catch (_) {
-          try {
-            // Try (value)
-            return (d as dynamic)(value);
-          } catch (_) {
-            // Fallback to default
-            return Utils.decode(value, charset: charset);
-          }
-        }
-      }
-    }
+    // Dynamic callable or class with `call` method
+    try {
+      // Try full shape (value, {charset, kind})
+      return (d as dynamic)(value, charset: charset, kind: kind);
+    } on NoSuchMethodError catch (_) {
+      // fall through
+    } on TypeError catch (_) {
+      // fall through
+    } 
+    try {
+      // Try (value, {charset})
+      return (d as dynamic)(value, charset: charset);
+    } on NoSuchMethodError catch (_) {
+      // fall through
+    } on TypeError catch (_) {
+      // fall through
+    }
+    try {
+      // Try (value, {kind})
+      return (d as dynamic)(value, kind: kind);
+    } on NoSuchMethodError catch (_) {
+      // fall through
+    } on TypeError catch (_) {
+      // fall through
+    }
+    try {
+      // Try (value)
+      return (d as dynamic)(value);
+    } on NoSuchMethodError catch (_) {
+      // Fallback to default
+      return Utils.decode(value, charset: charset);
+    } on TypeError catch (_) {
+      // Fallback to default
+      return Utils.decode(value, charset: charset);
+    }
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fb620c5 and 49373c7.

📒 Files selected for processing (7)
  • Makefile (1 hunks)
  • lib/qs_dart.dart (1 hunks)
  • lib/src/enums/decode_kind.dart (1 hunks)
  • lib/src/extensions/decode.dart (3 hunks)
  • lib/src/models/decode_options.dart (4 hunks)
  • lib/src/qs.dart (3 hunks)
  • test/unit/decode_test.dart (1 hunks)
⏰ 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). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (23)
lib/src/enums/decode_kind.dart (1)

27-36: Enum introduction looks good

Clear separation between key and value decoding contexts; the inline docs convey the intended behaviour well.

lib/src/qs.dart (2)

16-18: Re-exporting DecodeKind from QS is a good DX improvement

This lets consumers stick to a single import path. No issues spotted.


71-79: Guardrail now applies only to raw string inputs — aligns with option isolation

Great tweak: disabling list parsing based on parameter count only for String inputs avoids surprising behaviour when a pre-tokenised map is supplied.

lib/qs_dart.dart (1)

22-22: Exporting DecodeKind on the package’s public surface is appropriate

Matches the re-export in QS and provides consistent access. Looks good.

lib/src/extensions/decode.dart (1)

148-169: Key-aware decoding is correctly plumbed

Passing DecodeKind.key for keys and DecodeKind.value for values is the right cut. Also good to decode bare keys with key kind. Ensure DecodeOptions.decoder supports both legacy and new signatures as per PR description.

Would you like me to scan the codebase to confirm DecodeOptions.decoder adapts legacy callables? I can provide a ripgrep script to locate its declaration and usages to double-check signature compatibility.

test/unit/decode_test.dart (15)

2018-2035: Solid coverage: key-aware decoder receives DecodeKind for keys and values

Good assertion of KEY/ VALUE order; verifies both key and value paths are routed through the decoder with the right kind and that defaults are preserved.


2037-2041: Backwards compatibility preserved for legacy single-arg decoders

Nice check that classic (value)-only decoders still work and affect both keys and values.


2042-2047: Callable with only kind named arg supported

Good demonstration that a decoder accepting only DecodeKind can modify keys while leaving values intact.


2049-2063: Option isolation verified across calls under list guardrail

This ensures any internal guardrail changes don’t mutate the provided options. Thanks for targeting the string-input-only path.


2066-2077: Bare key decoding passes KEY to decoder with strictNullHandling

Good to see the decoder is not invoked for a value in this case and that null is preserved.


2079-2092: Comma splitting emits VALUE per segment, in correct order

Order-sensitive assertion is helpful for users writing stateful decoders.


2094-2105: Selective key mutation scenario well covered

This guards against regressions where values might be accidentally passed as KEY or vice versa.


2107-2115: Decoder returning null for VALUE path is respected

Ensures downstream code doesn’t coerce null to empty string unexpectedly.


2117-2124: Decoder not invoked for Map input

Good safety: bypassing the decoder for already-parsed inputs avoids surprising behaviour.


2126-2141: Duplicates path exercises KEY/VALUE sequence twice

Nice way to validate per-pair invocation order with the default combine strategy.


2143-2158: Charset sentinel observed by decoder via charset parameter

Great to have explicit observation coverage for the charset toggling seen by decoders.


2160-2173: parseLists=false still routes KEY and VALUE correctly

Validates that list parsing choice doesn’t affect decoder kind semantics.


2176-2187: Dynamic fallback: mismatched named params uses value-only invocation

The expectations and the call sink verify that both key and value go through the simplified call. This is the right behaviour for loosely-typed callables.


2189-2195: Dynamic fallback: required named param forces Utils.decode

Good negative path; ensures we do not attempt to satisfy incompatible required named parameters and instead gracefully fall back.


2198-2216: Helper callables (_Loose1, _Loose2) are minimal and purposeful

Clear intent and comments; they precisely exercise the dynamic invocation ladder.

lib/src/models/decode_options.dart (3)

31-37: New Decoder typedef with key-awareness is clear and future-proof

The unified typedef that includes optional charset and kind aligns with the API expansion and keeps implementations ergonomic.


38-50: Compat typedefs (V1–V4) cover legacy and niche callables

Good call preserving backwards compatibility while enabling progressive enhancement.


232-253: copyWith handles decoder correctly and keeps existing value by default

Good ergonomics; passing through the existing _decoder unless overridden matches expectations.

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

🧹 Nitpick comments (2)
lib/src/models/decode_options.dart (2)

31-50: Canonicalise decoder typedefs; avoid duplicated shapes and check the canonical alias

Decoder and DecoderV3 share the exact same signature. Keeping both as independent typedefs risks future drift in comments or parameters. Alias DecoderV3 to Decoder and switch the type check to Decoder to make the “preferred” name authoritative.

Apply:

-/// Full-featured: decoder with [charset] and key/value [kind].
-typedef DecoderV3 = dynamic Function(String? value,
-    {Encoding? charset, DecodeKind? kind});
+/// Full-featured: decoder with [charset] and key/value [kind].
+typedef DecoderV3 = Decoder;

And prefer the canonical alias in the dispatch:

-    if (d is DecoderV3) {
+    if (d is Decoder) {
       return d(value, charset: charset, kind: kind);
     }

Also applies to: 176-186


165-224: Default to options.charset when none is provided

When falling back to Utils.decode, charset may be null. Use charset ?? this.charset to maintain consistent decoding per options. This also protects dynamic-call fallbacks that end in the default path.

Apply:

-      return Utils.decode(value, charset: charset);
+      return Utils.decode(value, charset: charset ?? this.charset);
@@
-      // Fallback to default
-      return Utils.decode(value, charset: charset);
+      // Fallback to default
+      return Utils.decode(value, charset: charset ?? this.charset);
@@
-      // Fallback to default
-      return Utils.decode(value, charset: charset);
+      // Fallback to default
+      return Utils.decode(value, charset: charset ?? this.charset);
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 49373c7 and 0acf28d.

📒 Files selected for processing (2)
  • lib/src/enums/decode_kind.dart (1 hunks)
  • lib/src/models/decode_options.dart (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/enums/decode_kind.dart
🔇 Additional comments (5)
lib/src/models/decode_options.dart (5)

55-56: Widened decoder type to Object? enables callable objects—good choice

Switching the decoder field/ctor/copyWith to Object? to support callable objects (instances with call) matches the PR goal and is idiomatic given the dynamic fallback below. Including _decoder in Equatable props is appropriate.

Also applies to: 72-75, 163-164, 288-306


72-72: Nicely enforces allowDots when decodeDotInKeys is enabled

Initialising allowDots to allowDots ?? (decodeDotInKeys ?? false) guarantees dot-notation parsing whenever decodeDotInKeys is true. This aligns with the documented behaviour and avoids invalid states.


165-224: Dispatch order and exception narrowness look pragmatic

Prioritising strongly typed decoders (V3/V2/V4/V1) before dynamic invocation is sensible and avoids unnecessary try/catch. Catching NoSuchMethodError and TypeError for call-shape mismatches while letting user-code exceptions bubble up is a good default.


244-265: copyWith mirrors the constructor changes correctly

The copyWith signature and decoder propagation (decoder ?? _decoder) are consistent with the widened type and preserve existing decoders by default.


4-4: Import path is correct

  • pubspec.yaml declares the package name as qs_dart.
  • All imports use package:qs_dart/…, and no imports of package:qs/ were found.

No changes required.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/src/models/decode_options.dart (1)

115-120: Docs contradict themselves about decodeDotInKeys vs allowDots.

Earlier docs (Lines 79–83) say allowDots is implied and will be treated as true even if passed false. Here, this section says such a combination is invalid and will error. Pick one. Based on the PR summary, implication (no error) is intended.

Apply this diff to reconcile:

   /// Decode dots that appear in *keys* (e.g., `a.b=c`).
   ///
-  /// This explicitly opts into dot‑notation handling and implies [allowDots].
-  /// Setting [decodeDotInKeys] to `true` while forcing [allowDots] to `false`
-  /// is invalid and will cause an error in [QS.decode].
+  /// This explicitly opts into dot‑notation handling and implies [allowDots].
+  /// If you pass `allowDots: false` alongside `decodeDotInKeys: true`,
+  /// `allowDots` will still be treated as `true`.
🧹 Nitpick comments (5)
lib/src/models/decode_options.dart (5)

31-40: Consider making kind non-nullable in the preferred Decoder signature.

As this is the “preferred” signature, a non-nullable DecodeKind kind better signals intent and reduces null-handling inside custom decoders. The defaulting still happens at call sites.

Apply this diff:

-typedef Decoder = dynamic Function(
-  String? value, {
-  Encoding? charset,
-  DecodeKind? kind,
-});
+typedef Decoder = dynamic Function(
+  String? value, {
+  Encoding? charset,
+  DecodeKind kind,
+});

54-54: Narrow the decoder option to Function? to prevent silent misconfiguration.

Typing this parameter as Function? (instead of Object?) still allows callable classes (call) but catches accidental non-callables (e.g., integers) at compile time rather than silently falling back via exceptions.

Apply this diff:

-    Object? decoder,
+    Function? decoder,

160-163: Prefer Function? for _decoder field.

Same rationale as the constructor: keeps the API flexible for callable objects while preventing obvious misconfigurations.

Apply this diff:

-  final Object? _decoder;
+  final Function? _decoder;

164-220: Avoid name shadowing and tighten the dynamic fallback path.

The local variable decoder shadows the instance method name decoder(...), which harms readability. Rename it. Also, the dynamic fallback makes multiple calls guarded by exceptions; that’s acceptable, but consider normalising the decoder once (see follow-up) to avoid per-call exception-driven control flow.

Apply this diff to remove shadowing:

   dynamic decoder(
     String? value, {
       Encoding? charset,
       DecodeKind kind = DecodeKind.value,
     }) {
-    final Object? decoder = _decoder;
+    final Object? fn = _decoder;

-    if (decoder == null) {
+    if (fn == null) {
       return Utils.decode(value, charset: charset ?? this.charset);
     }

     // Prefer strongly-typed variants first
-    if (decoder is Decoder) return decoder(value, charset: charset, kind: kind);
-    if (decoder is Decoder1) return decoder(value, charset: charset);
-    if (decoder is Decoder2) return decoder(value, kind: kind);
-    if (decoder is Decoder3) return decoder(value);
+    if (fn is Decoder) return fn(value, charset: charset, kind: kind);
+    if (fn is Decoder1) return fn(value, charset: charset);
+    if (fn is Decoder2) return fn(value, kind: kind);
+    if (fn is Decoder3) return fn(value);

     // Dynamic callable or class with `call` method
     try {
       // Try full shape (value, {charset, kind})
-      return (decoder as dynamic)(value, charset: charset, kind: kind);
+      return (fn as dynamic)(value, charset: charset, kind: kind);
     } on NoSuchMethodError catch (_) {
       // fall through
     } on TypeError catch (_) {
       // fall through
     }
     try {
       // Try (value, {charset})
-      return (decoder as dynamic)(value, charset: charset);
+      return (fn as dynamic)(value, charset: charset);
     } on NoSuchMethodError catch (_) {
       // fall through
     } on TypeError catch (_) {
       // fall through
     }
     try {
       // Try (value, {kind})
-      return (decoder as dynamic)(value, kind: kind);
+      return (fn as dynamic)(value, kind: kind);
     } on NoSuchMethodError catch (_) {
       // fall through
     } on TypeError catch (_) {
       // fall through
     }
     try {
       // Try (value)
-      return (decoder as dynamic)(value);
+      return (fn as dynamic)(value);
     } on NoSuchMethodError catch (_) {
       // Fallback to default
       return Utils.decode(value, charset: charset ?? this.charset);
     } on TypeError catch (_) {
       // Fallback to default
       return Utils.decode(value, charset: charset ?? this.charset);
     }
   }

As a follow-up, consider normalising the decoder at construction time to a single Decoder wrapper. This removes the need for repeated type checks and exception-driven fallbacks on every scalar decode:

// Example approach (outside the diffed range):
final class DecodeOptions with EquatableMixin {
  final Decoder _resolve; // always non-null, wraps Utils.decode by default

  DecodeOptions({ Function? decoder, /* ... */ })
      : _resolve = _normalise(decoder),
        /* ... */ ;

  static Decoder _normalise(Function? f) {
    if (f == null) {
      return (v, {charset, kind = DecodeKind.value}) =>
          Utils.decode(v, charset: charset ?? utf8);
    }
    if (f is Decoder) {
      return (v, {charset, kind = DecodeKind.value}) =>
          f(v, charset: charset, kind: kind);
    }
    if (f is Decoder1) {
      return (v, {charset, kind = DecodeKind.value}) =>
          f(v, charset: charset);
    }
    if (f is Decoder2) {
      return (v, {charset, kind = DecodeKind.value}) =>
          f(v, kind: kind);
    }
    if (f is Decoder3) {
      return (v, {charset, kind = DecodeKind.value}) =>
          f(v);
    }
    // Dynamic fallback once:
    return (v, {charset, kind = DecodeKind.value}) {
      try {
        return (f as dynamic)(v, charset: charset, kind: kind);
      } catch (_) {
        try { return (f as dynamic)(v, charset: charset); } catch (_) {}
        try { return (f as dynamic)(v, kind: kind); } catch (_) {}
        try { return (f as dynamic)(v); } catch (_) {}
        return Utils.decode(v, charset: charset ?? utf8);
      }
    };
  }

  dynamic decoder(String? value, {Encoding? charset, DecodeKind kind = DecodeKind.value}) =>
      _resolve(value, charset: charset, kind: kind);
}

240-261: Type of decoder in copyWith + ability to clear the decoder.

  • Mirror the constructor change and narrow to Function?.
  • Currently, copyWith(decoder: null) cannot clear a previously set decoder because decoder ?? _decoder retains the old one. Consider a sentinel to allow explicit clearing if needed.

Apply this diff:

-    Object? decoder,
+    Function? decoder,

Optionally, adopt a sentinel for explicit clearing:

// Outside the diffed range:
static const Object _unset = Object();
DecodeOptions copyWith({ /* ... */ Object? decoder = _unset, }) => DecodeOptions(
  /* ... */
  decoder: decoder == _unset ? _decoder : (decoder as Function?),
);
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0acf28d and 9acc6b7.

📒 Files selected for processing (1)
  • lib/src/models/decode_options.dart (5 hunks)
⏰ 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). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
lib/src/models/decode_options.dart (4)

4-4: LGTM: DecodeKind import correctly integrated.

Import aligns with the new key/value-aware decoding path and is used consistently below.


41-49: Back-compat decoder typedefs look solid.

Good coverage of legacy shapes and the “kind-only” variant. This will smooth migration for existing consumers.


71-74: LGTM: allowDots implied by decodeDotInKeys.

Initialiser captures the implication without leaking option state.


283-302: Verify inclusion of _decoder in props is intended.

Equatable will treat two otherwise identical options with different function identities as non-equal. This can be desirable (identity-sensitive caching) or surprising (e.g., two equivalent closures created per call). Confirm this matches consumer expectations; otherwise consider excluding _decoder from equality and including a helpful representation in toString() instead.

If desired, also make debugging easier by surfacing the decoder’s runtime type in toString():

-      '  strictNullHandling: $strictNullHandling\n'
+      '  strictNullHandling: $strictNullHandling,\n'
+      '  decoder: ${_decoder?.runtimeType}\n'

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/src/models/decode_options.dart (2)

240-261: Bug: copyWith’s decoder parameter type is too narrow and inconsistent

copyWith currently accepts dynamic Function(String?)? decoder, but the constructor takes Function? decoder. This prevents passing decoders with named parameters through copyWith, and is inconsistent with the stored type.

Apply this diff to align the API and avoid compile-time/type issues:

-    dynamic Function(String?)? decoder,
+    Function? decoder,

283-303: Equatable props omit throwOnLimitExceeded

This affects equality and could cause subtle bugs when caching or comparing options.

Apply this diff to include the missing field:

   List<Object?> get props => [
@@
         strictDepth,
         strictNullHandling,
+        throwOnLimitExceeded,
         _decoder,
       ];
🧹 Nitpick comments (4)
test/unit/decode_test.dart (1)

2143-2158: Optional: tighten the charset-sentinel observation

You currently assert the presence of at least one latin1 observation. If you want to be more deterministic (and future-proof accidental changes), consider asserting sequence around the sentinel, e.g., that observations after the sentinel reflect latin1 until overridden again.

lib/src/models/decode_options.dart (3)

52-71: Constructor now accepts Function? decoder for flexibility

This enables mixed shapes and callable objects. Be aware: you lose some compile-time safety, but tests demonstrate the dynamic ladder compensates adequately.

If you want a middle ground, you could additionally provide a strongly-typed Decoder? named constructor parameter and keep Function? as an escape hatch (second param), prioritising the typed one when both are supplied.


164-220: Dynamic invocation ladder is robust; only catches shape errors

You correctly prefer typed variants and catch only NoSuchMethodError/TypeError for signature mismatches, allowing user-thrown errors to surface. Sensible defaulting of charset on fallback.

You can reduce repetition and future mistakes by hoisting final Encoding _cs = charset ?? this.charset; and using _cs in all Utils.decode fallbacks.


263-281: toString omits throwOnLimitExceeded

Including it aids debugging and parity with other options you already print.

Apply this diff to include the field:

   String toString() => 'DecodeOptions(\n'
@@
-      '  strictNullHandling: $strictNullHandling\n'
+      '  strictNullHandling: $strictNullHandling,\n'
+      '  throwOnLimitExceeded: $throwOnLimitExceeded\n'
       ')';
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9acc6b7 and 157d7be.

📒 Files selected for processing (2)
  • lib/src/models/decode_options.dart (5 hunks)
  • test/unit/decode_test.dart (1 hunks)
⏰ 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). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (9)
test/unit/decode_test.dart (8)

2019-2035: Good coverage: key/value-aware decoder invocation order is validated

The test asserts KEY then VALUE per pair and exercises the new decoder signature with kind. Looks correct and stable.


2037-2041: Backward compatibility preserved for legacy single-arg decoders

Nice sanity check that older decoders (value-only) continue to work.


2042-2047: Decoder variant accepting only kind works as intended

This proves the flexible signature. Clear and useful.


2049-2063: Options isolation guardrail validated

This reliably exercises the “string-input-only” list guard and verifies no leakage across calls. Solid test.


2066-2077: KEY path for bare keys is correctly surfaced to custom decoders

Sound assertion that only KEY is observed when strictNullHandling is true and no value is present.


2079-2092: Comma splitting correctly emits VALUE per segment

The order expectation (key, then each value segment) is appropriate.


2176-2195: Dynamic fallback tests (_Loose1/_Loose2) cover edge signatures well

Great use of callable objects to exercise mismatched named parameters and required named-parameter failure paths. Clear signals that the ladder and final fallback work.

Also applies to: 2201-2216


2019-2024: DecodeKind is correctly re-exported by src/qs.dart

The file lib/src/qs.dart (line 17) exports src/enums/decode_kind.dart, and your tests import package:qs_dart/src/qs.dart, so DecodeKind is available without any additional imports. No changes needed.

lib/src/models/decode_options.dart (1)

31-49: Typedef matrix for decoder variants looks good

The set of Decoder/Decoder1/Decoder2/Decoder3 covers the intended shapes. Returning dynamic is consistent with existing decode behaviour.

@techouse techouse marked this pull request as draft August 20, 2025 06:13
@techouse techouse marked this pull request as ready for review August 20, 2025 06:43
@techouse techouse merged commit b440718 into main Aug 20, 2025
12 of 14 checks passed
@techouse techouse deleted the fix/decode-dot-in-keys branch August 20, 2025 06:43
@coderabbitai coderabbitai bot mentioned this pull request Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant