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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 1.9.31

- Bug fixes:
- `ConditionSQLEncoder.valueToParameterValue`: fixed encoding of a `List` of values containing `ConditionParameter`s; each element is now resolved individually instead of passing the whole list to every element.
- `ConditionEncoder.resolveValueToType`: fixed resolution of a single-element `Iterable` to a primitive type (was a no-op comparison instead of an assignment, leaving the value as a `List`).
- `MapGetterExtension.matchKeyIgnoreCase`: fixed case-insensitive key matching that always returned `null` (empty loop body); now returns the matching key. Also fixes `setMultiValue(..., ignoreCase: true)`.
- `Time`: `millisecond`/`microsecond` range validation now correctly rejects `1000` (valid range is `0..999`).
- `Time._bytesInStringFormat`: fixed the second-byte digit check that was effectively disabled (`length < 2` instead of `length >= 2`).
- `APISession.isExpired`: now honors the provided `now` argument instead of always using `DateTime.now()`.
- `APIServerResponseCache` cached entry: `replaceFileStat` no longer compares a variable to itself (`identical(myFileStat, myFileStat)`), so the file stat is correctly replaced.
- `WeakList.set`: now increments the internal modification counter, consistent with the other mutating methods.

- Tests:
- Added tests covering the bug fixes above (`Time` range/string parsing, `matchKeyIgnoreCase`/`getIgnoreCase`/`setMultiValue`, `APISession.isExpired`, and `ConditionSQLEncoder`/`ConditionEncoder` value resolution).

## 1.9.30

- `APIRootStarter`:
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ typedef APILogger =
/// Bones API Library class.
class BonesAPI {
// ignore: constant_identifier_names
static const String VERSION = '1.9.30';
static const String VERSION = '1.9.31';

static bool _boot = false;

Expand Down
4 changes: 3 additions & 1 deletion lib/src/bones_api_condition_encoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1543,14 +1543,16 @@ abstract class ConditionEncoder {
if (listLength == 0) {
return null;
} else if (listLength == 1) {
value == list.first;
value = list.first;
} else {
throw ArgumentError(
"Can't resolve a `List` with mutiple values to a primitive type: $valueTypeInfo >> $value",
);
}
}

if (value == null) return null;

var parsedValue = valueParser(value);
if (parsedValue != null) {
return parsedValue;
Expand Down
4 changes: 2 additions & 2 deletions lib/src/bones_api_condition_sql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -633,15 +633,15 @@ class ConditionSQLEncoder extends ConditionEncoder {
value.map((v) {
if (v is ConditionParameter) {
return conditionParameterToParameterValue(
value,
v,
context,
fieldKey,
fieldType,
valueAsList: valueAsList,
);
} else {
return _valueToParameterValueImpl(
value,
v,
fieldType,
key: fieldKey,
valueAsList: valueAsList,
Expand Down
4 changes: 3 additions & 1 deletion lib/src/bones_api_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ extension MapGetterExtension<K, V> on Map<K, V> {
var keyStr = key.toString();

for (var k in keys) {
if (equalsIgnoreAsciiCase(keyStr, k.toString())) {}
if (equalsIgnoreAsciiCase(keyStr, k.toString())) {
return k;
}
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_server_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ abstract class _CachedResponse
if (fileStat == null) return;

var myFileStat = _fileStat;
if (identical(myFileStat, myFileStat)) return;
if (identical(myFileStat, fileStat)) return;

if (myFileStat == null || myFileStat.file == fileStat.file) {
fileStat.updateFrom(myFileStat);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class APISession {
bool isExpired(Duration timeout, {DateTime? now}) {
now ??= DateTime.now();

var elapsedTime = lastAccessElapsedTime;
var elapsedTime = now.difference(lastAccessTime);
return elapsedTime.compareTo(timeout) > 0;
}

Expand Down
1 change: 0 additions & 1 deletion lib/src/bones_api_test_utils_freeport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:docker_commander/docker_commander_vm.dart';

final Set<int> _initFreePorts = <int>{};

@override
Future<int> resolveFreePort(int port) {
var startPort = port - 100;
var endPort = port + 100;
Expand Down
6 changes: 3 additions & 3 deletions lib/src/bones_api_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ class Time implements Comparable<Time> {
throw ArgumentError.value(second, 'second', 'Not in range: 0..59');
}

if (millisecond < 0 || millisecond > 1000) {
if (millisecond < 0 || millisecond > 999) {
throw ArgumentError.value(
millisecond,
'millisecond',
'Not in range: 0..999',
);
}

if (microsecond < 0 || microsecond > 1000) {
if (microsecond < 0 || microsecond > 999) {
throw ArgumentError.value(
microsecond,
'microsecond',
Expand Down Expand Up @@ -122,7 +122,7 @@ class Time implements Comparable<Time> {
if (value.isEmpty) return false;

if (!_isDigitByte(value[0])) return false;
if (value.length < 2 && !_isDigitByte(value[1])) return false;
if (value.length >= 2 && !_isDigitByte(value[1])) return false;

if (value.length == 8 &&
value[2] == _charColon &&
Expand Down
2 changes: 2 additions & 0 deletions lib/src/bones_api_utils_weaklist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class WeakList<E extends Object> extends ListBase<E?> {
_entries[index] = WeakReference(value);
}

++_modCount;

return prevWeakRef.target;
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: bones_api
description: Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters.
version: 1.9.30
version: 1.9.31
homepage: https://github.com/Colossus-Services/bones_api

environment:
Expand Down
42 changes: 42 additions & 0 deletions test/bones_api_condition_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,48 @@ void main() {
TableFieldReference('account', 'address', int, 'address', 'id', int),
});
}

{
// An `IN` list mixing a `ConditionParameter` and a literal value.
// Each element must be encoded individually (not the whole list).
var cond = KeyConditionIN(
[ConditionKeyField('id')],
[ConditionParameter.key('a'), 99],
);

var sql = await sqlEncoder.encode(
cond,
'account',
parameters: {'a': 7},
);

expect(
sql.outputString,
equals('( "ac"."id" IN ( ( @a__0 ) , ( 99 ) ) )'),
);
expect(sql.parametersPlaceholders, equals({'a__0': 7}));
}
});

test('resolveValueToType single-element list', () async {
var enc = ConditionSQLEncoder(
_TestSchemeProvider(),
sqlElementQuote: '"',
);

// A single-element `Iterable` must be resolved to its element when the
// target type is a primitive:
expect(await enc.resolveValueToType([42], int), equals(42));
expect(await enc.resolveValueToType(['abc'], String), equals('abc'));

// An empty `Iterable` resolves to `null`:
expect(await enc.resolveValueToType([], int), isNull);

// A plain value is resolved as-is:
expect(await enc.resolveValueToType(7, int), equals(7));

// A multi-element list can't be resolved to a primitive:
expect(() => enc.resolveValueToType([1, 2], int), throwsArgumentError);
});
});

Expand Down
24 changes: 24 additions & 0 deletions test/bones_api_session_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ import 'package:shared_map/shared_map.dart';
import 'package:test/test.dart';

void main() {
group('APISession', () {
test('isExpired uses provided `now`', () async {
var session = APISession('s1');

var lastAccess = session.lastAccessTime;
var timeout = Duration(seconds: 10);

// Within the timeout window:
expect(
session.isExpired(timeout, now: lastAccess.add(Duration(seconds: 5))),
isFalse,
);
// Exactly at the timeout (elapsed == timeout is NOT expired):
expect(session.isExpired(timeout, now: lastAccess.add(timeout)), isFalse);
// Past the timeout window:
expect(
session.isExpired(timeout, now: lastAccess.add(Duration(seconds: 11))),
isTrue,
);
// A `now` before/at the last access is never expired:
expect(session.isExpired(timeout, now: lastAccess), isFalse);
});
});

group('APISessionSet', () {
final sharedStoreRef = SharedStore("t1").sharedReference();

Expand Down
31 changes: 31 additions & 0 deletions test/bones_api_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,37 @@ void main() {
);
});

test('range validation', () async {
// Valid boundaries:
expect(Time(0, 0, 0, 0, 0).toString(), equals('00:00:00'));
expect(Time(23, 59, 59, 999, 999).toString(), equals('23:59:59.999999'));

// millisecond/microsecond must be in range 0..999 (1000 is invalid):
expect(() => Time(0, 0, 0, 1000), throwsArgumentError);
expect(() => Time(0, 0, 0, -1), throwsArgumentError);
expect(() => Time(0, 0, 0, 0, 1000), throwsArgumentError);
expect(() => Time(0, 0, 0, 0, -1), throwsArgumentError);

expect(() => Time(0, 60), throwsArgumentError);
expect(() => Time(0, 0, 60), throwsArgumentError);
});

test('fromBytes string format', () async {
// 8-byte ASCII string "20:11:31" must be parsed as a string, not as
// binary-encoded microseconds:
expect(Time.fromBytes('20:11:31'.codeUnits), equals(Time(20, 11, 31)));

expect(
Time.fromBytes('20:11:31.123'.codeUnits),
equals(Time(20, 11, 31, 123)),
);

expect(
Time.fromBytes('20:11:31.123456'.codeUnits),
equals(Time(20, 11, 31, 123, 456)),
);
});

test('(all combinations 32-bits)', () async {
for (var h = 0; h <= 23; ++h) {
for (var m = 0; m <= 59; ++m) {
Expand Down
35 changes: 35 additions & 0 deletions test/bones_api_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,41 @@ void main() {
);
});
});

group('MapGetterExtension', () {
test('matchKeyIgnoreCase', () async {
var map = {'Foo': 1, 'BAR': 2};

// Exact match:
expect(map.matchKeyIgnoreCase('Foo'), equals('Foo'));
// Case-insensitive match should return the ORIGINAL key:
expect(map.matchKeyIgnoreCase('foo'), equals('Foo'));
expect(map.matchKeyIgnoreCase('FOO'), equals('Foo'));
expect(map.matchKeyIgnoreCase('bar'), equals('BAR'));
expect(map.matchKeyIgnoreCase('Bar'), equals('BAR'));
// No match:
expect(map.matchKeyIgnoreCase('baz'), isNull);
});

test('getIgnoreCase', () async {
var map = {'Foo': 1, 'BAR': 2};

expect(map.getIgnoreCase('foo'), equals(1));
expect(map.getIgnoreCase('bar'), equals(2));
expect(map.getIgnoreCase('baz'), isNull);
expect(map.getIgnoreCase('baz', defaultValue: -1), equals(-1));
});

test('setMultiValue ignoreCase', () async {
var map = <String, Object>{'Foo': 'a'};

map.setMultiValue('foo', 'b', ignoreCase: true);

// Should accumulate into the existing (original-case) key, not create a new one:
expect(map.keys.toList(), equals(['Foo']));
expect(map['Foo'], equals(['a', 'b']));
});
});
}

Future<T> _asyncValue<T>(T value) =>
Expand Down
Loading