Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 72 additions & 9 deletions lib/src/extensions/decode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,32 @@ extension _$Decode on QS {
),
);

static dynamic _parseArrayValue(dynamic val, DecodeOptions options) =>
val is String && val.isNotEmpty && options.comma && val.contains(',')
? val.split(',')
: val;
static dynamic _parseListValue(
dynamic val,
DecodeOptions options,
int currentListLength,
) {
if (val is String && val.isNotEmpty && options.comma && val.contains(',')) {
final List<String> splitVal = val.split(',');
if (options.throwOnLimitExceeded && splitVal.length > options.listLimit) {
throw RangeError(
'List limit exceeded. '
'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
);
}
return splitVal;
}

if (options.throwOnLimitExceeded &&
currentListLength >= options.listLimit) {
throw RangeError(
'List limit exceeded. '
'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
);
}

return val;
}

static Map<String, dynamic> _parseQueryStringValues(
String str, [
Expand All @@ -23,12 +45,27 @@ extension _$Decode on QS {
(options.ignoreQueryPrefix ? str.replaceFirst('?', '') : str)
.replaceAll(RegExp(r'%5B', caseSensitive: false), '[')
.replaceAll(RegExp(r'%5D', caseSensitive: false), ']');
final num? limit = options.parameterLimit == double.infinity

final int? limit = options.parameterLimit == double.infinity
? null
: options.parameterLimit;
: options.parameterLimit.toInt();

if (limit != null && limit <= 0) {
throw ArgumentError('Parameter limit must be a positive integer.');
}

final Iterable<String> parts = limit != null && limit > 0
? cleanStr.split(options.delimiter).take(limit.toInt())
? cleanStr
.split(options.delimiter)
.take(options.throwOnLimitExceeded ? limit + 1 : limit)
: cleanStr.split(options.delimiter);

if (options.throwOnLimitExceeded && limit != null && parts.length > limit) {
throw RangeError(
'Parameter limit exceeded. Only $limit parameter${limit == 1 ? '' : 's'} allowed.',
);
}

int skipIndex = -1; // Keep track of where the utf8 sentinel was found
int i;

Expand Down Expand Up @@ -65,7 +102,13 @@ extension _$Decode on QS {
} else {
key = options.decoder(part.slice(0, pos), charset: charset);
val = Utils.apply<dynamic>(
_parseArrayValue(part.slice(pos + 1), options),
_parseListValue(
part.slice(pos + 1),
options,
obj.containsKey(key) && obj[key] is List
? (obj[key] as List).length
: 0,
),
(dynamic val) => options.decoder(val, charset: charset),
);
}
Expand Down Expand Up @@ -102,7 +145,27 @@ extension _$Decode on QS {
DecodeOptions options,
bool valuesParsed,
) {
dynamic leaf = valuesParsed ? val : _parseArrayValue(val, options);
late final int currentListLength;

if (chain.isNotEmpty && chain.last == '[]') {
final int? parentKey = int.tryParse(chain.slice(0, -1).join(''));

currentListLength = parentKey != null &&
val is List &&
val.firstWhereIndexedOrNull((int i, _) => i == parentKey) != null
? val.elementAt(parentKey).length
: 0;
} else {
currentListLength = 0;
}

dynamic leaf = valuesParsed
? val
: _parseListValue(
val,
options,
currentListLength,
);

for (int i = chain.length - 1; i >= 0; --i) {
dynamic obj;
Expand Down
16 changes: 11 additions & 5 deletions lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import 'dart:math' show min;
import 'package:qs_dart/src/models/undefined.dart';

extension IterableExtension<T> on Iterable<T> {
/// Returns a new [Iterable] without [Undefined] elements.
Iterable<T> whereNotUndefined() => where((T el) => el is! Undefined);
/// Returns a new [Iterable] without elements of type [Q].
Iterable<T> whereNotType<Q>() => where((T el) => el is! Q);
}

extension ListExtension<T> on List<T> {
/// Returns a new [List] without [Undefined] elements.
List<T> whereNotUndefined() => where((T el) => el is! Undefined).toList();
/// Extracts a section of a list and returns a new list.
///
/// Modeled after JavaScript's `Array.prototype.slice()` method.
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
List<T> slice([int start = 0, int? end]) => sublist(
(start < 0 ? length + start : start).clamp(0, length),
(end == null ? length : (end < 0 ? length + end : end))
.clamp(0, length),
);
}

extension StringExtension on String {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/models/decode_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class DecodeOptions with EquatableMixin {
this.parseLists = true,
this.strictDepth = false,
this.strictNullHandling = false,
this.throwOnLimitExceeded = false,
}) : allowDots = allowDots ?? decodeDotInKeys == true || false,
decodeDotInKeys = decodeDotInKeys ?? false,
_decoder = decoder,
Expand Down Expand Up @@ -110,6 +111,9 @@ final class DecodeOptions with EquatableMixin {
/// Set to true to decode values without `=` to `null`.
final bool strictNullHandling;

/// Set to `true` to throw an error when the limit is exceeded.
final bool throwOnLimitExceeded;

/// Set a [Decoder] to affect the decoding of the input.
final Decoder? _decoder;

Expand Down
1 change: 1 addition & 0 deletions lib/src/qs.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert' show latin1, utf8, Encoding;
import 'dart:typed_data' show ByteBuffer;

import 'package:collection/collection.dart' show IterableExtension;
import 'package:qs_dart/src/enums/duplicates.dart';
import 'package:qs_dart/src/enums/format.dart';
import 'package:qs_dart/src/enums/list_format.dart';
Expand Down
42 changes: 15 additions & 27 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ final class Utils {
target_[target_.length] = source;
}

if (target is Set) {
target = target_.values.whereNotUndefined().toSet();
} else {
target = target_.values.whereNotUndefined().toList();
}
target = target_.values.any((el) => el is Undefined)
? SplayTreeMap.from({
for (final MapEntry<int, dynamic> entry in target_.entries)
if (entry.value is! Undefined) entry.key: entry.value,
})
: target is Set
? target_.values.toSet()
: target_.values.toList();
} else {
if (source is Iterable) {
// check if source is a list of maps and target is a list of maps
Expand All @@ -70,9 +73,11 @@ final class Utils {
}
} else {
if (target is Set) {
target = Set.of(target)..addAll(source.whereNotUndefined());
target = Set.of(target)
..addAll(source.whereNotType<Undefined>());
} else {
target = List.of(target)..addAll(source.whereNotUndefined());
target = List.of(target)
..addAll(source.whereNotType<Undefined>());
}
}
} else if (source != null) {
Expand All @@ -96,7 +101,7 @@ final class Utils {
}
} else if (source != null) {
if (target is! Iterable && source is Iterable) {
return [target, ...source.whereNotUndefined()];
return [target, ...source.whereNotType<Undefined>()];
}
return [target, source];
}
Expand All @@ -115,11 +120,11 @@ final class Utils {

return [
if (target is Iterable)
...target.whereNotUndefined()
...target.whereNotType<Undefined>()
else if (target != null)
target,
if (source is Iterable)
...(source as Iterable).whereNotUndefined()
...(source as Iterable).whereNotType<Undefined>()
else
source,
];
Expand Down Expand Up @@ -367,28 +372,11 @@ final class Utils {
}
}

_compactQueue(queue);

removeUndefinedFromMap(value);

return value;
}

static void _compactQueue(List<Map> queue) {
while (queue.length > 1) {
final Map item = queue.removeLast();
final dynamic obj = item['obj'][item['prop']];

if (obj is Iterable) {
if (obj is Set) {
item['obj'][item['prop']] = obj.whereNotUndefined().toSet();
} else {
item['obj'][item['prop']] = obj.whereNotUndefined().toList();
}
}
}
}

@visibleForTesting
static void removeUndefinedFromList(List value) {
for (int i = 0; i < value.length; i++) {
Expand Down
Loading