Skip to content

Commit

Permalink
✨ decode: add throwOnLimitExceeded option (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
techouse authored Nov 23, 2024
1 parent a46a104 commit 90f545f
Showing 8 changed files with 254 additions and 47 deletions.
81 changes: 72 additions & 9 deletions lib/src/extensions/decode.dart
Original file line number Diff line number Diff line change
@@ -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, [
@@ -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;

@@ -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),
);
}
@@ -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;
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 {
4 changes: 4 additions & 0 deletions lib/src/models/decode_options.dart
Original file line number Diff line number Diff line change
@@ -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,
@@ -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;

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';
42 changes: 15 additions & 27 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
@@ -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
@@ -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) {
@@ -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];
}
@@ -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,
];
@@ -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++) {
Loading

0 comments on commit 90f545f

Please sign in to comment.