From 1cf927ec9c88f4cad0909efd76da056617f642c7 Mon Sep 17 00:00:00 2001 From: Oleksii Shtanko Date: Thu, 18 Jul 2024 12:43:47 +0300 Subject: [PATCH] Add nullable map and future extensions --- CHANGELOG.md | 4 ++ README.md | 12 +++++ example/nullx_example.dart | 105 ++++++++++++++++++++++++++++++++++++- lib/nullx.dart | 2 + lib/src/future.dart | 89 +++++++++++++++++++++++++++++++ lib/src/map.dart | 63 ++++++++++++++++++++++ pubspec.yaml | 2 +- test/future_test.dart | 85 ++++++++++++++++++++++++++++++ test/map_test.dart | 93 ++++++++++++++++++++++++++++++++ 9 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 lib/src/future.dart create mode 100644 lib/src/map.dart create mode 100644 test/future_test.dart create mode 100644 test/map_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 867e490..5b38002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.7 +- Add nullable map extensions +- Add nullable future extensions + ## 0.1.6 - Add `filterNotNullTo`, `filterNotNull`, `listOfNotNull`, `ifNull` functions and extensions - Add unit tests diff --git a/README.md b/README.md index c5e5997..c912414 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,18 @@ void main() { } ``` +```dart +void main() { + final Map? nullableMap = {'a': 1, 'b': 2}; + print(nullableMap.isNullOrEmpty); // false + print(nullableMap.getOrElse('c', 0)); // 0 + nullableMap.putIfAbsentOrElse('c', 3); // {a: 1, b: 2, c: 3} + nullableMap.updateValue('a', (value) => value! + 10); // {a: 11, b: 2, c: 3} + final filteredMap = nullableMap.filter((entry) => entry.value > 2); + print(filteredMap); // {a: 11, c: 3} +} +``` + ## Contributing Contributions are welcome! Please read the contributing guide to learn how to contribute to the project and set up a development environment. diff --git a/example/nullx_example.dart b/example/nullx_example.dart index 473f79f..7d3227f 100644 --- a/example/nullx_example.dart +++ b/example/nullx_example.dart @@ -1,6 +1,6 @@ import 'package:nullx/nullx.dart'; -void main() { +void main() async { /// Variables // ignore: unnecessary_nullable_for_final_variable_declarations @@ -13,6 +13,12 @@ void main() { const String? nullString = null; const double? nullDouble = null; const bool? nullBool = null; + // ignore: unnecessary_nullable_for_final_variable_declarations + final Map? nullableMap = {'a': 1, 'b': 2}; + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? nullableFuture = Future.value(42); + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? failedFuture = Future.error(Exception('Failed')); // ignore: unnecessary_nullable_for_final_variable_declarations final List? nullableIntList = [1, null, 3, null]; @@ -183,6 +189,103 @@ void main() { // Performs an operation on the age if it's not null age.let((a) => a); + // Check if the map is null or empty + // ignore: avoid_print + print(nullableMap.isNullOrEmpty); // false + + // Get value for key or return default + // ignore: avoid_print + print(nullableMap.getOrElse('c', 0)); // 0 + + // Put a value if the key is absent + nullableMap.putIfAbsentOrElse('c', 3); + // ignore: avoid_print + print(nullableMap); // {a: 1, b: 2, c: 3} + + // Update a value using a function + nullableMap.updateValue('a', (value) => value! + 10); + // ignore: avoid_print + print(nullableMap); // {a: 11, b: 2, c: 3} + // ignore: avoid_print + + // Filter the map + final filteredMap = nullableMap.filter((entry) => entry.value > 2); + // ignore: avoid_print + print(filteredMap); // {a: 11, c: 3} + + // Map keys and values + final mappedMap = nullableMap.mapKeysAndValues( + (entry) => MapEntry(entry.key.toUpperCase(), entry.value.toString()), + ); + // ignore: avoid_print + print(mappedMap); // {A: 11, B: 2, C: 3} + + // Iterate through the map + // ignore: avoid_print + nullableMap.forEachEntry((key, value) => print('$key: $value')); + // Output: + // a: 11 + // b: 2 + // c: 3 + + // Check if the map contains a key or value + // ignore: avoid_print + print(nullableMap.containsKeyOrNull('a')); // true + // ignore: avoid_print + print(nullableMap.containsValueOrNull(4)); // false + + // Return a default value if the Future completes with null + final int result2 = await nullableFuture.orDefault(5); + // ignore: avoid_print + print(result2); // 42 + + // Return null if the Future completes with an error + final int? errorHandled = await failedFuture.onErrorReturnNull(); + // ignore: avoid_print + print(errorHandled); // null + + // Return a default value if the Future completes with an error + final int? errorHandledWithValue = await failedFuture.onErrorReturn(5); + // ignore: avoid_print + print(errorHandledWithValue); // 5 + + // Provide an alternative Future if the original completes with null + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? nullableFutureWithNull = Future.value(); + final int alternative = await nullableFutureWithNull.orElse(() async => 99); + // ignore: avoid_print + print(alternative); // 99 + + // Execute an action when the Future completes + // ignore: avoid_print + await nullableFuture.whenComplete(() => print('Completed')); // Completed + + // Ignore any errors the Future may throw + await failedFuture.ignoreErrors(); // No output, error ignored + + // Timeout a Future and return null if it doesn't complete in time + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? slowFuture = + Future.delayed(const Duration(seconds: 2), () => 10); + final int? timedOut = + await slowFuture.timeoutWithNull(const Duration(seconds: 1)); + // ignore: avoid_print + print(timedOut); // null + + // Chain another Future using thenOrNull + final Future chained = + nullableFuture.thenOrNull((value) => Future.value('Value: $value')); + // ignore: avoid_print + print(await chained); // Value: 42 + + // Catch an error and return null using catchErrorOrNull + final int? caughtError = await failedFuture.catchErrorOrNull((error) { + // ignore: avoid_print + print('Caught error: $error'); + }); + // ignore: avoid_print + print(caughtError); // Caught error: Exception: Failed, null + // Throws a [NotImplementedError] indicating that an operation is try { todo(); diff --git a/lib/nullx.dart b/lib/nullx.dart index e7dc3ef..51eae91 100644 --- a/lib/nullx.dart +++ b/lib/nullx.dart @@ -5,5 +5,7 @@ library nullx; export 'src/collections.dart'; export 'src/exception.dart'; +export 'src/future.dart'; +export 'src/map.dart'; export 'src/types.dart'; export 'src/utils.dart'; diff --git a/lib/src/future.dart b/lib/src/future.dart new file mode 100644 index 0000000..e36736f --- /dev/null +++ b/lib/src/future.dart @@ -0,0 +1,89 @@ +import 'dart:async'; + +/// Extension on `Future?` providing additional null-aware and error +/// handling operations. +/// +/// This extension adds methods to nullable `Future` objects, allowing for +/// more expressive handling of asynchronous operations that could result in +/// null or error states. It includes methods for providing default values, +/// handling errors gracefully, and more nuanced manipulations like executing +/// alternative futures or suppressing errors. +extension NullableFutureExtensions on Future? { + /// Returns the future's value if not null; otherwise, returns a default value + Future orDefault(T defaultValue) async => (await this) ?? defaultValue; + + /// Returns the future's value, or null if the future itself is null. + Future orNull() async => await this; + + /// Attempts to return the future's value; on error, returns a specified value + Future onErrorReturn(T value) async { + try { + return await this; + } catch (e) { + return value; + } + } + + /// Attempts to return the future's value; on error, returns null. + Future onErrorReturnNull() async { + try { + return await this; + } catch (e) { + return null; + } + } + + /// Returns the future's value if not null; otherwise, executes an alternative + /// future. + Future orElse(Future Function() alternative) async { + return (await this) ?? await alternative(); + } + + /// Executes a specified action when the future completes, regardless of the + /// outcome. + Future whenComplete(Function() action) async { + try { + return await this; + } finally { + action(); + } + } + + /// Suppresses any errors that occur during the future's execution. + Future ignoreErrors() async { + try { + await this; + } catch (_) {} + } + + /// Returns null if the future does not complete within a specified duration. + Future timeoutWithNull(Duration duration) async { + try { + return await this?.timeout(duration); + } catch (e) { + return null; + } + } + + /// Applies a transformation to the future's value if not null. + Future thenOrNull(FutureOr Function(T?) onValue) async { + return this == null ? null : await this!.then(onValue); + } + + /// Attempts to return the future's value; on error, executes an onError + /// function and returns null. + Future catchErrorOrNull( + Function(Object) onError, { + bool Function(Object)? test, + }) async { + try { + return await this; + } catch (e) { + if (test == null || test(e)) { + onError(e); + return null; + } + rethrow; + } + } +} diff --git a/lib/src/map.dart b/lib/src/map.dart new file mode 100644 index 0000000..9ea5942 --- /dev/null +++ b/lib/src/map.dart @@ -0,0 +1,63 @@ +/// Extension on `Map?` providing additional null-aware operations. +/// +/// This extension adds methods to `Map` objects that allow for more expressive +/// handling of operations that could involve `null` maps. It includes methods +/// for checking if a map is null or empty, providing default values, and more +/// nuanced manipulations like filtering, updating, and transforming maps safely +extension NullableMapExtensions on Map? { + /// Checks if the map is null or empty. + bool get isNullOrEmpty => this == null || this!.isEmpty; + + /// Checks if the map is not null and not empty. + bool get isNotNullOrEmpty => !isNullOrEmpty; + + /// Returns the map if it's not null, otherwise returns the provided default + /// value. + Map orDefault(Map defaultValue) => this ?? defaultValue; + + /// Returns the value for the given key if it exists, otherwise returns the + /// provided default value. + V getOrElse(K key, V defaultValue) => this?[key] ?? defaultValue; + + /// Puts the default value for the given key if the key is absent in the map. + void putIfAbsentOrElse(K key, V defaultValue) { + if (this != null) { + this!.putIfAbsent(key, () => defaultValue); + } + } + + /// Updates the value for the given key using the provided update function. + void updateValue(K key, V Function(V?) update) { + if (this != null) { + this![key] = update(this![key]); + } + } + + /// Returns a new map containing the entries that satisfy the provided test. + Map filter(bool Function(MapEntry) test) { + if (this == null) return {}; + return Map.fromEntries(this!.entries.where(test)); + } + + /// Maps the keys and values of the map using the provided convert function + /// and returns a new map. + Map mapKeysAndValues( + MapEntry Function(MapEntry) convert, + ) { + if (this == null) return {}; + return Map.fromEntries(this!.entries.map(convert)); + } + + /// Performs an action for each key-value pair in the map. + void forEachEntry(void Function(K key, V value) action) { + if (this != null) { + this!.forEach(action); + } + } + + /// Checks if a key exists in the map, safely handling null maps. + bool containsKeyOrNull(K key) => this?.containsKey(key) ?? false; + + /// Checks if a value exists in the map, safely handling null maps. + bool containsValueOrNull(V value) => this?.containsValue(value) ?? false; +} diff --git a/pubspec.yaml b/pubspec.yaml index e526f7c..e33586d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: nullx homepage: https://shtanko.dev description: >- nullx is a collection of elegant extensions for handling null types in Dart. -version: 0.1.6 +version: 0.1.7 repository: https://github.com/ashtanko/nullx topics: diff --git a/test/future_test.dart b/test/future_test.dart new file mode 100644 index 0000000..ea95fdc --- /dev/null +++ b/test/future_test.dart @@ -0,0 +1,85 @@ +import 'package:nullx/nullx.dart'; +import 'package:test/test.dart'; + +void main() { + group('NullableFutureExtensions', () { + test('orDefault returns default value when future is null', () async { + Future? nullFuture; + expect(await nullFuture.orDefault(10), equals(10)); + }); + + test('orDefault returns future value when not null', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future.value(5); + expect(await future.orDefault(10), equals(5)); + }); + + test('orNull returns null when future is null', () async { + Future? nullFuture; + expect(await nullFuture.orNull(), isNull); + }); + + test('onErrorReturn returns provided value on error', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future(() => throw Exception()); + expect(await future.onErrorReturn(10), equals(10)); + }); + + test('onErrorReturnNull returns null on error', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future(() => throw Exception()); + expect(await future.onErrorReturnNull(), isNull); + }); + + test('orElse executes alternative future when original is null', () async { + Future? nullFuture; + expect(await nullFuture.orElse(() => Future.value(10)), equals(10)); + }); + + test('whenComplete executes action regardless of outcome', () async { + bool actionExecuted = false; + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future.value(5); + await future.whenComplete(() => actionExecuted = true); + expect(actionExecuted, isTrue); + }); + + test('ignoreErrors suppresses any errors', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future(() => throw Exception()); + await future.ignoreErrors(); + // Expectation: no uncaught exceptions + }); + + test('timeoutWithNull returns null on timeout', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = + Future.delayed(const Duration(seconds: 2)); + expect( + await future.timeoutWithNull(const Duration(milliseconds: 1)), + isNull, + ); + }); + + test('thenOrNull applies transformation when future is not null', () async { + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future.value(5); + expect( + await future.thenOrNull((value) => Future.value(value! * 2)), + equals(10), + ); + }); + + test('catchErrorOrNull returns null and executes onError on error', + () async { + bool onErrorExecuted = false; + // ignore: unnecessary_nullable_for_final_variable_declarations + final Future? future = Future(() => throw Exception()); + expect( + await future.catchErrorOrNull((_) => onErrorExecuted = true), + isNull, + ); + expect(onErrorExecuted, isTrue); + }); + }); +} diff --git a/test/map_test.dart b/test/map_test.dart new file mode 100644 index 0000000..3c2f333 --- /dev/null +++ b/test/map_test.dart @@ -0,0 +1,93 @@ +import 'package:nullx/nullx.dart'; +import 'package:test/test.dart'; + +void main() { + group('NullableMapExtensions', () { + Map? nullableMap; + setUp(() { + nullableMap = {'a': 1, 'b': 2}; + }); + + test('isNotNullOrEmpty returns true for non-empty map', () { + expect(nullableMap.isNotNullOrEmpty, isTrue); + }); + + test('isNotNullOrEmpty returns false for null map', () { + nullableMap = null; + expect(nullableMap.isNotNullOrEmpty, isFalse); + }); + + test('orDefault returns same map if not null', () { + final result = nullableMap.orDefault({'c': 3}); + expect(result, equals({'a': 1, 'b': 2})); + }); + + test('orDefault returns default map if null', () { + nullableMap = null; + final result = nullableMap.orDefault({'c': 3}); + expect(result, equals({'c': 3})); + }); + + test('getOrElse returns value for existing key', () { + final value = nullableMap.getOrElse('a', 0); + expect(value, equals(1)); + }); + + test('getOrElse returns default value for non-existing key', () { + final value = nullableMap.getOrElse('c', 0); + expect(value, equals(0)); + }); + + test('putIfAbsentOrElse adds new key-value if key does not exist', () { + nullableMap.putIfAbsentOrElse('c', 3); + expect(nullableMap, containsPair('c', 3)); + }); + + test('putIfAbsentOrElse does not overwrite existing key', () { + nullableMap.putIfAbsentOrElse('a', 0); + expect(nullableMap, containsPair('a', 1)); + }); + + test('updateValue updates value for existing key', () { + nullableMap.updateValue('a', (oldValue) => oldValue! + 1); + expect(nullableMap, containsPair('a', 2)); + }); + + test('filter retains only entries that match the test', () { + final result = nullableMap.filter((entry) => entry.key == 'a'); + expect(result.length, equals(1)); + expect(result, containsPair('a', 1)); + }); + + test('mapKeysAndValues transforms map entries', () { + final result = nullableMap.mapKeysAndValues( + (entry) => MapEntry(entry.key.toUpperCase(), entry.value * 2), + ); + expect(result, equals({'A': 2, 'B': 4})); + }); + + test('forEachEntry executes action for each map entry', () { + final List keys = []; + nullableMap.forEachEntry((key, value) { + keys.add(key); + }); + expect(keys, containsAll(['a', 'b'])); + }); + + test('containsKeyOrNull returns true for existing key', () { + expect(nullableMap.containsKeyOrNull('a'), isTrue); + }); + + test('containsKeyOrNull returns false for non-existing key', () { + expect(nullableMap.containsKeyOrNull('c'), isFalse); + }); + + test('containsValueOrNull returns true for existing value', () { + expect(nullableMap.containsValueOrNull(1), isTrue); + }); + + test('containsValueOrNull returns false for non-existing value', () { + expect(nullableMap.containsValueOrNull(3), isFalse); + }); + }); +}