11import 'dart:convert' show Encoding, latin1, utf8;
22
33import 'package:equatable/equatable.dart' ;
4+ import 'package:qs_dart/src/enums/decode_kind.dart' ;
45import 'package:qs_dart/src/enums/duplicates.dart' ;
56import 'package:qs_dart/src/utils.dart' ;
67
@@ -27,28 +28,30 @@ import 'package:qs_dart/src/utils.dart';
2728/// See also: the options types in other ports for parity, and the individual
2829/// doc comments below for precise semantics.
2930
30- /// Signature for a custom scalar decoder used by [DecodeOptions] .
31+ /// Preferred signature for a custom scalar decoder used by [DecodeOptions] .
3132///
32- /// The function receives a single raw token (already split from the query)
33- /// and an optional [charset] hint (either [utf8] or [latin1] ). The [charset]
34- /// reflects the *effective* charset after any sentinel (`utf8=✓` ) handling.
35- ///
36- /// Return the decoded value for the token — typically a `String` , `num` ,
37- /// `bool` , or `null` . If no decoder is provided, the library falls back to
38- /// [Utils.decode] .
39- ///
40- /// Notes
41- /// - This hook runs on **individual tokens only**; do not parse brackets,
42- /// delimiters, or build containers here.
43- /// - If you throw from this function, the error will surface out of
44- /// [QS.decode] .
45- typedef Decoder = dynamic Function (String ? value, {Encoding ? charset});
33+ /// Implementations may choose to ignore [charset] or [kind] , but both are
34+ /// provided to enable key-aware decoding when desired.
35+ typedef Decoder = dynamic Function (
36+ String ? value, {
37+ Encoding ? charset,
38+ DecodeKind ? kind,
39+ });
40+
41+ /// Back-compat: decoder with optional [charset] only.
42+ typedef Decoder1 = dynamic Function (String ? value, {Encoding ? charset});
43+
44+ /// Decoder that accepts only [kind] (no [charset] ).
45+ typedef Decoder2 = dynamic Function (String ? value, {DecodeKind ? kind});
46+
47+ /// Back-compat: single-argument decoder (value only).
48+ typedef Decoder3 = dynamic Function (String ? value);
4649
4750/// Options that configure the output of [QS.decode] .
4851final class DecodeOptions with EquatableMixin {
4952 const DecodeOptions ({
5053 bool ? allowDots,
51- Decoder ? decoder,
54+ Function ? decoder,
5255 bool ? decodeDotInKeys,
5356 this .allowEmptyLists = false ,
5457 this .listLimit = 20 ,
@@ -65,7 +68,7 @@ final class DecodeOptions with EquatableMixin {
6568 this .strictDepth = false ,
6669 this .strictNullHandling = false ,
6770 this .throwOnLimitExceeded = false ,
68- }) : allowDots = allowDots ?? decodeDotInKeys == true || false ,
71+ }) : allowDots = allowDots ?? ( decodeDotInKeys ?? false ) ,
6972 decodeDotInKeys = decodeDotInKeys ?? false ,
7073 _decoder = decoder,
7174 assert (
@@ -156,13 +159,65 @@ final class DecodeOptions with EquatableMixin {
156159
157160 /// Optional custom scalar decoder for a single token.
158161 /// If not provided, falls back to [Utils.decode] .
159- final Decoder ? _decoder;
162+ final Function ? _decoder;
163+
164+ /// Decode a single scalar using either the custom decoder or the default
165+ /// implementation in [Utils.decode] . The [kind] indicates whether the token
166+ /// is a key (or key segment) or a value.
167+ dynamic decoder (
168+ String ? value, {
169+ Encoding ? charset,
170+ DecodeKind kind = DecodeKind .value,
171+ }) {
172+ final Function ? fn = _decoder;
173+
174+ // If no custom decoder is provided, use the default decoding logic.
175+ if (fn == null ) {
176+ return Utils .decode (value, charset: charset ?? this .charset);
177+ }
178+
179+ // Prefer strongly-typed variants first
180+ if (fn is Decoder ) return fn (value, charset: charset, kind: kind);
181+ if (fn is Decoder1 ) return fn (value, charset: charset);
182+ if (fn is Decoder2 ) return fn (value, kind: kind);
183+ if (fn is Decoder3 ) return fn (value);
160184
161- /// Decode a single scalar using either the custom [Decoder] or the default
162- /// implementation in [Utils.decode] .
163- dynamic decoder (String ? value, {Encoding ? charset}) => _decoder is Function
164- ? _decoder? .call (value, charset: charset)
165- : Utils .decode (value, charset: charset);
185+ // Dynamic callable or class with `call` method
186+ try {
187+ // Try full shape (value, {charset, kind})
188+ return (fn as dynamic )(value, charset: charset, kind: kind);
189+ } on NoSuchMethodError catch (_) {
190+ // fall through
191+ } on TypeError catch (_) {
192+ // fall through
193+ }
194+ try {
195+ // Try (value, {charset})
196+ return (fn as dynamic )(value, charset: charset);
197+ } on NoSuchMethodError catch (_) {
198+ // fall through
199+ } on TypeError catch (_) {
200+ // fall through
201+ }
202+ try {
203+ // Try (value, {kind})
204+ return (fn as dynamic )(value, kind: kind);
205+ } on NoSuchMethodError catch (_) {
206+ // fall through
207+ } on TypeError catch (_) {
208+ // fall through
209+ }
210+ try {
211+ // Try (value)
212+ return (fn as dynamic )(value);
213+ } on NoSuchMethodError catch (_) {
214+ // Fallback to default
215+ return Utils .decode (value, charset: charset ?? this .charset);
216+ } on TypeError catch (_) {
217+ // Fallback to default
218+ return Utils .decode (value, charset: charset ?? this .charset);
219+ }
220+ }
166221
167222 /// Return a new [DecodeOptions] with the provided overrides.
168223 DecodeOptions copyWith ({
@@ -182,7 +237,7 @@ final class DecodeOptions with EquatableMixin {
182237 bool ? parseLists,
183238 bool ? strictNullHandling,
184239 bool ? strictDepth,
185- Decoder ? decoder,
240+ dynamic Function ( String ? ) ? decoder,
186241 }) =>
187242 DecodeOptions (
188243 allowDots: allowDots ?? this .allowDots,
0 commit comments