Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ decode: add throwOnLimitExceeded option #26

Merged
merged 5 commits into from
Nov 23, 2024
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 @@
),
);

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(

Check warning on line 19 in lib/src/extensions/decode.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/extensions/decode.dart#L19

Added line #L19 was not covered by tests
'List limit exceeded. '
'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',

Check warning on line 21 in lib/src/extensions/decode.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/extensions/decode.dart#L21

Added line #L21 was not covered by tests
);
}
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 @@
(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();

techouse marked this conversation as resolved.
Show resolved Hide resolved
if (limit != null && limit <= 0) {
throw ArgumentError('Parameter limit must be a positive integer.');

Check warning on line 54 in lib/src/extensions/decode.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/extensions/decode.dart#L54

Added line #L54 was not covered by tests
}

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 @@
} 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 @@
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

Check warning on line 156 in lib/src/extensions/decode.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/extensions/decode.dart#L154-L156

Added lines #L154 - L156 were not covered by tests
techouse marked this conversation as resolved.
Show resolved Hide resolved
: 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;
techouse marked this conversation as resolved.
Show resolved Hide resolved
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 @@
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 @@
}
} 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 @@
}
} 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 @@

return [
if (target is Iterable)
...target.whereNotUndefined()
...target.whereNotType<Undefined>()

Check warning on line 123 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L123

Added line #L123 was not covered by tests
else if (target != null)
target,
if (source is Iterable)
...(source as Iterable).whereNotUndefined()
...(source as Iterable).whereNotType<Undefined>()

Check warning on line 127 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L127

Added line #L127 was not covered by tests
else
source,
];
Expand Down Expand Up @@ -367,28 +372,11 @@
}
}

_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
Loading