From 3c76fc4694f1a97a3ded068a14ce820d9950831a Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 6 Nov 2024 20:36:33 +0300 Subject: [PATCH 01/44] Option | extension methods | unwrap --- lib/src/core/option/extension_extract.dart | 219 +++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 lib/src/core/option/extension_extract.dart diff --git a/lib/src/core/option/extension_extract.dart b/lib/src/core/option/extension_extract.dart new file mode 100644 index 0000000..069402a --- /dev/null +++ b/lib/src/core/option/extension_extract.dart @@ -0,0 +1,219 @@ +import 'package:hmi_core/hmi_core_failure.dart'; +import 'package:hmi_core/hmi_core_option.dart'; +import 'package:hmi_core/hmi_core_result.dart'; +/// +/// Extracting contained values +extension Extract on Option { + /// + /// Returns the contained [Some] value, consuming the `self` value. + /// + /// Because this function may panic, its use is generally discouraged. + /// Instead, prefer to use pattern matching and handle the [`None`] + /// case explicitly, or call [`unwrap_or`], [`unwrap_or_else`], or + /// [`unwrap_or_default`]. + /// + /// [`unwrap_or`]: Option::unwrap_or + /// [`unwrap_or_else`]: Option::unwrap_or_else + /// [`unwrap_or_default`]: Option::unwrap_or_default + /// + /// Throws an [Failure] if the self value equals [`None`]. + /// + /// # Examples + /// + /// ```dart + /// final Option x = Ok(2); + /// x.unwrap(); // 2 + /// ``` + /// + /// Should throw a Failure: + /// ```dart + /// final Result x = Err("emergency failure"); + /// // throw Failure with message: "Called unwrap() on Result with error: emergency failure" + /// x.unwrap(); + /// ``` + V unwrap() { + return switch (this) { + Some(:final V value) => value, + None() => throw Failure( + message: "Called unwrap() on None", + stackTrace: StackTrace.current, + ), + }; + } + /// + /// Returns the contained [Ok] value, consuming the `this` value. + /// + /// Because this function may throw a [Failure], its use is generally discouraged. + /// Instead, prefer to use pattern matching and handle the [Err] + /// case explicitly, or call [unwrapOr], or [unwrapOrElse]; + /// + /// ### Throws an error + /// + /// Throws an [Failure] if the value is an [Err], with a failure message including the + /// passed [message], and the content of the [Err]. + /// + /// ### Examples + /// + /// Basic usage: + /// + /// ```dart + /// final Result x = Ok(2); + /// x.expect("A custom message"); // 2 + /// ``` + /// + /// Should throw a Failure: + /// ```dart + /// final Result x = Err("emergency failure"); + /// // throw Failure with message: "Testing expect: emergency failure" + /// x.expect("Testing expect"); + /// ``` + V expect(String message) { + return switch (this) { + Ok(:final V value) => value, + Err(:final E error) => throw Failure( + message: "$message: $error", + stackTrace: StackTrace.current, + ), + }; + } + /// + /// Returns the contained [Err] error, consuming the `this` value. + /// + /// ### Throws an error + /// + /// Throws an [Failure] if the value is an [Ok], with a custom failure message provided + /// by the [Ok]'s value. + /// + /// ### Examples + /// ```dart + /// final Result = Err("emergency failure"); + /// x.unwrapErr(); // "emergency failure" + /// ``` + /// + /// Should throw a Failure: + /// ```dart + /// final Result x = Ok(2); + /// // throw Failure with message: "Called unwrapErr() on Result with value: 2" + /// x.unwrapErr(); + /// ``` + E unwrapErr() { + return switch (this) { + Ok(:final V value) => throw Failure( + message: "Called unwrapErr() on Result with value: $value", + stackTrace: StackTrace.current, + ), + Err(:final E error) => error, + }; + } + /// + /// Returns the contained [Err] error, consuming the `this` value. + /// + /// ### Throws an error + /// + /// Throws an [Failure] if the value is an [Ok], with a failure message including the + /// passed message, and the content of the [Ok]. + /// + /// + /// ### Examples + /// + /// Basic usage: + /// ```dart + /// final Result x = Err("emergency failure"); + /// x.expectErr("A custom message"); // "emergency failure" + /// ``` + /// + /// Should throw a Failure: + /// ```dart + /// final Result x = Ok(2); + /// // throw Failure with message: "Testing expectErr: 2" + /// x.expectErr("Testing expectErr"); + /// ``` + E expectErr(String message) { + return switch (this) { + Ok(:final V value) => throw Failure( + message: "$message: $value", + stackTrace: StackTrace.current, + ), + Err(:final E error) => error, + }; + } + /// + /// Returns the contained [Ok] value or a provided default value [d]. + /// + /// Arguments passed to [unwrapOr] are eagerly evaluated; if you are passing + /// the result of a function call, it is recommended to use [unwrapOrElse], + /// which is lazily evaluated. + /// + /// ### Examples + /// ```dart + /// final defaultValue = 2; + /// final Result x = Ok(9); + /// x.unwrapOr(defaultValue); // 9 + /// + /// final Result y = Err("error"); + /// y.unwrapOr(defaultValue); // 2 + /// ``` + V unwrapOr(V d) { + return switch (this) { + Ok(:final V value) => value, + Err() => d, + }; + } + /// + /// Returns the contained [Ok] value or computes it from a closure [d]. + /// + /// ### Examples + /// ```dart + /// int count(String s) => s.length; + /// + /// final Result x = Ok(2); + /// x.unwrapOrElse(count); // 2 + /// + /// final Result y = Err("foo"); + /// y.unwrapOrElse(count); // 3 + /// ``` + V unwrapOrElse(V Function(E error) d) { + return switch (this) { + Ok(:final V value) => value, + Err(:final E error) => d(error), + }; + } +} +/// +/// Extracting contained values for [Ok] +extension ExtractOk on Result { + /// + /// Returns the contained [Ok] value, but never throw an error. + /// + /// It can be used as a maintainability safeguard that will fail + /// to compile if the error type of the [Result] is later changed + /// from [Never] to a type that can actually occur. + /// + /// ### Examples + /// ```dart + /// Result onlyGoodNews() => Ok("this is fine"); + /// onlyGoodNews().intoOk(); // "this is fine" + /// ``` + V intoOk() { + return (this as Ok).value; + } +} +/// +/// Extracting contained values for [Err] +extension ExtractErr on Result { + /// + /// Returns the contained [Err] value, but never throw an error. + /// + /// It can be used as a maintainability safeguard that will fail + /// to compile if the ok type of the [Result] is later changed + /// from [Never] to a type that can actually occur. + /// + /// ### Examples + /// ```dart + /// Result onlyBadNews() => Err("Oops, it failed"); + /// onlyBadNews().intoErr(); // "Oops, it failed" + /// ``` + E intoErr() { + return (this as Err).error; + } +} From fc10e49dc30f069f00450ffdf1545d89b9101bb5 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 6 Nov 2024 20:56:01 +0300 Subject: [PATCH 02/44] Option | extension methods | expect, unwrapOr, unwrapOrElse, unwrapOr --- lib/src/core/option/extension_extract.dart | 262 ++++++++++----------- 1 file changed, 127 insertions(+), 135 deletions(-) diff --git a/lib/src/core/option/extension_extract.dart b/lib/src/core/option/extension_extract.dart index 069402a..f7a4822 100644 --- a/lib/src/core/option/extension_extract.dart +++ b/lib/src/core/option/extension_extract.dart @@ -1,6 +1,5 @@ import 'package:hmi_core/hmi_core_failure.dart'; import 'package:hmi_core/hmi_core_option.dart'; -import 'package:hmi_core/hmi_core_result.dart'; /// /// Extracting contained values extension Extract on Option { @@ -41,179 +40,172 @@ extension Extract on Option { }; } /// - /// Returns the contained [Ok] value, consuming the `this` value. - /// - /// Because this function may throw a [Failure], its use is generally discouraged. - /// Instead, prefer to use pattern matching and handle the [Err] - /// case explicitly, or call [unwrapOr], or [unwrapOrElse]; - /// - /// ### Throws an error + /// Returns the contained [Some] value, consuming the `self` value. /// - /// Throws an [Failure] if the value is an [Err], with a failure message including the - /// passed [message], and the content of the [Err]. + /// # Throws an error if the value is a [None] with a custom error message provided by + /// `msg`. /// - /// ### Examples + /// # Examples /// /// Basic usage: - /// + /// /// ```dart - /// final Result x = Ok(2); + /// final Option x = Some(2); /// x.expect("A custom message"); // 2 /// ``` /// /// Should throw a Failure: + /// /// ```dart - /// final Result x = Err("emergency failure"); + /// final Option x = None(); /// // throw Failure with message: "Testing expect: emergency failure" /// x.expect("Testing expect"); /// ``` V expect(String message) { return switch (this) { - Ok(:final V value) => value, - Err(:final E error) => throw Failure( - message: "$message: $error", - stackTrace: StackTrace.current, - ), - }; - } - /// - /// Returns the contained [Err] error, consuming the `this` value. - /// - /// ### Throws an error - /// - /// Throws an [Failure] if the value is an [Ok], with a custom failure message provided - /// by the [Ok]'s value. - /// - /// ### Examples - /// ```dart - /// final Result = Err("emergency failure"); - /// x.unwrapErr(); // "emergency failure" - /// ``` - /// - /// Should throw a Failure: - /// ```dart - /// final Result x = Ok(2); - /// // throw Failure with message: "Called unwrapErr() on Result with value: 2" - /// x.unwrapErr(); - /// ``` - E unwrapErr() { - return switch (this) { - Ok(:final V value) => throw Failure( - message: "Called unwrapErr() on Result with value: $value", - stackTrace: StackTrace.current, - ), - Err(:final E error) => error, - }; - } - /// - /// Returns the contained [Err] error, consuming the `this` value. - /// - /// ### Throws an error - /// - /// Throws an [Failure] if the value is an [Ok], with a failure message including the - /// passed message, and the content of the [Ok]. - /// - /// - /// ### Examples - /// - /// Basic usage: - /// ```dart - /// final Result x = Err("emergency failure"); - /// x.expectErr("A custom message"); // "emergency failure" - /// ``` - /// - /// Should throw a Failure: - /// ```dart - /// final Result x = Ok(2); - /// // throw Failure with message: "Testing expectErr: 2" - /// x.expectErr("Testing expectErr"); - /// ``` - E expectErr(String message) { - return switch (this) { - Ok(:final V value) => throw Failure( - message: "$message: $value", + Some(:final V value) => value, + None() => throw Failure( + message: message, stackTrace: StackTrace.current, ), - Err(:final E error) => error, }; } - /// - /// Returns the contained [Ok] value or a provided default value [d]. - /// - /// Arguments passed to [unwrapOr] are eagerly evaluated; if you are passing - /// the result of a function call, it is recommended to use [unwrapOrElse], + + // /// + // /// # Examples + // /// + // /// ```dart + // /// final Option = None(); + // /// x.unwrapErr(); // "emergency failure" + // /// ``` + // /// + // /// Should throw a Failure: + // /// + // /// ```dart + // /// final Result x = Ok(2); + // /// // throw Failure with message: "Called unwrapErr() on Result with value: 2" + // /// x.unwrapErr(); + // /// ``` + // E unwrapErr() { + // return switch (this) { + // Ok(:final V value) => throw Failure( + // message: "Called unwrapErr() on Result with value: $value", + // stackTrace: StackTrace.current, + // ), + // Err(:final E error) => error, + // }; + // } + // /// + // /// Returns the contained [Err] error, consuming the `this` value. + // /// + // /// ### Throws an error + // /// + // /// Throws an [Failure] if the value is an [Ok], with a failure message including the + // /// passed message, and the content of the [Ok]. + // /// + // /// + // /// ### Examples + // /// + // /// Basic usage: + // /// ```dart + // /// final Result x = Err("emergency failure"); + // /// x.expectErr("A custom message"); // "emergency failure" + // /// ``` + // /// + // /// Should throw a Failure: + // /// ```dart + // /// final Result x = Ok(2); + // /// // throw Failure with message: "Testing expectErr: 2" + // /// x.expectErr("Testing expectErr"); + // /// ``` + // E expectErr(String message) { + // return switch (this) { + // Ok(:final V value) => throw Failure( + // message: "$message: $value", + // stackTrace: StackTrace.current, + // ), + // Err(:final E error) => error, + // }; + // } + /// + /// Returns the contained [Some] value or a provided default. + /// + /// Arguments passed to `unwrap_or` are eagerly evaluated; if you are passing + /// the result of a function call, it is recommended to use [`unwrap_or_else`], /// which is lazily evaluated. /// /// ### Examples + /// /// ```dart /// final defaultValue = 2; - /// final Result x = Ok(9); + /// final Option x = Some(9); /// x.unwrapOr(defaultValue); // 9 - /// - /// final Result y = Err("error"); + /// // + /// final Option y = None(); /// y.unwrapOr(defaultValue); // 2 /// ``` V unwrapOr(V d) { return switch (this) { - Ok(:final V value) => value, - Err() => d, + Some(:final V value) => value, + None() => d, }; } /// - /// Returns the contained [Ok] value or computes it from a closure [d]. + /// Returns the contained [`Some`] value or computes it from a closure. /// /// ### Examples + /// /// ```dart - /// int count(String s) => s.length; + /// int onNone() => 'sNone'; /// - /// final Result x = Ok(2); - /// x.unwrapOrElse(count); // 2 + /// final Option x = Some('isSome'); + /// x.unwrapOrElse(onNone); // 'isSome' /// - /// final Result y = Err("foo"); - /// y.unwrapOrElse(count); // 3 + /// final Option y = None(); + /// y.unwrapOrElse(onNone); // 'isNone' /// ``` - V unwrapOrElse(V Function(E error) d) { + V unwrapOrElse(V Function() d) { return switch (this) { - Ok(:final V value) => value, - Err(:final E error) => d(error), + Some(:final V value) => value, + None() => d(), }; } } -/// -/// Extracting contained values for [Ok] -extension ExtractOk on Result { - /// - /// Returns the contained [Ok] value, but never throw an error. - /// - /// It can be used as a maintainability safeguard that will fail - /// to compile if the error type of the [Result] is later changed - /// from [Never] to a type that can actually occur. - /// - /// ### Examples - /// ```dart - /// Result onlyGoodNews() => Ok("this is fine"); - /// onlyGoodNews().intoOk(); // "this is fine" - /// ``` - V intoOk() { - return (this as Ok).value; - } -} -/// -/// Extracting contained values for [Err] -extension ExtractErr on Result { - /// - /// Returns the contained [Err] value, but never throw an error. - /// - /// It can be used as a maintainability safeguard that will fail - /// to compile if the ok type of the [Result] is later changed - /// from [Never] to a type that can actually occur. - /// - /// ### Examples - /// ```dart - /// Result onlyBadNews() => Err("Oops, it failed"); - /// onlyBadNews().intoErr(); // "Oops, it failed" - /// ``` - E intoErr() { - return (this as Err).error; - } -} +// /// +// /// Extracting contained values for [Ok] +// extension ExtractOk on Result { +// /// +// /// Returns the contained [Ok] value, but never throw an error. +// /// +// /// It can be used as a maintainability safeguard that will fail +// /// to compile if the error type of the [Result] is later changed +// /// from [Never] to a type that can actually occur. +// /// +// /// ### Examples +// /// ```dart +// /// Result onlyGoodNews() => Ok("this is fine"); +// /// onlyGoodNews().intoOk(); // "this is fine" +// /// ``` +// V intoOk() { +// return (this as Ok).value; +// } +// } +// /// +// /// Extracting contained values for [Err] +// extension ExtractErr on Result { +// /// +// /// Returns the contained [Err] value, but never throw an error. +// /// +// /// It can be used as a maintainability safeguard that will fail +// /// to compile if the ok type of the [Result] is later changed +// /// from [Never] to a type that can actually occur. +// /// +// /// ### Examples +// /// ```dart +// /// Result onlyBadNews() => Err("Oops, it failed"); +// /// onlyBadNews().intoErr(); // "Oops, it failed" +// /// ``` +// E intoErr() { +// return (this as Err).error; +// } +// } From ace72f48155d0b0860b5ca05e0f69a49f50addca Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 6 Nov 2024 21:00:20 +0300 Subject: [PATCH 03/44] Option | extension methods | extension_extract --- lib/hmi_core_option.dart | 3 ++- lib/hmi_core_result.dart | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/hmi_core_option.dart b/lib/hmi_core_option.dart index 8d589bd..a8b9523 100644 --- a/lib/hmi_core_option.dart +++ b/lib/hmi_core_option.dart @@ -1,3 +1,4 @@ library hmi_core_option; -export 'src/core/option/option.dart'; \ No newline at end of file +export 'src/core/option/option.dart'; +export 'src/core/option/extension_extract.dart'; \ No newline at end of file diff --git a/lib/hmi_core_result.dart b/lib/hmi_core_result.dart index 70780c4..8568a26 100644 --- a/lib/hmi_core_result.dart +++ b/lib/hmi_core_result.dart @@ -1,3 +1,8 @@ library hmi_core_result; -export 'src/core/result/result.dart'; \ No newline at end of file +export 'src/core/result/result.dart'; +export 'src/core/result/extension_adapter.dart'; +export 'src/core/result/extension_boolean_operations.dart'; +export 'src/core/result/extension_extract.dart'; +export 'src/core/result/extension_querying.dart'; +export 'src/core/result/extension_transform.dart'; \ No newline at end of file From 0dd741b57ba51061e0d6e264cd6d4be71f9e0975 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 6 Nov 2024 21:04:25 +0300 Subject: [PATCH 04/44] Option | extension methods | extension_extract --- lib/src/core/option/extension_extract.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/option/extension_extract.dart b/lib/src/core/option/extension_extract.dart index f7a4822..8bb7aab 100644 --- a/lib/src/core/option/extension_extract.dart +++ b/lib/src/core/option/extension_extract.dart @@ -2,7 +2,7 @@ import 'package:hmi_core/hmi_core_failure.dart'; import 'package:hmi_core/hmi_core_option.dart'; /// /// Extracting contained values -extension Extract on Option { +extension Extract on Option { /// /// Returns the contained [Some] value, consuming the `self` value. /// From 8a34c3eeecac71a60c33ea480fee885656df61f5 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Thu, 7 Nov 2024 13:57:23 +0300 Subject: [PATCH 05/44] Option | extension methods | extension_transform --- lib/hmi_core_option.dart | 3 +- lib/src/core/option/extension_transform.dart | 35 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 lib/src/core/option/extension_transform.dart diff --git a/lib/hmi_core_option.dart b/lib/hmi_core_option.dart index a8b9523..c087118 100644 --- a/lib/hmi_core_option.dart +++ b/lib/hmi_core_option.dart @@ -1,4 +1,5 @@ library hmi_core_option; export 'src/core/option/option.dart'; -export 'src/core/option/extension_extract.dart'; \ No newline at end of file +export 'src/core/option/extension_extract.dart'; +export 'src/core/option/extension_transform.dart'; \ No newline at end of file diff --git a/lib/src/core/option/extension_transform.dart b/lib/src/core/option/extension_transform.dart new file mode 100644 index 0000000..33245e7 --- /dev/null +++ b/lib/src/core/option/extension_transform.dart @@ -0,0 +1,35 @@ +import 'package:hmi_core/hmi_core_option.dart'; +import 'package:hmi_core/hmi_core_result.dart'; + +/// +/// Transforming contained values +extension Transform on Option { + /// + /// Transforms the `Option` into a [Result], + /// mapping [Some(v)] to [Ok(v)] and [None] to [Err(err)]. + /// + /// Arguments passed to `ok_or` are eagerly evaluated; if you are passing the + /// result of a function call, it is recommended to use [`ok_or_else`], which is + /// lazily evaluated. + /// + /// [`Ok(v)`]: Ok + /// [`Err(err)`]: Err + /// [`Some(v)`]: Some + /// [`ok_or_else`]: Option::ok_or_else + /// + /// # Examples + /// + /// ``` + /// let x = Some("foo"); + /// assert_eq!(x.ok_or(0), Ok("foo")); + /// + /// let x: Option<&str> = None; + /// assert_eq!(x.ok_or(0), Err(0)); + /// ``` + Result okOr(E err) { + return switch (this) { + Some(:final value) => Ok(value), + None() => Err(err), + }; + } +} From 2f4771fcdb5867a02b0330492756aef2d25ec4ab Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Sun, 10 Nov 2024 17:50:00 +0300 Subject: [PATCH 06/44] Package version changed to 2.1.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6a6ca92..b89c979 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hmi_core description: A Flutter core package of hmi_widgets package which is helps to visualize states, values, changes and other artefacts from technological processes. -version: 2.0.0 +version: 2.1.0 homepage: https://github.com/a-givertzman/hmi_core environment: From aa2d0b62a65524eb34ce817ff5e3c2f964b2598b Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Sun, 10 Nov 2024 18:43:55 +0300 Subject: [PATCH 07/44] Package version changed to 2.1.2 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b89c979..a8e5673 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hmi_core description: A Flutter core package of hmi_widgets package which is helps to visualize states, values, changes and other artefacts from technological processes. -version: 2.1.0 +version: 2.1.2 homepage: https://github.com/a-givertzman/hmi_core environment: From 6b0ca4d5cb577e6e1e4b4ee608910b842b1e6023 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Mon, 11 Nov 2024 12:57:22 +0300 Subject: [PATCH 08/44] Log | fixes --- lib/src/core/log/log.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index 71492c4..98c70c8 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -3,7 +3,7 @@ import 'package:hmi_core/src/core/log/log_level.dart'; import 'package:logging/logging.dart'; export 'package:logging/logging.dart' hide Logger, Level; - +/// /// Use a [Log] to log debug messages. /// - First call initialize with optional root loging Level, default LogLevel.all /// - [Log]s are named using a hierarchical dot-separated name convention. @@ -19,19 +19,19 @@ class Log { Logger.root.level = level ?? LogLevel.all; // defaults to Level.INFO Logger.root.onRecord.listen((record) { if (record.level == LogLevel.all) { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.debug) { - _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.config) { - _logColored(ConsoleColors.fgPurple, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgPurple, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.info) { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.warning) { - _logColored(ConsoleColors.fgYellow, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgYellow, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.error) { - _logColored(ConsoleColors.fgRed, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgRed, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}: ${record.message}'); + _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } }); } From 079691b69213f972dfbbbdbd48c31721a197f174 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Mon, 18 Nov 2024 20:01:15 +0300 Subject: [PATCH 09/44] Option | extension methods | extension_querying --- lib/src/core/option/extension_querying.dart | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 lib/src/core/option/extension_querying.dart diff --git a/lib/src/core/option/extension_querying.dart b/lib/src/core/option/extension_querying.dart new file mode 100644 index 0000000..91330fb --- /dev/null +++ b/lib/src/core/option/extension_querying.dart @@ -0,0 +1,39 @@ +import 'package:hmi_core/hmi_core_option.dart'; +/// +/// Querying the contained values +extension Querying on Option { + /// + /// Returns `true` if the option is [Some], and `false` otherwise. + /// + /// ### Examples + /// ```dart + /// final Option x = Some(-3); + /// x.isSome(); // true + /// + /// final Option y = None(); + /// y.isSome(); // false + /// ``` + bool isSome() { + return switch (this) { + Some() => true, + None() => false, + }; + } + /// + /// Returns `true` if the option is [None], and `false` otherwise. + /// + /// ### Examples + /// ```dart + /// final Option x = Some(-3); + /// x.isNone(); // false + /// + /// final Option y = None(); + /// y.isNone(); // true + /// ``` + bool isNone() { + return switch (this) { + Some() => false, + None() => true, + }; + } +} From 4c0c8e28c95b50092a4c10e2d20a8e681b9b3784 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Mon, 18 Nov 2024 20:10:15 +0300 Subject: [PATCH 10/44] Option | extension methods | extension_querying --- lib/hmi_core_option.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/hmi_core_option.dart b/lib/hmi_core_option.dart index c087118..b257111 100644 --- a/lib/hmi_core_option.dart +++ b/lib/hmi_core_option.dart @@ -2,4 +2,5 @@ library hmi_core_option; export 'src/core/option/option.dart'; export 'src/core/option/extension_extract.dart'; -export 'src/core/option/extension_transform.dart'; \ No newline at end of file +export 'src/core/option/extension_transform.dart'; +export 'src/core/option/extension_querying.dart'; \ No newline at end of file From 223b6c1b1e1c2d0f6759b0f16e95d8cb6d157fc9 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Fri, 29 Nov 2024 17:56:43 +0300 Subject: [PATCH 11/44] Option | rename extensions to avoid conflicts --- lib/hmi_core_option.dart | 6 +++--- ...extension_extract.dart => option_extract_extension.dart} | 2 +- ...tension_querying.dart => option_querying_extension.dart} | 2 +- ...nsion_transform.dart => option_transform_extension.dart} | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) rename lib/src/core/option/{extension_extract.dart => option_extract_extension.dart} (99%) rename lib/src/core/option/{extension_querying.dart => option_querying_extension.dart} (94%) rename lib/src/core/option/{extension_transform.dart => option_transform_extension.dart} (95%) diff --git a/lib/hmi_core_option.dart b/lib/hmi_core_option.dart index b257111..7e6fb7c 100644 --- a/lib/hmi_core_option.dart +++ b/lib/hmi_core_option.dart @@ -1,6 +1,6 @@ library hmi_core_option; export 'src/core/option/option.dart'; -export 'src/core/option/extension_extract.dart'; -export 'src/core/option/extension_transform.dart'; -export 'src/core/option/extension_querying.dart'; \ No newline at end of file +export 'src/core/option/option_extract_extension.dart'; +export 'src/core/option/option_transform_extension.dart'; +export 'src/core/option/option_querying_extension.dart'; diff --git a/lib/src/core/option/extension_extract.dart b/lib/src/core/option/option_extract_extension.dart similarity index 99% rename from lib/src/core/option/extension_extract.dart rename to lib/src/core/option/option_extract_extension.dart index 8bb7aab..f10a848 100644 --- a/lib/src/core/option/extension_extract.dart +++ b/lib/src/core/option/option_extract_extension.dart @@ -2,7 +2,7 @@ import 'package:hmi_core/hmi_core_failure.dart'; import 'package:hmi_core/hmi_core_option.dart'; /// /// Extracting contained values -extension Extract on Option { +extension OptionExtract on Option { /// /// Returns the contained [Some] value, consuming the `self` value. /// diff --git a/lib/src/core/option/extension_querying.dart b/lib/src/core/option/option_querying_extension.dart similarity index 94% rename from lib/src/core/option/extension_querying.dart rename to lib/src/core/option/option_querying_extension.dart index 91330fb..2606091 100644 --- a/lib/src/core/option/extension_querying.dart +++ b/lib/src/core/option/option_querying_extension.dart @@ -1,7 +1,7 @@ import 'package:hmi_core/hmi_core_option.dart'; /// /// Querying the contained values -extension Querying on Option { +extension OptionQuerying on Option { /// /// Returns `true` if the option is [Some], and `false` otherwise. /// diff --git a/lib/src/core/option/extension_transform.dart b/lib/src/core/option/option_transform_extension.dart similarity index 95% rename from lib/src/core/option/extension_transform.dart rename to lib/src/core/option/option_transform_extension.dart index 33245e7..e210401 100644 --- a/lib/src/core/option/extension_transform.dart +++ b/lib/src/core/option/option_transform_extension.dart @@ -1,9 +1,8 @@ import 'package:hmi_core/hmi_core_option.dart'; import 'package:hmi_core/hmi_core_result.dart'; - /// /// Transforming contained values -extension Transform on Option { +extension OptionTransform on Option { /// /// Transforms the `Option` into a [Result], /// mapping [Some(v)] to [Ok(v)] and [None] to [Err(err)]. From e856df87259aae2a9e522d6c229bc2c611787f90 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Tue, 3 Dec 2024 14:40:02 +0300 Subject: [PATCH 12/44] Option | extensions | renamed --- lib/src/core/option/extension_extract.dart | 2 +- lib/src/core/option/extension_querying.dart | 2 +- lib/src/core/option/extension_transform.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/core/option/extension_extract.dart b/lib/src/core/option/extension_extract.dart index 8bb7aab..f10a848 100644 --- a/lib/src/core/option/extension_extract.dart +++ b/lib/src/core/option/extension_extract.dart @@ -2,7 +2,7 @@ import 'package:hmi_core/hmi_core_failure.dart'; import 'package:hmi_core/hmi_core_option.dart'; /// /// Extracting contained values -extension Extract on Option { +extension OptionExtract on Option { /// /// Returns the contained [Some] value, consuming the `self` value. /// diff --git a/lib/src/core/option/extension_querying.dart b/lib/src/core/option/extension_querying.dart index 91330fb..2606091 100644 --- a/lib/src/core/option/extension_querying.dart +++ b/lib/src/core/option/extension_querying.dart @@ -1,7 +1,7 @@ import 'package:hmi_core/hmi_core_option.dart'; /// /// Querying the contained values -extension Querying on Option { +extension OptionQuerying on Option { /// /// Returns `true` if the option is [Some], and `false` otherwise. /// diff --git a/lib/src/core/option/extension_transform.dart b/lib/src/core/option/extension_transform.dart index 33245e7..55cad80 100644 --- a/lib/src/core/option/extension_transform.dart +++ b/lib/src/core/option/extension_transform.dart @@ -3,7 +3,7 @@ import 'package:hmi_core/hmi_core_result.dart'; /// /// Transforming contained values -extension Transform on Option { +extension OptionTransform on Option { /// /// Transforms the `Option` into a [Result], /// mapping [Some(v)] to [Ok(v)] and [None] to [Err(err)]. From 760c2653c8712febbed3d1fc4607b7642c7cba92 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 12:46:51 +0300 Subject: [PATCH 13/44] Log | LogLevel | trace --- lib/src/core/log/log.dart | 9 +++++++++ lib/src/core/log/log_level.dart | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index 98c70c8..a53eaed 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -20,6 +20,8 @@ class Log { Logger.root.onRecord.listen((record) { if (record.level == LogLevel.all) { _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + } else if (record.level == LogLevel.trace) { + _logColored(ConsoleColors.fgCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.debug) { _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.config) { @@ -40,6 +42,13 @@ class Log { log(true, '$color$message${ConsoleColors.reset}'); } /// + /// Log message at level [LogLevel.trace]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void trace(Object? message, [Object? error, StackTrace? stackTrace]) => + Logger(_name).log(LogLevel.trace, message, error, stackTrace); + /// /// Log message at level [LogLevel.debug]. /// /// See [log] for information on how non-String [message] arguments are diff --git a/lib/src/core/log/log_level.dart b/lib/src/core/log/log_level.dart index c30d338..e18e7d6 100644 --- a/lib/src/core/log/log_level.dart +++ b/lib/src/core/log/log_level.dart @@ -6,7 +6,7 @@ import 'package:logging/logging.dart'; /// follows (in descending order): /// - [LogLevel.off], /// - [LogLevel.error], [LogLevel.warning], -/// - [LogLevel.info], [LogLevel.config], [LogLevel.debug], +/// - [LogLevel.info], [LogLevel.config], [LogLevel.debug], [LogLevel.trace], /// - [LogLevel.all]. /// /// We recommend using one of the predefined logging levels. If you define your @@ -17,6 +17,9 @@ class LogLevel extends Level { /// Special key to turn on logging for all levels ([value] = 0). static const LogLevel all = LogLevel('ALL', 0); /// + /// Key for detailed tracing information ([value] = 400). + static const LogLevel trace = LogLevel('TRACE', 400);// Level.FINER; + /// /// Key for tracing information ([value] = 500). static const LogLevel debug = LogLevel('DEBUG', 500);// Level.FINE; /// @@ -61,6 +64,7 @@ class LogLevel extends Level { /// static const List levels = [ all, + trace, debug, config, info, From 0e4c9c9137a0432d687198b82b29f58b573c4102 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 13:07:07 +0300 Subject: [PATCH 14/44] Log | ConsoleColors fix --- lib/src/core/log/console_colors.dart | 60 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/src/core/log/console_colors.dart b/lib/src/core/log/console_colors.dart index f8d3139..3db4c62 100644 --- a/lib/src/core/log/console_colors.dart +++ b/lib/src/core/log/console_colors.dart @@ -1,44 +1,44 @@ /// /// Colors used to customize console output class ConsoleColors { - static const reset = '\x1b[0m'; // reset - static const bright = '\x1b[1m'; + static const reset = '\x1b[0m'; // reset + static const bright = '\x1b[1m'; // - static const dim = '\x1b[2m'; + static const dim = '\x1b[2m'; static const underscore = '\x1b[4m'; - static const blink = '\x1b[5m'; - static const reverse = '\x1b[7m'; - static const hidden = '\x1b[8m'; + static const blink = '\x1b[5m'; + static const reverse = '\x1b[7m'; + static const hidden = '\x1b[8m'; // // Regular foreground - static const fgBlack = '\x1b[0;30m'; // Black - static const fgRed = '\x1b[0;31m'; // Red - static const fgGreen = '\x1b[0;32m'; // Green - static const fgYellow = '\x1b[0;33m'; // Yellow - static const fgBlue = '\x1b[0;34m'; // Blue - static const fgPurple = '\x1b[0;35m'; // Purple - static const fgCyan = '\x1b[0;36m'; // Cyan - static const fgWhite = '\x1b[0;37m'; // White - static const fgGray = '\x1b[90m'; + static const fgBlack = '\x1b[30m'; // Black + static const fgRed = '\x1b[31m'; // Red + static const fgGreen = '\x1b[32m'; // Green + static const fgYellow = '\x1b[33m'; // Yellow + static const fgBlue = '\x1b[34m'; // Blue + static const fgPurple = '\x1b[35m'; // Purple + static const fgCyan = '\x1b[36m'; // Cyan + static const fgWhite = '\x1b[37m'; // White + static const fgGray = '\x1b[90m'; // // Bold - static const fgBoldBlack = '\x1b[1;30m'; // Black - static const fgBoldRed = '\x1b[1;31m'; // Red - static const fgBoldGreen = '\x1b[1;32m'; // Green + static const fgBoldBlack = '\x1b[1;30m'; // Black + static const fgBoldRed = '\x1b[1;31m'; // Red + static const fgBoldGreen = '\x1b[1;32m'; // Green static const fgBoldYellow = '\x1b[1;33m'; // Yellow - static const fgBoldBlue = '\x1b[1;34m'; // Blue + static const fgBoldBlue = '\x1b[1;34m'; // Blue static const fgBoldPurple = '\x1b[1;35m'; // Purple - static const fgBoldCyan = '\x1b[1;36m'; // Cyan - static const fgBoldWhite = '\x1b[1;37m'; // White + static const fgBoldCyan = '\x1b[1;36m'; // Cyan + static const fgBoldWhite = '\x1b[1;37m'; // White // // Regular background - static const bgBlack = '\x1b[40m'; - static const bgRed = '\x1b[41m'; - static const bgGreen = '\x1b[42m'; - static const bgYellow = '\x1b[43m'; - static const bgBlue = '\x1b[44m'; - static const bgMagenta = '\x1b[45m'; - static const bgCyan = '\x1b[46m'; - static const bgWhite = '\x1b[47m'; - static const bgGray = '\x1b[100m'; + static const bgBlack = '\x1b[40m'; + static const bgRed = '\x1b[41m'; + static const bgGreen = '\x1b[42m'; + static const bgYellow = '\x1b[43m'; + static const bgBlue = '\x1b[44m'; + static const bgMagenta = '\x1b[45m'; + static const bgCyan = '\x1b[46m'; + static const bgWhite = '\x1b[47m'; + static const bgGray = '\x1b[100m'; } From 5851c3c12b1a0d1f5397106edb43bd194df45442 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 13:10:32 +0300 Subject: [PATCH 15/44] Log | ConsoleColors fix --- lib/src/core/log/log.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index a53eaed..45975ad 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -21,7 +21,7 @@ class Log { if (record.level == LogLevel.all) { _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.trace) { - _logColored(ConsoleColors.fgCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + _logColored(ConsoleColors.fgBoldCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.debug) { _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); } else if (record.level == LogLevel.config) { From d60ba95a2f3e67950e257a48b03ab71e2cb5c4ee Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 13:24:49 +0300 Subject: [PATCH 16/44] Log | LogLevel | fixes --- lib/src/core/log/console_colors.dart | 16 +++++----- lib/src/core/log/log.dart | 45 +++++++++++++++++----------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/src/core/log/console_colors.dart b/lib/src/core/log/console_colors.dart index 3db4c62..da46336 100644 --- a/lib/src/core/log/console_colors.dart +++ b/lib/src/core/log/console_colors.dart @@ -22,14 +22,14 @@ class ConsoleColors { static const fgGray = '\x1b[90m'; // // Bold - static const fgBoldBlack = '\x1b[1;30m'; // Black - static const fgBoldRed = '\x1b[1;31m'; // Red - static const fgBoldGreen = '\x1b[1;32m'; // Green - static const fgBoldYellow = '\x1b[1;33m'; // Yellow - static const fgBoldBlue = '\x1b[1;34m'; // Blue - static const fgBoldPurple = '\x1b[1;35m'; // Purple - static const fgBoldCyan = '\x1b[1;36m'; // Cyan - static const fgBoldWhite = '\x1b[1;37m'; // White + static const fgBoldBlack = '\x1b[1;30m'; // Bold Black + static const fgBoldRed = '\x1b[1;31m'; // Bold Red + static const fgBoldGreen = '\x1b[1;32m'; // Bold Green + static const fgBoldYellow = '\x1b[1;33m'; // Bold Yellow + static const fgBoldBlue = '\x1b[1;34m'; // Bold Blue + static const fgBoldPurple = '\x1b[1;35m'; // Bold Purple + static const fgBoldCyan = '\x1b[1;36m'; // Bold Cyan + static const fgBoldWhite = '\x1b[1;37m'; // Bold White // // Regular background static const bgBlack = '\x1b[40m'; diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index 45975ad..7bda68b 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -18,23 +18,34 @@ class Log { hierarchicalLoggingEnabled = true; Logger.root.level = level ?? LogLevel.all; // defaults to Level.INFO Logger.root.onRecord.listen((record) { - if (record.level == LogLevel.all) { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.trace) { - _logColored(ConsoleColors.fgBoldCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.debug) { - _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.config) { - _logColored(ConsoleColors.fgPurple, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.info) { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.warning) { - _logColored(ConsoleColors.fgYellow, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else if (record.level == LogLevel.error) { - _logColored(ConsoleColors.fgRed, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } else { - _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - } + final color = switch (record.level) { + LogLevel.all => ConsoleColors.fgGray, + LogLevel.trace => ConsoleColors.fgBoldCyan, + LogLevel.debug => ConsoleColors.fgBlue, + LogLevel.config => ConsoleColors.fgPurple, + LogLevel.info => ConsoleColors.fgGray, + LogLevel.warning => ConsoleColors.fgYellow, + LogLevel.error => ConsoleColors.fgRed, + _ => ConsoleColors.fgGray, + }; + _logColored(color, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // if (record.level == LogLevel.all) { + // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.trace) { + // _logColored(ConsoleColors.fgBoldCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.debug) { + // _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.config) { + // _logColored(ConsoleColors.fgPurple, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.info) { + // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.warning) { + // _logColored(ConsoleColors.fgYellow, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else if (record.level == LogLevel.error) { + // _logColored(ConsoleColors.fgRed, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } else { + // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); + // } }); } /// From 95e4f9b72b7043088e34d4e818ceaec4d72665b2 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 13:28:25 +0300 Subject: [PATCH 17/44] Log | LogLevel | fixes --- lib/src/core/log/log.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index 7bda68b..4f04219 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -81,6 +81,13 @@ class Log { void warning(Object? message, [Object? error, StackTrace? stackTrace]) => Logger(_name).log(LogLevel.warning, message, error, stackTrace); /// + /// Log message at level [LogLevel.warning]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void warn(Object? message, [Object? error, StackTrace? stackTrace]) => + Logger(_name).log(LogLevel.warning, message, error, stackTrace); + /// /// Log message at level [LogLevel.error]. /// /// See [log] for information on how non-String [message] arguments are handled. From 30fcb8d57adba6843b8cf9c3a51f32f9014747d3 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 11 Dec 2024 13:43:51 +0300 Subject: [PATCH 18/44] Log | LogLevel | fixes --- lib/src/core/log/log.dart | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/src/core/log/log.dart b/lib/src/core/log/log.dart index 4f04219..a317413 100644 --- a/lib/src/core/log/log.dart +++ b/lib/src/core/log/log.dart @@ -20,7 +20,7 @@ class Log { Logger.root.onRecord.listen((record) { final color = switch (record.level) { LogLevel.all => ConsoleColors.fgGray, - LogLevel.trace => ConsoleColors.fgBoldCyan, + LogLevel.trace => ConsoleColors.fgCyan, LogLevel.debug => ConsoleColors.fgBlue, LogLevel.config => ConsoleColors.fgPurple, LogLevel.info => ConsoleColors.fgGray, @@ -29,23 +29,6 @@ class Log { _ => ConsoleColors.fgGray, }; _logColored(color, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // if (record.level == LogLevel.all) { - // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.trace) { - // _logColored(ConsoleColors.fgBoldCyan, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.debug) { - // _logColored(ConsoleColors.fgBlue, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.config) { - // _logColored(ConsoleColors.fgPurple, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.info) { - // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.warning) { - // _logColored(ConsoleColors.fgYellow, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else if (record.level == LogLevel.error) { - // _logColored(ConsoleColors.fgRed, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } else { - // _logColored(ConsoleColors.fgGray, '${record.time} | ${record.level.name} | ${record.loggerName}${record.message}'); - // } }); } /// From b31361c67d4592850212c3be2ebea227c1c7a62c Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Thu, 12 Dec 2024 20:25:45 +0300 Subject: [PATCH 19/44] Localizations | add getters --- lib/src/translate/localizations.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/translate/localizations.dart b/lib/src/translate/localizations.dart index db858eb..34d51e8 100644 --- a/lib/src/translate/localizations.dart +++ b/lib/src/translate/localizations.dart @@ -280,4 +280,12 @@ class Localizations { } return translations[_appLang.index]; } + /// + /// Returns current App lang + AppLang get appLang => _appLang; + /// + /// Returns list of available languages + List get languages { + return AppLang.values; + } } From a28446a4ff6bff6b78e3597f562ffce17d118bce Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Fri, 13 Dec 2024 13:36:35 +0300 Subject: [PATCH 20/44] AppSetting | onError --- lib/src/app_settings/setting.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index bf59da1..15221cc 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -41,8 +41,8 @@ class Setting { @override String toString() => '${AppSettings.getSetting(_name)}'; } - - +/// +/// class _SettingValue implements Setting { final dynamic _value; // From 9ad3f9376d7aac7316773829529aa82ff3d65ec1 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Fri, 13 Dec 2024 14:30:09 +0300 Subject: [PATCH 21/44] AppSetting | onError --- lib/src/app_settings/app_settings.dart | 15 ++++++++++----- lib/src/app_settings/setting.dart | 17 ++++++++++++----- test/unit/core/log/log_level_test.dart | 1 + 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 6e480d0..ffd4c09 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -24,18 +24,23 @@ class AppSettings { await jsonMap.decoded .then((result) => switch(result) { Ok(value: final map) => _map.addAll(map), - Err() => _log.warning('Failed to initialize app settings from file.'), + Err(: final error) => _log.warning('$AppSettings.initialize | Failed to initialize app settings from file. Error: $error'), }); } } /// - static dynamic getSetting(String settingName) { - final setting = _map[settingName]; + /// Returns stored value by it's key + static dynamic getSetting(String key, dynamic Function(Failure err)? onError) { + final setting = _map[key]; if (setting == null) { - throw Failure( - message: 'Ошибка в методе $AppSettings.getSetting(): Не найдена настройка "$settingName"', + final err = Failure( + message: '$AppSettings.getSetting | Not found key "$key"', stackTrace: StackTrace.current, ); + if (onError != null) { + return onError(err); + } + throw err; } return setting; } diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index 15221cc..93c461c 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -1,4 +1,5 @@ import 'app_settings.dart'; +import 'package:hmi_core/src/core/error/failure.dart'; /// /// Holds setting value stored in AppSettings by it name @@ -6,22 +7,25 @@ import 'app_settings.dart'; class Setting { final String _name; final double _factor; + final dynamic Function(Failure err)? _onError; /// /// - [name] - the name of value stored in AppSettings /// - [factor] - returned value int or double will by multiplied by factor const Setting( String name, { double factor = 1.0, + dynamic Function(Failure err)? onError, }) : _name = name, - _factor = factor; + _factor = factor, + _onError = onError; /// - /// + /// Returns [Setting] new instance containing a [value] const factory Setting.from(dynamic value) = _SettingValue; /// /// Returns setting value in int represantation int get toInt { - final value = AppSettings.getSetting(_name); + final value = AppSettings.getSetting(_name, _onError); if (value is int) { return _factor == 1 ? value : (value * _factor).toInt(); } @@ -30,7 +34,7 @@ class Setting { /// /// Returns setting value in double represantation double get toDouble { - final value = AppSettings.getSetting(_name); + final value = AppSettings.getSetting(_name, _onError); if (value is double) { return value * _factor; } @@ -39,7 +43,7 @@ class Setting { /// /// Returns setting value in string represantation @override - String toString() => '${AppSettings.getSetting(_name)}'; + String toString() => '${AppSettings.getSetting(_name, _onError)}'; } /// /// @@ -55,6 +59,9 @@ class _SettingValue implements Setting { String get _name => throw UnimplementedError(); // @override + dynamic Function(Failure err)? get _onError => throw UnimplementedError(); + // + @override double get toDouble { final value = _value; if (value is double) { diff --git a/test/unit/core/log/log_level_test.dart b/test/unit/core/log/log_level_test.dart index f3856ed..4c67a65 100644 --- a/test/unit/core/log/log_level_test.dart +++ b/test/unit/core/log/log_level_test.dart @@ -5,6 +5,7 @@ void main() { group('LogLevel test', () { const target = { 'all': ['ALL', 0], + 'trace': ['TRACE', 400], 'debug': ['DEBUG', 500], 'config': ['CONFIG', 700], 'info': ['INFO', 800], From fb9eefa8dd78df04d05672e2c104da863a103eb3 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Fri, 13 Dec 2024 14:44:18 +0300 Subject: [PATCH 22/44] AppSetting | onError --- lib/src/app_settings/app_settings.dart | 2 +- lib/src/app_settings/setting.dart | 6 ++-- .../app_settings_initialize_test.dart | 7 +++-- .../entities/app_settings/setting_test.dart | 29 +++++++++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index ffd4c09..7a0fae6 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -30,7 +30,7 @@ class AppSettings { } /// /// Returns stored value by it's key - static dynamic getSetting(String key, dynamic Function(Failure err)? onError) { + static dynamic getSetting(String key, {dynamic Function(Failure err)? onError}) { final setting = _map[key]; if (setting == null) { final err = Failure( diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index 93c461c..226d369 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -25,7 +25,7 @@ class Setting { /// /// Returns setting value in int represantation int get toInt { - final value = AppSettings.getSetting(_name, _onError); + final value = AppSettings.getSetting(_name, onError: _onError); if (value is int) { return _factor == 1 ? value : (value * _factor).toInt(); } @@ -34,7 +34,7 @@ class Setting { /// /// Returns setting value in double represantation double get toDouble { - final value = AppSettings.getSetting(_name, _onError); + final value = AppSettings.getSetting(_name, onError: _onError); if (value is double) { return value * _factor; } @@ -43,7 +43,7 @@ class Setting { /// /// Returns setting value in string represantation @override - String toString() => '${AppSettings.getSetting(_name, _onError)}'; + String toString() => '${AppSettings.getSetting(_name, onError: _onError)}'; } /// /// diff --git a/test/unit/core/entities/app_settings/app_settings_initialize_test.dart b/test/unit/core/entities/app_settings/app_settings_initialize_test.dart index be8d566..4a0301c 100644 --- a/test/unit/core/entities/app_settings/app_settings_initialize_test.dart +++ b/test/unit/core/entities/app_settings/app_settings_initialize_test.dart @@ -15,9 +15,10 @@ void main() { FakeTextFile(textFile), ), ); - expect(AppSettings.getSetting('test_setting_1'), setSettings['test_setting_1']); - expect(AppSettings.getSetting('test_setting_2'), setSettings['test_setting_2']); - expect(AppSettings.getSetting('test_setting_3'), setSettings['test_setting_3']); + expect(AppSettings.getSetting('test_setting_1', onError: ((err) => null)), setSettings['test_setting_1']); + expect(AppSettings.getSetting('test_setting_2', onError: ((err) => null)), setSettings['test_setting_2']); + expect(AppSettings.getSetting('test_setting_3', onError: ((err) => null)), setSettings['test_setting_3']); + expect(AppSettings.getSetting('1234', onError: ((err) => 1234)), 1234); } }); // test('throws with valid json format and invalid data', () async { diff --git a/test/unit/core/entities/app_settings/setting_test.dart b/test/unit/core/entities/app_settings/setting_test.dart index de3e250..b838d0a 100644 --- a/test/unit/core/entities/app_settings/setting_test.dart +++ b/test/unit/core/entities/app_settings/setting_test.dart @@ -41,6 +41,35 @@ void main() { expect(const Setting('test_setting_3').toString(), setSettings['test_setting_3']!.toString()); } }); + test('key not found', () async { + for (final settings in validSettings) { + final textFile = settings['text_file'] as String; + final setSettings = settings['set_settings'] as Map; + await AppSettings.initialize( + jsonMap: JsonMap.fromTextFile( + FakeTextFile(textFile), + ), + ); + for (final setting in setSettings.entries) { + final int testSetting = Setting(setting.key).toInt; + log.debug('as Int | ${setting.key}: $testSetting'); + } + for (final setting in setSettings.entries) { + final double testSetting = Setting(setting.key).toDouble; + log.debug('as Double | ${setting.key}: $testSetting'); + } + for (final setting in setSettings.entries) { + final String testSetting = Setting(setting.key).toString(); + log.debug('as String | ${setting.key}: $testSetting'); + } + expect(Setting('test_setting_1111', onError: (err) => 111,).toInt, 111); + expect(Setting('test_setting_1222', onError: (err) => -222,).toInt, -222); + expect(Setting('test_setting_1.10', onError: (err) => 1.10,).toDouble, 1.10); + expect(Setting('test_setting_2.22', onError: (err) => 2.22,).toDouble, 2.22); + expect(Setting('test_setting_1234', onError: (err) => '1234').toString(), '1234'); + expect(Setting('test_setting_2345', onError: (err) => '2345').toString(), '2345'); + } + }); test('valid data with factor', () async { const factors = [0.123, -0.123, 3.54, -5.12]; for (final factor in factors) { From 270b85ec6cb9f352c0ef453251b83427d37b0ee1 Mon Sep 17 00:00:00 2001 From: Minyewoo Date: Mon, 3 Feb 2025 18:48:18 +0500 Subject: [PATCH 23/44] colors | Moved colors to hmi_widgets lib --- lib/hmi_core.dart | 2 - lib/hmi_core_alarm_colors.dart | 3 -- lib/hmi_core_entities.dart | 2 - lib/hmi_core_state_colors.dart | 3 -- lib/src/core/entities/alarm_colors.dart | 18 -------- lib/src/core/entities/state_colors.dart | 28 ----------- .../alarm_colors/alarm_colors_test.dart | 31 ------------- .../state_colors/state_colors_test.dart | 46 ------------------- 8 files changed, 133 deletions(-) delete mode 100644 lib/hmi_core_alarm_colors.dart delete mode 100644 lib/hmi_core_state_colors.dart delete mode 100644 lib/src/core/entities/alarm_colors.dart delete mode 100644 lib/src/core/entities/state_colors.dart delete mode 100644 test/unit/core/entities/alarm_colors/alarm_colors_test.dart delete mode 100644 test/unit/core/entities/state_colors/state_colors_test.dart diff --git a/lib/hmi_core.dart b/lib/hmi_core.dart index 8c45aef..2879cbe 100644 --- a/lib/hmi_core.dart +++ b/lib/hmi_core.dart @@ -8,8 +8,6 @@ export 'src/core/entities/ds_data_type.dart'; export 'src/core/entities/ds_status.dart'; export 'src/core/entities/ds_timestamp.dart'; export 'src/core/entities/state_constatnts.dart'; -export 'src/core/entities/alarm_colors.dart'; -export 'src/core/entities/state_colors.dart'; export 'src/core/entities/stacked.dart'; // RelativeValue export 'src/core/relative_value.dart'; diff --git a/lib/hmi_core_alarm_colors.dart b/lib/hmi_core_alarm_colors.dart deleted file mode 100644 index 298ca09..0000000 --- a/lib/hmi_core_alarm_colors.dart +++ /dev/null @@ -1,3 +0,0 @@ -library hmi_core_alarm_colors; - -export 'src/core/entities/alarm_colors.dart'; \ No newline at end of file diff --git a/lib/hmi_core_entities.dart b/lib/hmi_core_entities.dart index f0b348e..ed127e3 100644 --- a/lib/hmi_core_entities.dart +++ b/lib/hmi_core_entities.dart @@ -8,6 +8,4 @@ export 'src/core/entities/ds_data_type.dart'; export 'src/core/entities/ds_status.dart'; export 'src/core/entities/ds_timestamp.dart'; export 'src/core/entities/state_constatnts.dart'; -export 'src/core/entities/alarm_colors.dart'; -export 'src/core/entities/state_colors.dart'; export 'src/core/entities/stacked.dart'; diff --git a/lib/hmi_core_state_colors.dart b/lib/hmi_core_state_colors.dart deleted file mode 100644 index 33b3fca..0000000 --- a/lib/hmi_core_state_colors.dart +++ /dev/null @@ -1,3 +0,0 @@ -library hmi_core_state_colors; - -export 'src/core/entities/state_colors.dart'; \ No newline at end of file diff --git a/lib/src/core/entities/alarm_colors.dart b/lib/src/core/entities/alarm_colors.dart deleted file mode 100644 index 700fac5..0000000 --- a/lib/src/core/entities/alarm_colors.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -/// -class AlarmColors { - final Color class1; - final Color class2; - final Color class3; - final Color class4; - final Color class5; - final Color class6; - const AlarmColors({ - required this.class1, - required this.class2, - required this.class3, - required this.class4, - required this.class5, - required this.class6, - }); -} \ No newline at end of file diff --git a/lib/src/core/entities/state_colors.dart b/lib/src/core/entities/state_colors.dart deleted file mode 100644 index 37bfda7..0000000 --- a/lib/src/core/entities/state_colors.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -/// -class StateColors { - final Color error; - final Color alarm; - final Color obsolete; - final Color invalid; - final Color timeInvalid; - final Color lowLevel; - final Color alarmLowLevel; - final Color highLevel; - final Color alarmHighLevel; - final Color off; - final Color on; - const StateColors({ - required this.error, - required this.alarm, - required this.obsolete, - required this.invalid, - required this.timeInvalid, - required this.lowLevel, - required this.alarmLowLevel, - required this.highLevel, - required this.alarmHighLevel, - required this.off, - required this.on, - }); -} \ No newline at end of file diff --git a/test/unit/core/entities/alarm_colors/alarm_colors_test.dart b/test/unit/core/entities/alarm_colors/alarm_colors_test.dart deleted file mode 100644 index 8c82ad0..0000000 --- a/test/unit/core/entities/alarm_colors/alarm_colors_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hmi_core/hmi_core.dart'; - -void main() { - test('AlarmColors test', () { - final colors = { - 1: const Color(0xAA000001), - 2: const Color(0xAA000002), - 3: const Color(0xAA000003), - 4: const Color(0xAA000004), - 5: const Color(0xAA000005), - 6: const Color(0xAA000006), - }; - final alarmColors = AlarmColors( - class1: colors[1]!, - class2: colors[2]!, - class3: colors[3]!, - class4: colors[4]!, - class5: colors[5]!, - class6: colors[6]!, - ); - expect(alarmColors, isA()); - expect(alarmColors.class1, colors[1]!); - expect(alarmColors.class2, colors[2]!); - expect(alarmColors.class3, colors[3]!); - expect(alarmColors.class4, colors[4]!); - expect(alarmColors.class5, colors[5]!); - expect(alarmColors.class6, colors[6]!); - }); -} \ No newline at end of file diff --git a/test/unit/core/entities/state_colors/state_colors_test.dart b/test/unit/core/entities/state_colors/state_colors_test.dart deleted file mode 100644 index bd32fba..0000000 --- a/test/unit/core/entities/state_colors/state_colors_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hmi_core/hmi_core.dart'; - -void main() { - test('StateColors test', () { - final colors = { - 'alarm': const Color(0xAA000000), - 'error': const Color(0xAA000001), - 'obsolete': const Color(0xAA000002), - 'invalid': const Color(0xAA000003), - 'timeInvalid': const Color(0xAA000004), - 'lowLevel': const Color(0xAA000005), - 'alarmLowLevel': const Color(0xAA000006), - 'highLevel': const Color(0xAA000007), - 'alarmHighLevel': const Color(0xAA000008), - 'off': const Color(0xAA000009), - 'on': const Color(0xAA000010), - }; - final stateColors = StateColors( - error: colors['error']!, - alarm: colors['alarm']!, - obsolete: colors['obsolete']!, - invalid: colors['invalid']!, - timeInvalid: colors['timeInvalid']!, - lowLevel: colors['lowLevel']!, - alarmLowLevel: colors['alarmLowLevel']!, - highLevel: colors['highLevel']!, - alarmHighLevel: colors['alarmHighLevel']!, - off: colors['off']!, - on: colors['on']!, - ); - expect(stateColors, isA()); - expect(stateColors.error, colors['error']!); - expect(stateColors.alarm, colors['alarm']!); - expect(stateColors.obsolete, colors['obsolete']!); - expect(stateColors.invalid, colors['invalid']!); - expect(stateColors.timeInvalid, colors['timeInvalid']!); - expect(stateColors.lowLevel, colors['lowLevel']!); - expect(stateColors.alarmLowLevel, colors['alarmLowLevel']!); - expect(stateColors.highLevel, colors['highLevel']!); - expect(stateColors.alarmHighLevel, colors['alarmHighLevel']!); - expect(stateColors.off, colors['off']!); - expect(stateColors.on, colors['on']!); - }); -} \ No newline at end of file From b0c2bc95319ccf6ccba9c960dd1c4aad4f890895 Mon Sep 17 00:00:00 2001 From: Minyewoo Date: Mon, 3 Feb 2025 18:49:08 +0500 Subject: [PATCH 24/44] pubspec | increased version to 2.1.5 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 99e9b7e..685aca9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hmi_core description: A Flutter core package of hmi_widgets package which is helps to visualize states, values, changes and other artefacts from technological processes. -version: 2.1.1 +version: 2.1.5 homepage: https://github.com/a-givertzman/hmi_core environment: From 87051c28b1eb5d82275c23e46380e758d8c6bd18 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Thu, 17 Apr 2025 19:12:13 +0300 Subject: [PATCH 25/44] JsonMap | add .fromTextFiles constructor --- lib/src/core/json/json_map.dart | 91 +++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/lib/src/core/json/json_map.dart b/lib/src/core/json/json_map.dart index a01e1c1..d16e316 100644 --- a/lib/src/core/json/json_map.dart +++ b/lib/src/core/json/json_map.dart @@ -1,38 +1,73 @@ import 'dart:async'; import 'dart:convert'; -import 'package:hmi_core/src/core/error/failure.dart'; import 'package:hmi_core/src/core/log/log.dart'; -import 'package:hmi_core/src/core/result/result.dart'; import 'package:hmi_core/src/core/text_file.dart'; +import 'package:hmi_core/src/core/error/failure.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'package:hmi_core/src/core/result/result_transform_extension.dart'; +import 'package:hmi_core/src/core/result/result_boolean_operations_extension.dart'; /// +/// Decode json string into [Map]. class JsonMap { static const _log = Log('JsonMap'); - final FutureOr> _content; - const JsonMap._(FutureOr> content) : _content = content; - JsonMap.fromString(String content) : this._(Ok(content)); - JsonMap.fromTextFile(TextFile textFile) : this._(textFile.content); + final FutureOr>> _contents; + const JsonMap._(FutureOr>> contents) + : _contents = contents; + /// + /// Creates empty [JsonMap]. + const JsonMap.empty() : this._(const [Ok('')]); + /// + /// Creates [JsonMap] that parses itself from [text] with json. + JsonMap.fromString(String text) + : this._( + [Ok(text)], + ); + /// + /// Creates [JsonMap] that parses itself from json stored in [textFile]. + JsonMap.fromTextFile(TextFile textFile) + : this._( + textFile.content.then((content) => [content]), + ); + /// + /// Creates [JsonMap] that parses itself from json map stored in [textFiles]. + JsonMap.fromTextFiles(List textFiles) + : this._(Future.wait( + textFiles.map((textFile) => textFile.content), + )); /// + /// Decodes json and returns [Ok] with decoded [Map] + /// if successful, and [Err] otherwise. Future>> get decoded async { - const failureMessage = 'Failed to parse json map from file.'; - switch(await _content) { - case Ok(:final value): - try { - final decodedJson = const JsonCodec().decode(value) as Map; - return Ok( - decodedJson.cast(), - ); - } catch(error) { - _log.warning(failureMessage); - return Err( - Failure( - message: error.toString(), - stackTrace: StackTrace.current, - ), - ); - } - case Err(:final error): - _log.warning(failureMessage); - return Err(error); - } + final contentsResults = await _contents; + final contentsResult = contentsResults.fold>>( + Ok(List.from([])), + (contentsResult, contentResult) => contentsResult.andThen( + (contents) => contentResult.andThen( + (content) => Ok(contents..add(content)), + ), + ), + ); + return contentsResult + .andThen( + (contents) => contents.fold>>( + Ok(Map.from({})), + (mapResult, content) => mapResult.andThen((map) { + try { + final decodedJson = const JsonCodec().decode(content) as Map; + return Ok(map..addAll(decodedJson.cast())); + } catch (error, stackTrace) { + return Err(Failure( + message: '$error', + stackTrace: stackTrace, + )); + } + }), + ), + ) + .inspectErr( + (error) => _log.warning( + 'Failed to parse map from json., ${error.message}', + ), + ); } -} \ No newline at end of file +} From feb1ec1b8ddbdb1c487dad61e67d31e9dae2a665 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Thu, 17 Apr 2025 19:14:02 +0300 Subject: [PATCH 26/44] JsonMap | add tests for .fromFile constructor --- ...st.dart => json_map_from_string_test.dart} | 45 ++-- .../json/json_map_from_text_file_test.dart | 195 ++++++++++++++++++ 2 files changed, 221 insertions(+), 19 deletions(-) rename test/unit/core/json/{json_map_decoded_test.dart => json_map_from_string_test.dart} (88%) create mode 100644 test/unit/core/json/json_map_from_text_file_test.dart diff --git a/test/unit/core/json/json_map_decoded_test.dart b/test/unit/core/json/json_map_from_string_test.dart similarity index 88% rename from test/unit/core/json/json_map_decoded_test.dart rename to test/unit/core/json/json_map_from_string_test.dart index 8a0d10b..a237201 100644 --- a/test/unit/core/json/json_map_decoded_test.dart +++ b/test/unit/core/json/json_map_from_string_test.dart @@ -2,9 +2,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/src/core/json/json_map.dart'; import 'package:hmi_core/src/core/log/log.dart'; import 'package:hmi_core/src/core/result/result.dart'; +// void main() { Log.initialize(); - group('JsonMap decoded', () { + // + group('JsonMap.fromString decoded', () { + // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -30,10 +33,11 @@ void main() { final jsonMap = JsonMap.fromString(textJson); final result = await jsonMap.decoded; expect(result, isA()); - final decodedJson = (await jsonMap.decoded as Ok).value; + final decodedJson = (await jsonMap.decoded as Ok).value; expect(decodedJson, equals(parsedMap)); } }); + // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -59,10 +63,11 @@ void main() { final jsonMap = JsonMap.fromString(textJson); final result = await jsonMap.decoded; expect(result, isA()); - final decodedJson = (await jsonMap.decoded as Ok).value; + final decodedJson = (await jsonMap.decoded as Ok).value; expect(decodedJson, equals(parsedMap)); } }); + // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -88,10 +93,11 @@ void main() { final jsonMap = JsonMap.fromString(textJson); final result = await jsonMap.decoded; expect(result, isA()); - final decodedJson = (await jsonMap.decoded as Ok).value; + final decodedJson = (await jsonMap.decoded as Ok).value; expect(decodedJson, equals(parsedMap)); } }); + // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -117,25 +123,26 @@ void main() { final jsonMap = JsonMap.fromString(textJson); final result = await jsonMap.decoded; expect(result, isA()); - final decodedJson = (await jsonMap.decoded as Ok).value; + final decodedJson = (await jsonMap.decoded as Ok).value; expect(decodedJson, equals(parsedMap)); } }); + // test('returns Err on invalid jsons', () async { final invalidJsons = [ - { - 'text_json': '{', - }, - { - 'text_json': 'asd', - }, - { - 'text_json': '{"test":123', - }, - { - 'text_json': '[', - }, - ]; + { + 'text_json': '{', + }, + { + 'text_json': 'asd', + }, + { + 'text_json': '{"test":123', + }, + { + 'text_json': '[', + }, + ]; for (final invalidJson in invalidJsons) { final textJson = invalidJson['text_json']!; final jsonMap = JsonMap.fromString(textJson); @@ -144,4 +151,4 @@ void main() { } }); }); -} \ No newline at end of file +} diff --git a/test/unit/core/json/json_map_from_text_file_test.dart b/test/unit/core/json/json_map_from_text_file_test.dart new file mode 100644 index 0000000..bb6a894 --- /dev/null +++ b/test/unit/core/json/json_map_from_text_file_test.dart @@ -0,0 +1,195 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/hmi_core_failure.dart'; +import 'package:hmi_core/hmi_core_text_file.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +/// +/// Fake implementation of [TextFile] +class FakeTextFile implements TextFile { + ResultF _content; + /// + /// Creates [FakeTextFile] with a given [content] if provided. + FakeTextFile(ResultF content) : _content = content; + // + @override + Future> get content async { + return _content; + } + // + @override + Future write(String text) async { + _content = Ok(text); + } +} +// +void main() { + Log.initialize(); + // + group('JsonMap.fromTextFile decoded', () { + test('returns valid Map on valid jsons', () async { + final validIntJsons = [ + { + 'text_json': '{"value1":0,"value2":27,"value3":-123}', + 'parsed_map': { + 'value1': 0, + 'value2': 27, + 'value3': -123, + }, + }, + { + 'text_json': '{"value1":4294967295,"value2":2147483647,"value3":-2147483648}', + 'parsed_map': { + 'value1': 4294967295, + 'value2': 2147483647, + 'value3': -2147483648, + }, + }, + ]; + for (final validData in validIntJsons) { + final textJson = validData['text_json'] as String; + final parsedMap = validData['parsed_map']! as Map; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + } + }); + // + test('returns valid Map on valid jsons', () async { + final validIntJsons = [ + { + 'text_json': '{"value1":true,"value2":false,"value3":true}', + 'parsed_map': { + 'value1': true, + 'value2': false, + 'value3': true, + }, + }, + { + 'text_json': '{"value1":false,"value2":false,"value3":false}', + 'parsed_map': { + 'value1': false, + 'value2': false, + 'value3': false, + }, + }, + ]; + for (final validData in validIntJsons) { + final textJson = validData['text_json'] as String; + final parsedMap = validData['parsed_map']! as Map; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + } + }); + // + test('returns valid Map on valid jsons', () async { + final validIntJsons = [ + { + 'text_json': '{"value1":0.0,"value2":123.45,"value3":-1.80e308}', + 'parsed_map': { + 'value1': 0.0, + 'value2': 123.45, + 'value3': -1.80e308, + }, + }, + { + 'text_json': '{"value1":2.23e-308,"value2":-2.23e-308,"value3":1.80e308}', + 'parsed_map': { + 'value1': 2.23e-308, + 'value2': -2.23e-308, + 'value3': 1.80e308, + }, + }, + ]; + for (final validData in validIntJsons) { + final textJson = validData['text_json'] as String; + final parsedMap = validData['parsed_map']! as Map; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + } + }); + // + test('returns valid Map on valid jsons', () async { + final validIntJsons = [ + { + 'text_json': '{"value1":"0.0","value2":"123.45","value3":"-1.80e308"}', + 'parsed_map': { + 'value1': '0.0', + 'value2': '123.45', + 'value3': '-1.80e308', + }, + }, + { + 'text_json': '{"value1":"abcdefghijklmnopqrstuvwxyz","value2":"1234567890","value3":"!@#\$%^&*()_+-="}', + 'parsed_map': { + 'value1': 'abcdefghijklmnopqrstuvwxyz', + 'value2': '1234567890', + 'value3': '!@#\$%^&*()_+-=', + }, + }, + ]; + for (final validData in validIntJsons) { + final textJson = validData['text_json'] as String; + final parsedMap = validData['parsed_map']! as Map; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + } + }); + // + test('returns Err on invalid jsons', () async { + final invalidJsons = [ + { + 'text_json': '{', + }, + { + 'text_json': 'asd', + }, + { + 'text_json': '{"test":123', + }, + { + 'text_json': '[', + }, + ]; + for (final invalidJson in invalidJsons) { + final textJson = invalidJson['text_json']!; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + } + }); + // + test('returns Err on TextFile error', () async { + final filesWithError = [ + FakeTextFile(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '', + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '{"validJson":true}', + stackTrace: StackTrace.current, + ))), + ]; + for (final file in filesWithError) { + final jsonMap = JsonMap.fromTextFile(file); + final result = await jsonMap.decoded; + expect(result, isA()); + } + }); + }); +} From 3b1336c2c19e8c0cc290e47a192201fe14545d15 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Thu, 17 Apr 2025 19:19:47 +0300 Subject: [PATCH 27/44] AppSettings | add writable settings --- lib/src/app_settings/app_settings.dart | 174 ++++++++++++++++-- lib/src/core/json/json_list.dart | 25 ++- test/unit/auth/user_password_test.dart | 4 +- .../app_settings_initialize_test.dart | 5 +- .../entities/app_settings/setting_test.dart | 9 +- 5 files changed, 185 insertions(+), 32 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 7a0fae6..6aaa219 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -1,11 +1,20 @@ +import 'dart:convert'; import 'package:hmi_core/src/core/error/failure.dart'; import 'package:hmi_core/src/core/json/json_map.dart'; import 'package:hmi_core/src/core/log/log.dart'; -import 'package:hmi_core/src/core/result/result.dart'; +import 'package:hmi_core/hmi_core_text_file.dart'; +import 'package:hmi_core/src/core/result/result_transform_extension.dart'; /// +/// Stores application settings. +/// +/// Settings can be accessed for reading and writing by its keys. +/// +/// For key names prefer to use kebab-case notation, with unique prefixes +/// for different groups of settings. E.g. `ui-padding`, `ui-font-size`, +/// `api-host`, `api-port`, etc. class AppSettings { static const _log = Log('AppSettings'); - static final _map = { + static final _settings = { 'displaySizeWidth': 1024, 'displaySizeHeight': 768, // Place Durations in milliseconds! @@ -18,30 +27,165 @@ class AppSettings { 'floatingActionButtonSize': 60.0, 'floatingActionIconSize': 45.0, }; + static final _writable = {}; + static TextFile _writeFile = const TextFile.path('stored_settings.json'); + /// + /// Initializes app settings with [readOnly] and [writable] settings. + /// + /// [readOnly] settings are accessible for reading only. + /// + /// [writable] settings are accessible for reading and updating (writing). /// - static Future initialize({JsonMap? jsonMap}) async { - if (jsonMap != null) { - await jsonMap.decoded - .then((result) => switch(result) { - Ok(value: final map) => _map.addAll(map), - Err(: final error) => _log.warning('$AppSettings.initialize | Failed to initialize app settings from file. Error: $error'), - }); + /// [writeFile] is used to write and restore [writable] settings. + /// If not passed, file with path `stored_settings.json` will be used + /// by default. + static Future initialize({ + JsonMap readOnly = const JsonMap.empty(), + JsonMap writable = const JsonMap.empty(), + TextFile? writeFile, + }) async { + if (writeFile != null) { + _writeFile = writeFile; } + await _addSettings( + readOnly, + onSettingAdded: (entry) { + _log.info( + 'Added read-only setting "${entry.key}": ${entry.value}...', + ); + if (_settings.containsKey(entry.key)) { + _log.warning( + 'Setting with key "${entry.key}" was overwritten.', + ); + } + _writable[entry.key] = false; + }, + ); + await _addSettings( + writable, + onSettingAdded: (entry) { + _log.info( + 'Added writeable setting "${entry.key}": ${entry.value}...', + ); + if (_settings.containsKey(entry.key)) { + _log.warning( + 'Setting with key "${entry.key}" was overwritten.', + ); + } + _writable[entry.key] = true; + }, + ); + await _addSettings( + JsonMap.fromTextFile(_writeFile), + onSettingAdded: (entry) { + _log.info( + 'Restoring writeable setting "${entry.key}": ${entry.value}...', + ); + if (!_settings.containsKey(entry.key)) { + _settings.remove(entry.key); + _log.warning( + 'Setting with key "${entry.key}" does not exist and was ignored.', + ); + } + }, + ); + } + // + static Future _addSettings( + JsonMap settings, { + void Function(MapEntry)? onSettingAdded, + }) async { + final decodedResult = await settings.decoded; + decodedResult.inspect((map) { + for (final entry in map.entries) { + _settings[entry.key] = entry.value; + onSettingAdded?.call(entry); + } + }).inspectErr((error) { + _log.warning('Failed to initialize app settings, ${error.message}.'); + }); } /// - /// Returns stored value by it's key - static dynamic getSetting(String key, {dynamic Function(Failure err)? onError}) { - final setting = _map[key]; - if (setting == null) { + /// Returns value of app setting by [key]. + /// + /// Calls [onError] that can return other value based on passed [Failure]. + static dynamic getSetting( + String key, { + dynamic Function(Failure err)? onError, + }) { + if (!_settings.containsKey(key)) { final err = Failure( message: '$AppSettings.getSetting | Not found key "$key"', stackTrace: StackTrace.current, ); if (onError != null) { return onError(err); + } else { + throw err; } - throw err; } - return setting; + return _settings[key]; + } + /// + /// Immediately updates app setting with key [settingName] to new [value] + /// and save it asynchronously to file. + /// + /// Calls [onSuccess] or [onError] when setting will be saved successfully + /// or with error. In case of error returns app setting to its previous value. + static Future setSetting( + String settingName, + dynamic value, { + void Function(Failure error)? onError, + void Function()? onSuccess, + }) async { + if (!_settings.containsKey(settingName) || + !_writable.containsKey(settingName) || + !_writable[settingName]!) { + final failure = Failure( + message: 'Cannot update setting with key "$settingName".', + stackTrace: StackTrace.current, + ); + onError?.call(failure); + } else { + final valueBackup = _settings[settingName]; + _settings[settingName] = value; + final storedMap = await _getWritableSettings(); + storedMap[settingName] = value; + await _writeFile.write(json.encode(storedMap)).then( + (_) { + onSuccess?.call(); + }, + ).catchError( + (error, stackTrace) { + final failure = Failure( + message: 'Failed to save setting "$settingName", $error.', + stackTrace: stackTrace, + ); + _log.warning(failure.message); + _settings[settingName] = valueBackup; + onError?.call(failure); + }, + ); + } + } + /// + /// Returns map of writable settings. + /// + /// Gets entries from [_writeFile] if it exists, + /// otherwise gets writable entries from [_settings]. + static Future> _getWritableSettings() async { + final mapResult = await JsonMap.fromTextFile(_writeFile).decoded; + return mapResult.mapOrElse( + (_) { + return Map.fromEntries( + _settings.entries.where( + (entry) => _writable.containsKey(entry.key), + ), + ); + }, + (map) { + return map; + }, + ); } } diff --git a/lib/src/core/json/json_list.dart b/lib/src/core/json/json_list.dart index 55797b4..51d886b 100644 --- a/lib/src/core/json/json_list.dart +++ b/lib/src/core/json/json_list.dart @@ -4,24 +4,37 @@ import 'package:hmi_core/src/core/error/failure.dart'; import 'package:hmi_core/src/core/log/log.dart'; import 'package:hmi_core/src/core/result/result.dart'; import 'package:hmi_core/src/core/text_file.dart'; -/// +/// TODO: doc class JsonList { static const _log = Log('JsonList'); final FutureOr> _content; const JsonList._(FutureOr> content) : _content = content; - JsonList.fromString(String content) : this._(Ok(content)); - JsonList.fromTextFile(TextFile textFile) : this._(textFile.content); + /// TODO: doc + JsonList.fromString(String content) + : this._( + Ok(content), + ); + /// TODO: doc + JsonList.fromTextFile(TextFile textFile) + : this._( + textFile.content, + ); /// + /// Creates list that parses itself from json list stored in [textFiles]. + factory JsonList.fromTextFiles(List textFiles) { + throw UnimplementedError(); + } + /// TODO: doc Future>> get decoded async { const failureMessage = 'Failed to parse json list from file.'; - switch(await _content) { + switch (await _content) { case Ok(:final value): try { final decodedJson = const JsonCodec().decode(value) as List; return Ok( decodedJson.cast(), ); - } catch(error) { + } catch (error) { _log.warning(failureMessage); return Err( Failure( @@ -35,4 +48,4 @@ class JsonList { return Err(error); } } -} \ No newline at end of file +} diff --git a/test/unit/auth/user_password_test.dart b/test/unit/auth/user_password_test.dart index a4463f3..158e21e 100644 --- a/test/unit/auth/user_password_test.dart +++ b/test/unit/auth/user_password_test.dart @@ -1,13 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/hmi_core.dart'; - - void main() { group('UserPassword', () { Log.initialize(level: LogLevel.all); const log = Log('UserPassword'); AppSettings.initialize( - jsonMap: JsonMap.fromString('{"passwordKey": "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЁЯЧСМИТЬБЮйцукенгшщзхъфывапролджэёячсмитьбю"}'), + readOnly: JsonMap.fromString('{"passwordKey": "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЁЯЧСМИТЬБЮйцукенгшщзхъфывапролджэёячсмитьбю"}'), ); test('value', () async { const rawPassValue = '123qwe'; diff --git a/test/unit/core/entities/app_settings/app_settings_initialize_test.dart b/test/unit/core/entities/app_settings/app_settings_initialize_test.dart index 4a0301c..b02a553 100644 --- a/test/unit/core/entities/app_settings/app_settings_initialize_test.dart +++ b/test/unit/core/entities/app_settings/app_settings_initialize_test.dart @@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/hmi_core.dart'; import 'fake_text_file.dart'; import 'settings_data.dart'; - void main() { Log.initialize(); group('AppUiSettings initialize', () { @@ -11,7 +10,7 @@ void main() { final textFile = settings['text_file'] as String; final setSettings = settings['set_settings'] as Map; await AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ); @@ -38,7 +37,7 @@ void main() { for (final invalidJson in invalidJsons) { final textFile = invalidJson['text_file'] as String; expectLater(AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ), diff --git a/test/unit/core/entities/app_settings/setting_test.dart b/test/unit/core/entities/app_settings/setting_test.dart index b838d0a..440b7c2 100644 --- a/test/unit/core/entities/app_settings/setting_test.dart +++ b/test/unit/core/entities/app_settings/setting_test.dart @@ -3,7 +3,6 @@ import 'package:hmi_core/hmi_core.dart'; import 'package:hmi_core/hmi_core_app_settings.dart'; import 'fake_text_file.dart'; import 'settings_data.dart'; - void main() { Log.initialize(); group('Setting', () { @@ -14,7 +13,7 @@ void main() { final textFile = settings['text_file'] as String; final setSettings = settings['set_settings'] as Map; await AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ); @@ -46,7 +45,7 @@ void main() { final textFile = settings['text_file'] as String; final setSettings = settings['set_settings'] as Map; await AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ); @@ -77,7 +76,7 @@ void main() { final textFile = settings['text_file'] as String; final setSettings = settings['set_settings'] as Map; await AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ); @@ -105,7 +104,7 @@ void main() { for (final invalidJson in invalidJsons) { final textFile = invalidJson['text_file'] as String; expectLater(AppSettings.initialize( - jsonMap: JsonMap.fromTextFile( + readOnly: JsonMap.fromTextFile( FakeTextFile(textFile), ), ), From 3f0fe24499416102fb95e0178ea0283c7fbaa66b Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 17:42:21 +0300 Subject: [PATCH 28/44] Setting | add update method for writable settings --- lib/src/app_settings/setting.dart | 64 +++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index 226d369..13db9c6 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -1,29 +1,32 @@ import 'app_settings.dart'; import 'package:hmi_core/src/core/error/failure.dart'; - /// -/// Holds setting value stored in AppSettings by it name -/// - value can be returned in int, double or string represantation +/// Holds setting value stored in [AppSettings] by it name. +/// +/// Value can be returned in int, double or string representation. class Setting { final String _name; final double _factor; final dynamic Function(Failure err)? _onError; /// - /// - [name] - the name of value stored in AppSettings - /// - [factor] - returned value int or double will by multiplied by factor + /// Holds setting value stored in [AppSettings] by it [name]. + /// + /// Value can be returned in int, double or string representation. + /// [int] and [double] values are multiplied by [factor] before returning. + /// + /// Calls [onError] if getting value from [AppSettings] will fail. const Setting( String name, { double factor = 1.0, dynamic Function(Failure err)? onError, - }) : - _name = name, - _factor = factor, - _onError = onError; + }) : _name = name, + _factor = factor, + _onError = onError; /// /// Returns [Setting] new instance containing a [value] const factory Setting.from(dynamic value) = _SettingValue; - /// - /// Returns setting value in int represantation + /// + /// Returns setting value in [int] representation int get toInt { final value = AppSettings.getSetting(_name, onError: _onError); if (value is int) { @@ -31,8 +34,8 @@ class Setting { } return (double.parse('$value') * _factor).toInt(); } - /// - /// Returns setting value in double represantation + /// + /// Returns setting value in [double] representation double get toDouble { final value = AppSettings.getSetting(_name, onError: _onError); if (value is double) { @@ -40,8 +43,25 @@ class Setting { } return double.parse('$value') * _factor; } - /// - /// Returns setting value in string represantation + /// + /// Immediately updates app setting to new [value] and save it asynchronously. + /// + /// Calls [onSuccess] or [onError] when setting will be saved successfully + /// or with error. In case of error returns app setting to its previous value. + Future update( + dynamic value, { + void Function(Failure error)? onError, + void Function()? onSuccess, + }) async { + await AppSettings.setSetting( + _name, + value, + onError: onError, + onSuccess: onSuccess, + ); + } + /// + /// Returns setting value in [String] representation @override String toString() => '${AppSettings.getSetting(_name, onError: _onError)}'; } @@ -80,5 +100,17 @@ class _SettingValue implements Setting { } // @override + Future update( + dynamic value, { + void Function(Failure error)? onError, + void Function()? onSuccess, + }) async { + onError?.call(Failure( + message: 'Cannot update setting created during app runtime', + stackTrace: StackTrace.current, + )); + } + // + @override String toString() => '$_value'; -} \ No newline at end of file +} From 237836a53d85465db84a19adaa9413da7400cf10 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 17:44:13 +0300 Subject: [PATCH 29/44] JsonList | add fromTextFiles constructor --- lib/src/core/json/json_list.dart | 96 ++++++++++++++++++++------------ lib/src/core/json/json_map.dart | 6 +- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/lib/src/core/json/json_list.dart b/lib/src/core/json/json_list.dart index 51d886b..c7f758a 100644 --- a/lib/src/core/json/json_list.dart +++ b/lib/src/core/json/json_list.dart @@ -1,51 +1,73 @@ import 'dart:async'; import 'dart:convert'; -import 'package:hmi_core/src/core/error/failure.dart'; import 'package:hmi_core/src/core/log/log.dart'; -import 'package:hmi_core/src/core/result/result.dart'; import 'package:hmi_core/src/core/text_file.dart'; -/// TODO: doc +import 'package:hmi_core/src/core/error/failure.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'package:hmi_core/src/core/result/result_transform_extension.dart'; +import 'package:hmi_core/src/core/result/result_boolean_operations_extension.dart'; +/// +/// Decode json string into [List]. class JsonList { - static const _log = Log('JsonList'); - final FutureOr> _content; - const JsonList._(FutureOr> content) : _content = content; - /// TODO: doc - JsonList.fromString(String content) + static const _log = Log('JsonList '); + final FutureOr>> _contents; + const JsonList._(FutureOr>> contents) + : _contents = contents; + /// + /// Creates empty [JsonList]. + const JsonList.empty() : this._(const [Ok('')]); + /// + /// Creates [JsonList] that parses itself from [text] with json. + JsonList.fromString(String text) : this._( - Ok(content), + [Ok(text)], ); - /// TODO: doc + /// + /// Creates [JsonList] that parses itself from json stored in [textFile]. JsonList.fromTextFile(TextFile textFile) : this._( - textFile.content, + textFile.content.then((content) => [content]), ); /// - /// Creates list that parses itself from json list stored in [textFiles]. - factory JsonList.fromTextFiles(List textFiles) { - throw UnimplementedError(); - } - /// TODO: doc + /// Creates [JsonList] that parses itself from json stored in [textFiles]. + JsonList.fromTextFiles(List textFiles) + : this._(Future.wait( + textFiles.map((textFile) => textFile.content), + )); + /// + /// Decodes json and returns [Ok] with decoded [List] + /// if successful, and [Err] otherwise. Future>> get decoded async { - const failureMessage = 'Failed to parse json list from file.'; - switch (await _content) { - case Ok(:final value): - try { - final decodedJson = const JsonCodec().decode(value) as List; - return Ok( - decodedJson.cast(), - ); - } catch (error) { - _log.warning(failureMessage); - return Err( - Failure( - message: error.toString(), - stackTrace: StackTrace.current, - ), - ); - } - case Err(:final error): - _log.warning(failureMessage); - return Err(error); - } + final contentsResults = await _contents; + final contentsResult = contentsResults.fold>>( + Ok(List.from([])), + (contentsResult, contentResult) => contentsResult.andThen( + (contents) => contentResult.andThen( + (content) => Ok(contents..add(content)), + ), + ), + ); + return contentsResult + .andThen( + (contents) => contents.fold>>( + Ok(List.from([])), + (listResult, content) => listResult.andThen((list) { + try { + final decodedJson = const JsonCodec().decode(content) as List; + return Ok(list..addAll(decodedJson.cast())); + } catch (error, stackTrace) { + return Err(Failure( + message: '$error', + stackTrace: stackTrace, + )); + } + }), + ), + ) + .inspectErr( + (error) => _log.warning( + 'Failed to parse list from json, ${error.message}', + ), + ); } } diff --git a/lib/src/core/json/json_map.dart b/lib/src/core/json/json_map.dart index d16e316..6d9abc5 100644 --- a/lib/src/core/json/json_map.dart +++ b/lib/src/core/json/json_map.dart @@ -9,7 +9,7 @@ import 'package:hmi_core/src/core/result/result_boolean_operations_extension.dar /// /// Decode json string into [Map]. class JsonMap { - static const _log = Log('JsonMap'); + static const _log = Log('JsonMap '); final FutureOr>> _contents; const JsonMap._(FutureOr>> contents) : _contents = contents; @@ -29,7 +29,7 @@ class JsonMap { textFile.content.then((content) => [content]), ); /// - /// Creates [JsonMap] that parses itself from json map stored in [textFiles]. + /// Creates [JsonMap] that parses itself from json stored in [textFiles]. JsonMap.fromTextFiles(List textFiles) : this._(Future.wait( textFiles.map((textFile) => textFile.content), @@ -66,7 +66,7 @@ class JsonMap { ) .inspectErr( (error) => _log.warning( - 'Failed to parse map from json., ${error.message}', + 'Failed to parse map from json, ${error.message}', ), ); } From 05089b1765a3cdc2a8871798ebbc869dcdbcd2f4 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 17:52:12 +0300 Subject: [PATCH 30/44] JsonMap, JsonList | add tests for each named constructors --- ...t.dart => json_list_from_string_test.dart} | 55 ++-- .../json/json_list_from_text_file_test.dart | 161 ++++++++++++ .../json/json_list_from_text_files_test.dart | 196 +++++++++++++++ .../core/json/json_map_from_string_test.dart | 62 +++-- .../json/json_map_from_text_file_test.dart | 57 ++++- .../json/json_map_from_text_files_test.dart | 237 ++++++++++++++++++ 6 files changed, 708 insertions(+), 60 deletions(-) rename test/unit/core/json/{json_list_decoded_test.dart => json_list_from_string_test.dart} (78%) create mode 100644 test/unit/core/json/json_list_from_text_file_test.dart create mode 100644 test/unit/core/json/json_list_from_text_files_test.dart create mode 100644 test/unit/core/json/json_map_from_text_files_test.dart diff --git a/test/unit/core/json/json_list_decoded_test.dart b/test/unit/core/json/json_list_from_string_test.dart similarity index 78% rename from test/unit/core/json/json_list_decoded_test.dart rename to test/unit/core/json/json_list_from_string_test.dart index 7aa557e..0ce74c2 100644 --- a/test/unit/core/json/json_list_decoded_test.dart +++ b/test/unit/core/json/json_list_from_string_test.dart @@ -2,9 +2,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/src/core/json/json_list.dart'; import 'package:hmi_core/src/core/log/log.dart'; import 'package:hmi_core/src/core/result/result.dart'; +// void main() { Log.initialize(); - group('JsonList decoded', () { + group('JsonList.fromString decoded', () { test('returns valid List on valid jsons', () async { final validIntJsons = [ { @@ -22,12 +23,12 @@ void main() { final jsonList = JsonList.fromString(textJson); final result = await jsonList.decoded; expect(result, isA()); - final decodedJson = (await jsonList.decoded as Ok).value; + final decodedJson = (await jsonList.decoded as Ok).value; expect(decodedJson, equals(parsedList)); } }); test('returns valid List on valid jsons', () async { - final validIntJsons = [ + final validBoolJsons = [ { 'text_json': '[true,false,true]', 'parsed_list': [true, false, true], @@ -37,18 +38,18 @@ void main() { 'parsed_list': [false, false, false], }, ]; - for (final validData in validIntJsons) { + for (final validData in validBoolJsons) { final textJson = validData['text_json'] as String; final parsedList = validData['parsed_list']! as List; final jsonList = JsonList.fromString(textJson); final result = await jsonList.decoded; expect(result, isA()); - final decodedJson = (await jsonList.decoded as Ok).value; + final decodedJson = (await jsonList.decoded as Ok).value; expect(decodedJson, equals(parsedList)); } }); test('returns valid List on valid jsons', () async { - final validIntJsons = [ + final validDoubleJsons = [ { 'text_json': '[0.0,123.45,-1.80e308]', 'parsed_list': [0.0, 123.45, -1.80e308], @@ -58,18 +59,18 @@ void main() { 'parsed_list': [2.23e-308, -2.23e-308, 1.80e308], }, ]; - for (final validData in validIntJsons) { + for (final validData in validDoubleJsons) { final textJson = validData['text_json'] as String; final parsedList = validData['parsed_list']! as List; final jsonList = JsonList.fromString(textJson); final result = await jsonList.decoded; expect(result, isA()); - final decodedJson = (await jsonList.decoded as Ok).value; + final decodedJson = (await jsonList.decoded as Ok).value; expect(decodedJson, equals(parsedList)); } }); test('returns valid List on valid jsons', () async { - final validIntJsons = [ + final validStringJsons = [ { 'text_json': '["0.0","123.45","-1.80e308"]', 'parsed_list': ["0.0", "123.45", "-1.80e308"], @@ -79,37 +80,37 @@ void main() { 'parsed_list': ["abcdefghijklmnopqrstuvwxyz", "1234567890", "!@#\$%^&*()_+-="], }, ]; - for (final validData in validIntJsons) { + for (final validData in validStringJsons) { final textJson = validData['text_json'] as String; final parsedList = validData['parsed_list']! as List; final jsonList = JsonList.fromString(textJson); final result = await jsonList.decoded; expect(result, isA()); - final decodedJson = (await jsonList.decoded as Ok).value; + final decodedJson = (await jsonList.decoded as Ok).value; expect(decodedJson, equals(parsedList)); } }); test('returns Err on invalid jsons', () async { final invalidJsons = [ - { - 'text_json': '{', - }, - { - 'text_json': 'asd', - }, - { - 'text_json': '{"test":123', - }, - { - 'text_json': '[', - }, - ]; + { + 'text_json': '{', + }, + { + 'text_json': 'asd', + }, + { + 'text_json': '{"test":123', + }, + { + 'text_json': '[', + }, + ]; for (final invalidJson in invalidJsons) { final textJson = invalidJson['text_json']!; - final jsonMap = JsonList.fromString(textJson); - final result = await jsonMap.decoded; + final jsonList = JsonList.fromString(textJson); + final result = await jsonList.decoded; expect(result, isA()); } }); }); -} \ No newline at end of file +} diff --git a/test/unit/core/json/json_list_from_text_file_test.dart b/test/unit/core/json/json_list_from_text_file_test.dart new file mode 100644 index 0000000..a9d40da --- /dev/null +++ b/test/unit/core/json/json_list_from_text_file_test.dart @@ -0,0 +1,161 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/src/core/json/json_list.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'json_map_from_text_file_test.dart'; +// +void main() { + Log.initialize(); + group('JsonList.fromTextFile decoded', () { + test('returns valid List on valid jsons', () async { + final validIntJsons = [ + { + 'text_json': '[0,27,-123]', + 'parsed_list': [0, 27, -123], + }, + { + 'text_json': '[4294967295, 2147483647, -2147483648]', + 'parsed_list': [4294967295, 2147483647, -2147483648], + }, + ]; + for (final validData in validIntJsons) { + final textJson = validData['text_json'] as String; + final parsedList = validData['parsed_list']! as List; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + } + }); + test('returns valid List on valid jsons', () async { + final validBoolJsons = [ + { + 'text_json': '[true,false,true]', + 'parsed_list': [true, false, true], + }, + { + 'text_json': '[false,false,false]', + 'parsed_list': [false, false, false], + }, + ]; + for (final validData in validBoolJsons) { + final textJson = validData['text_json'] as String; + final parsedList = validData['parsed_list']! as List; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + } + }); + test('returns valid List on valid jsons', () async { + final validDoubleJsons = [ + { + 'text_json': '[0.0,123.45,-1.80e308]', + 'parsed_list': [0.0, 123.45, -1.80e308], + }, + { + 'text_json': '[2.23e-308,-2.23e-308,1.80e308]', + 'parsed_list': [2.23e-308, -2.23e-308, 1.80e308], + }, + ]; + for (final validData in validDoubleJsons) { + final textJson = validData['text_json'] as String; + final parsedList = validData['parsed_list']! as List; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + } + }); + test('returns valid List on valid jsons', () async { + final validStringJsons = [ + { + 'text_json': '["0.0","123.45","-1.80e308"]', + 'parsed_list': ["0.0", "123.45", "-1.80e308"], + }, + { + 'text_json': '["abcdefghijklmnopqrstuvwxyz","1234567890","!@#\$%^&*()_+-="]', + 'parsed_list': ["abcdefghijklmnopqrstuvwxyz", "1234567890", "!@#\$%^&*()_+-="], + }, + ]; + for (final validData in validStringJsons) { + final textJson = validData['text_json'] as String; + final parsedList = validData['parsed_list']! as List; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + } + }); + test('returns Err on invalid jsons', () async { + final invalidJsons = [ + { + 'text_json': '{', + }, + { + 'text_json': 'asd', + }, + { + 'text_json': '{"test":123', + }, + { + 'text_json': '[', + }, + ]; + for (final invalidJson in invalidJsons) { + final textJson = invalidJson['text_json']!; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + } + }); + test('returns Err on valid non-list json', () async { + final invalidListJsons = [ + { + 'text_json': '{}', + }, + { + 'text_json': 'true', + }, + { + 'text_json': 'false', + }, + { + 'text_json': 'null', + }, + { + 'text_json': '"valid"', + }, + { + 'text_json': '123', + }, + { + 'text_json': '123.45', + }, + ]; + for (final invalidJson in invalidListJsons) { + final textJson = invalidJson['text_json']!; + final jsonList = JsonList.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonList.decoded; + expect(result, isA()); + } + }); + test('does not write to input file', () async { + bool isWritten = false; + final jsonMap = JsonList.fromTextFile( + FakeTextFile( + const Ok('["valid", "json"]'), + writeFuture: (_) async { + isWritten = true; + }, + ), + ); + await jsonMap.decoded; + expect(isWritten, isFalse); + }); + }); +} diff --git a/test/unit/core/json/json_list_from_text_files_test.dart b/test/unit/core/json/json_list_from_text_files_test.dart new file mode 100644 index 0000000..2913d41 --- /dev/null +++ b/test/unit/core/json/json_list_from_text_files_test.dart @@ -0,0 +1,196 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/src/core/error/failure.dart'; +import 'package:hmi_core/src/core/json/json_list.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'json_map_from_text_file_test.dart'; +// +void main() { + Log.initialize(); + group('JsonList.fromTextFiles decoded', () { + test('returns valid List on valid jsons', () async { + final validIntJsons = [ + '[0,27,-123]', + '[4294967295, 2147483647, -2147483648]', + ]; + final parsedList = [0, 27, -123, 4294967295, 2147483647, -2147483648]; + final jsonList = JsonList.fromTextFiles( + validIntJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + }); + test('returns valid List on valid jsons', () async { + final validBoolJsons = [ + '[true,false,true]', + '[false,false,false]', + ]; + final parsedList = [true, false, true, false, false, false]; + final jsonList = JsonList.fromTextFiles( + validBoolJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + }); + test('returns valid List on valid jsons', () async { + final validDoubleJsons = [ + '[0.0,123.45,-1.80e308]', + '[2.23e-308,-2.23e-308,1.80e308]', + ]; + final parsedList = [ + 0.0, + 123.45, + -1.80e308, + 2.23e-308, + -2.23e-308, + 1.80e308, + ]; + final jsonList = JsonList.fromTextFiles( + validDoubleJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + }); + test('returns valid List on valid jsons', () async { + final validStringJsons = [ + '["0.0","123.45","-1.80e308"]', + '["abcdefghijklmnopqrstuvwxyz","1234567890","!@#\$%^&*()_+-="]', + ]; + final parsedList = [ + '0.0', + '123.45', + '-1.80e308', + 'abcdefghijklmnopqrstuvwxyz', + '1234567890', + '!@#\$%^&*()_+-=', + ]; + final jsonList = JsonList.fromTextFiles( + validStringJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + final decodedJson = (await jsonList.decoded as Ok).value; + expect(decodedJson, equals(parsedList)); + }); + test('returns Err on invalid jsons', () async { + final invalidJsons = [ + '{', + 'asd', + '{"test":123', + '[', + ]; + final jsonList = JsonList.fromTextFiles( + invalidJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + }); + test('returns Err on valid non-list json', () async { + final invalidListJsons = [ + '{}', + 'true', + 'false', + 'null', + '"valid"', + '123', + '123.45', + ]; + final jsonList = JsonList.fromTextFiles( + invalidListJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + }); + test('does not write to input file', () async { + bool isWritten = false; + final jsonMap = JsonList.fromTextFile( + FakeTextFile( + const Ok('["valid", "json"]'), + writeFuture: (_) async { + isWritten = true; + }, + ), + ); + await jsonMap.decoded; + expect(isWritten, isFalse); + }); + test('returns Err if any json is invalid', () async { + final invalidJsons = [ + '["valid","json"]', + '{', + 'asd', + '{"test":123', + '[', + ]; + final jsonList = JsonList.fromTextFiles( + invalidJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonList.decoded; + expect(result, isA()); + }); + test('returns Err on TextFile error', () async { + final filesWithError = [ + FakeTextFile(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '', + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '["valid","json"]', + stackTrace: StackTrace.current, + ))), + ]; + final jsonList = JsonList.fromTextFiles(filesWithError); + final result = await jsonList.decoded; + expect(result, isA()); + }); + test('returns Err if any TextFile with error', () async { + final filesWithError = [ + FakeTextFile(const Ok('["valid","json"]')), + FakeTextFile(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '', + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '{"validJson":true}', + stackTrace: StackTrace.current, + ))), + ]; + final jsonList = JsonList.fromTextFiles(filesWithError); + final result = await jsonList.decoded; + expect(result, isA()); + }); + test('does not write to input file', () async { + bool isWritten = false; + final jsonMap = JsonList.fromTextFiles([ + FakeTextFile( + const Ok('["valid","json"]'), + writeFuture: (_) async { + isWritten = true; + }, + ), + FakeTextFile( + const Ok('invalid json'), + writeFuture: (_) async { + isWritten = true; + }, + ), + ]); + await jsonMap.decoded; + expect(isWritten, isFalse); + }); + }); +} diff --git a/test/unit/core/json/json_map_from_string_test.dart b/test/unit/core/json/json_map_from_string_test.dart index a237201..381e2a0 100644 --- a/test/unit/core/json/json_map_from_string_test.dart +++ b/test/unit/core/json/json_map_from_string_test.dart @@ -2,21 +2,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/src/core/json/json_map.dart'; import 'package:hmi_core/src/core/log/log.dart'; import 'package:hmi_core/src/core/result/result.dart'; + // void main() { Log.initialize(); - // group('JsonMap.fromString decoded', () { - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { 'text_json': '{"value1":0,"value2":27,"value3":-123}', - 'parsed_map': { - 'value1': 0, - 'value2': 27, - 'value3': -123, - }, + 'parsed_map': {'value1': 0, 'value2': 27, 'value3': -123}, }, { 'text_json': '{"value1":4294967295,"value2":2147483647,"value3":-2147483648}', @@ -37,24 +32,15 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { 'text_json': '{"value1":true,"value2":false,"value3":true}', - 'parsed_map': { - 'value1': true, - 'value2': false, - 'value3': true, - }, + 'parsed_map': {'value1': true, 'value2': false, 'value3': true}, }, { 'text_json': '{"value1":false,"value2":false,"value3":false}', - 'parsed_map': { - 'value1': false, - 'value2': false, - 'value3': false, - }, + 'parsed_map': {'value1': false, 'value2': false, 'value3': false}, }, ]; for (final validData in validIntJsons) { @@ -67,16 +53,11 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { 'text_json': '{"value1":0.0,"value2":123.45,"value3":-1.80e308}', - 'parsed_map': { - 'value1': 0.0, - 'value2': 123.45, - 'value3': -1.80e308, - }, + 'parsed_map': {'value1': 0.0, 'value2': 123.45, 'value3': -1.80e308}, }, { 'text_json': '{"value1":2.23e-308,"value2":-2.23e-308,"value3":1.80e308}', @@ -97,7 +78,6 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -127,7 +107,6 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns Err on invalid jsons', () async { final invalidJsons = [ { @@ -150,5 +129,36 @@ void main() { expect(result, isA()); } }); + test('returns Err on valid non-map jsons', () async { + final invalidJsons = [ + { + 'text_json': '[]', + }, + { + 'text_json': 'true', + }, + { + 'text_json': 'false', + }, + { + 'text_json': 'null', + }, + { + 'text_json': '"valid"', + }, + { + 'text_json': '123', + }, + { + 'text_json': '123.45', + }, + ]; + for (final invalidJson in invalidJsons) { + final textJson = invalidJson['text_json']!; + final jsonMap = JsonMap.fromString(textJson); + final result = await jsonMap.decoded; + expect(result, isA()); + } + }); }); } diff --git a/test/unit/core/json/json_map_from_text_file_test.dart b/test/unit/core/json/json_map_from_text_file_test.dart index bb6a894..19a224f 100644 --- a/test/unit/core/json/json_map_from_text_file_test.dart +++ b/test/unit/core/json/json_map_from_text_file_test.dart @@ -8,9 +8,11 @@ import 'package:hmi_core/src/core/result/result.dart'; /// Fake implementation of [TextFile] class FakeTextFile implements TextFile { ResultF _content; + final Future Function(String value)? writeFuture; /// /// Creates [FakeTextFile] with a given [content] if provided. - FakeTextFile(ResultF content) : _content = content; + FakeTextFile(ResultF content, {this.writeFuture}) + : _content = content; // @override Future> get content async { @@ -19,13 +21,13 @@ class FakeTextFile implements TextFile { // @override Future write(String text) async { + await writeFuture?.call(text); _content = Ok(text); } } // void main() { Log.initialize(); - // group('JsonMap.fromTextFile decoded', () { test('returns valid Map on valid jsons', () async { final validIntJsons = [ @@ -56,7 +58,6 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -86,7 +87,6 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -109,14 +109,14 @@ void main() { for (final validData in validIntJsons) { final textJson = validData['text_json'] as String; final parsedMap = validData['parsed_map']! as Map; - final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final jsonMap = + JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); final result = await jsonMap.decoded; expect(result, isA()); final decodedJson = (await jsonMap.decoded as Ok).value; expect(decodedJson, equals(parsedMap)); } }); - // test('returns valid Map on valid jsons', () async { final validIntJsons = [ { @@ -146,7 +146,6 @@ void main() { expect(decodedJson, equals(parsedMap)); } }); - // test('returns Err on invalid jsons', () async { final invalidJsons = [ { @@ -170,6 +169,37 @@ void main() { } }); // + test('returns Err on valid non-map jsons', () async { + final invalidMapJsons = [ + { + 'text_json': '[]', + }, + { + 'text_json': 'true', + }, + { + 'text_json': 'false', + }, + { + 'text_json': 'null', + }, + { + 'text_json': '"valid"', + }, + { + 'text_json': '123', + }, + { + 'text_json': '123.45', + }, + ]; + for (final invalidJson in invalidMapJsons) { + final textJson = invalidJson['text_json']!; + final jsonMap = JsonMap.fromTextFile(FakeTextFile(Ok(textJson))); + final result = await jsonMap.decoded; + expect(result, isA()); + } + }); test('returns Err on TextFile error', () async { final filesWithError = [ FakeTextFile(Err(Failure( @@ -192,4 +222,17 @@ void main() { } }); }); + test('does not write to input file', () async { + bool isWritten = false; + final jsonMap = JsonMap.fromTextFile( + FakeTextFile( + const Ok('{"validJson":true}'), + writeFuture: (_) async { + isWritten = true; + }, + ), + ); + await jsonMap.decoded; + expect(isWritten, isFalse); + }); } diff --git a/test/unit/core/json/json_map_from_text_files_test.dart b/test/unit/core/json/json_map_from_text_files_test.dart new file mode 100644 index 0000000..5a3efdf --- /dev/null +++ b/test/unit/core/json/json_map_from_text_files_test.dart @@ -0,0 +1,237 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/hmi_core_failure.dart'; +import 'package:hmi_core/hmi_core_text_file.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +/// +/// Fake implementation of [TextFile] +class FakeTextFile implements TextFile { + ResultF _content; + final Future Function(String value)? writeFuture; + /// + /// Creates [FakeTextFile] with a given [content] if provided. + FakeTextFile(ResultF content, {this.writeFuture}) + : _content = content; + // + @override + Future> get content async { + return _content; + } + // + @override + Future write(String text) async { + await writeFuture?.call(text); + _content = Ok(text); + } +} +// +void main() { + Log.initialize(); + group('JsonMap.fromTextFiles decoded', () { + test('returns valid Map on valid jsons', () async { + final validIntJsons = [ + '{"value1":0,"value2":27,"value3":-123}', + '{"value4":4294967295,"value5":2147483647,"value6":-2147483648}', + ]; + final parsedMap = { + 'value1': 0, + 'value2': 27, + 'value3': -123, + 'value4': 4294967295, + 'value5': 2147483647, + 'value6': -2147483648, + }; + final jsonMap = JsonMap.fromTextFiles( + validIntJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + }); + test('returns valid Map on valid jsons', () async { + final validBoolJsons = [ + '{"value1":true,"value2":false,"value3":true}', + '{"value4":false,"value5":false,"value6":false}', + ]; + final parsedMap = { + 'value1': true, + 'value2': false, + 'value3': true, + 'value4': false, + 'value5': false, + 'value6': false, + }; + final jsonMap = JsonMap.fromTextFiles( + validBoolJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + }); + test('returns valid Map on valid jsons', () async { + final validDoubleJsons = [ + '{"value1":0.0,"value2":123.45,"value3":-1.80e308}', + '{"value4":2.23e-308,"value5":-2.23e-308,"value6":1.80e308}', + ]; + final parsedMap = { + 'value1': 0.0, + 'value2': 123.45, + 'value3': -1.80e308, + 'value4': 2.23e-308, + 'value5': -2.23e-308, + 'value6': 1.80e308, + }; + final jsonMap = JsonMap.fromTextFiles( + validDoubleJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + }); + test('returns valid Map on valid jsons', () async { + final validStringJsons = [ + '{"value1":"0.0","value2":"123.45","value3":"-1.80e308"}', + '{"value4":"abcdefghijklmnopqrstuvwxyz","value5":"1234567890","value6":"!@#\$%^&*()_+-="}', + ]; + final parsedMap = { + 'value1': '0.0', + 'value2': '123.45', + 'value3': '-1.80e308', + 'value4': 'abcdefghijklmnopqrstuvwxyz', + 'value5': '1234567890', + 'value6': '!@#\$%^&*()_+-=', + }; + final jsonMap = JsonMap.fromTextFiles( + validStringJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + }); + test('rewrites values in order of passed files', () async { + final validJsons = [ + '{"value1":1,"value2":1,"value3":1}', + '{"value2":2,"value3":2}', + '{"value3":3}', + ]; + final parsedMap = { + 'value1': 1, + 'value2': 2, + 'value3': 3, + }; + final jsonMap = JsonMap.fromTextFiles( + validJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + final decodedJson = (await jsonMap.decoded as Ok).value; + expect(decodedJson, equals(parsedMap)); + }); + test('returns Err on invalid jsons', () async { + final invalidJsons = [ + '{', + 'asd', + '{"test":123', + '[', + ]; + final jsonMap = JsonMap.fromTextFiles( + invalidJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + }); + test('returns Err on valid non-map jsons', () async { + final invalidMapJsons = [ + '[]', + 'true', + 'false', + 'null', + '"valid"', + '123', + '123.45', + ]; + final jsonMap = JsonMap.fromTextFiles( + invalidMapJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + }); + test('returns Err if any json is invalid', () async { + final invalidJsons = [ + '{"validJson":true}', + '{', + 'asd', + '{"test":123', + '[', + ]; + final jsonMap = JsonMap.fromTextFiles( + invalidJsons.map((json) => FakeTextFile(Ok(json))).toList(), + ); + final result = await jsonMap.decoded; + expect(result, isA()); + }); + test('returns Err on TextFile error', () async { + final filesWithError = [ + FakeTextFile(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '', + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '{"validJson":true}', + stackTrace: StackTrace.current, + ))), + ]; + final jsonMap = JsonMap.fromTextFiles(filesWithError); + final result = await jsonMap.decoded; + expect(result, isA()); + }); + test('returns Err if any TextFile with error', () async { + final filesWithError = [ + FakeTextFile(const Ok('{"validJson":true}')), + FakeTextFile(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '', + stackTrace: StackTrace.current, + ))), + FakeTextFile(Err(Failure( + message: '{"validJson":true}', + stackTrace: StackTrace.current, + ))), + ]; + final jsonMap = JsonMap.fromTextFiles(filesWithError); + final result = await jsonMap.decoded; + expect(result, isA()); + }); + test('does not write to input file', () async { + bool isWritten = false; + final jsonMap = JsonMap.fromTextFiles([ + FakeTextFile( + const Ok('{"validJson":true}'), + writeFuture: (_) async { + isWritten = true; + }, + ), + FakeTextFile( + const Ok('invalid json'), + writeFuture: (_) async { + isWritten = true; + }, + ), + ]); + await jsonMap.decoded; + expect(isWritten, isFalse); + }); + }); +} From 385f3d0f18b45365e3db1b5ff013fd81b45fad00 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 18:09:18 +0300 Subject: [PATCH 31/44] AppSettings | add tests for setSetting method --- .../app_settings_set_setting_test.dart | 223 ++++++++++++++++++ .../entities/app_settings/fake_text_file.dart | 13 +- 2 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 test/unit/core/entities/app_settings/app_settings_set_setting_test.dart diff --git a/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart b/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart new file mode 100644 index 0000000..6dc211d --- /dev/null +++ b/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart @@ -0,0 +1,223 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/src/app_settings/app_settings.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'fake_text_file.dart'; +/// +/// Fake implementation of [JsonMap] for tests +class FakeJsonMap implements JsonMap { + final ResultF> _map; + /// + /// Creates [FakeJsonMap] with a given [map]. + const FakeJsonMap(ResultF> map) : _map = map; + // + @override + Future>> get decoded async { + return _map; + } +} +// +void main() { + Log.initialize(); + final testsData = [ + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":0}', + 'set_value': 0, + }, // same value + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":-27}', + 'set_value': -27, + }, // int + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":-27.0}', + 'set_value': -27.0, + }, // double + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":"abc"}', + 'set_value': 'abc', + }, // string + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":true}', + 'set_value': true, + }, // bool + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":null}', + 'set_value': null, + }, // null + ]; + group('AppSettings setSetting', () { + test('write setting to file', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeJson = data['write_json'] as String; + String writeContent = ''; + final writeFile = FakeTextFile( + writeContent, + writeFuture: (value) async { + writeContent = value; + }, + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting(key, setValue); + expect(writeJson, writeContent); + } + }); + test('update setting value', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.value(), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting(key, setValue); + expect(AppSettings.getSetting(key), setValue); + } + }); + test('call onSuccess if setting saved successfully', () async { + for (final data in testsData) { + bool isSuccess = false; + bool isError = false; + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.value(), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting( + key, + setValue, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isTrue); + expect(isError, isFalse); + } + }); + test('call onError if setting saved with error', () async { + for (final data in testsData) { + bool isSuccess = false; + bool isError = false; + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting( + key, + setValue, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + } + }); + test('call onError if setting is not found', () async { + bool isSuccess = false; + bool isError = false; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writeFile: writeFile, + ); + await AppSettings.setSetting( + 'test_not_found_setting', + 10, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + }); + test('call onError if setting is not writable', () async { + bool isSuccess = false; + bool isError = false; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + readOnly: const FakeJsonMap(Ok({"test_not_write_setting": 0})), + writeFile: writeFile, + ); + await AppSettings.setSetting( + 'test_not_write_setting', + 10, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + }); + test('returns setting to previous value on error', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting(key, setValue); + expect(AppSettings.getSetting(key), map[key]); + } + }); + }); +} diff --git a/test/unit/core/entities/app_settings/fake_text_file.dart b/test/unit/core/entities/app_settings/fake_text_file.dart index 4955e07..ef92fae 100644 --- a/test/unit/core/entities/app_settings/fake_text_file.dart +++ b/test/unit/core/entities/app_settings/fake_text_file.dart @@ -2,13 +2,16 @@ import 'package:hmi_core/src/core/result/result.dart'; import 'package:hmi_core/src/core/text_file.dart'; /// class FakeTextFile implements TextFile { - final String text; - final Future? writeFuture; - const FakeTextFile(this.text, {this.writeFuture}); + String text; + final Future Function(String value)? writeFuture; + FakeTextFile(this.text, {this.writeFuture}); // @override Future> get content => Future.value(Ok(text)); // @override - Future write(String text) => writeFuture ?? Future(() {return;}); -} \ No newline at end of file + Future write(String text) async { + await writeFuture?.call(text); + this.text = text; + } +} From 3f879b8889b6ab7ebecb678180456459b1932c0f Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 18:10:08 +0300 Subject: [PATCH 32/44] Setting | add tests for update method --- .../app_settings/setting_from_test.dart | 22 +- .../entities/app_settings/setting_test.dart | 1 - .../app_settings/setting_update_test.dart | 221 ++++++++++++++++++ 3 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 test/unit/core/entities/app_settings/setting_update_test.dart diff --git a/test/unit/core/entities/app_settings/setting_from_test.dart b/test/unit/core/entities/app_settings/setting_from_test.dart index dd9c6b9..a913065 100644 --- a/test/unit/core/entities/app_settings/setting_from_test.dart +++ b/test/unit/core/entities/app_settings/setting_from_test.dart @@ -1,13 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hmi_core/hmi_core.dart'; import 'package:hmi_core/hmi_core_app_settings.dart'; - void main() { group('Setting.from', () { Log.initialize(level: LogLevel.all); const log = Log('Setting'); const values = [12, -12, 0.44, -0.12, 123.32, -432.21]; - test('toInt', () async { + test('toInt', () { for (final value in values) { final setting = Setting.from(value); final vInt = setting.toInt; @@ -15,7 +14,7 @@ void main() { expect(vInt, value.toInt()); } }); - test('toDouble', () async { + test('toDouble', () { for (final value in values) { final setting = Setting.from(value); final vDauble = setting.toDouble; @@ -23,7 +22,7 @@ void main() { expect(vDauble, value); } }); - test('toString', () async { + test('toString', () { const values = [12, -12, 0.44, -0.12, 123.32, -432.21, 'aef323ewf']; for (final value in values) { final setting = Setting.from(value); @@ -32,5 +31,18 @@ void main() { expect(vString, '$value'); } }); + test('update', () async { + const values = [12, -12, 0.44, -0.12, 123.32, -432.21, 'aef323ewf']; + for (final value in values) { + final setting = Setting.from(value); + bool? isUpdateFailed; + await setting.update( + value, + onError: (_) => isUpdateFailed = true, + onSuccess: () => isUpdateFailed = false, + ); + expect(isUpdateFailed, isTrue); + } + }); }); -} \ No newline at end of file +} diff --git a/test/unit/core/entities/app_settings/setting_test.dart b/test/unit/core/entities/app_settings/setting_test.dart index 440b7c2..255a010 100644 --- a/test/unit/core/entities/app_settings/setting_test.dart +++ b/test/unit/core/entities/app_settings/setting_test.dart @@ -4,7 +4,6 @@ import 'package:hmi_core/hmi_core_app_settings.dart'; import 'fake_text_file.dart'; import 'settings_data.dart'; void main() { - Log.initialize(); group('Setting', () { Log.initialize(level: LogLevel.all); const log = Log('Setting'); diff --git a/test/unit/core/entities/app_settings/setting_update_test.dart b/test/unit/core/entities/app_settings/setting_update_test.dart new file mode 100644 index 0000000..9398b06 --- /dev/null +++ b/test/unit/core/entities/app_settings/setting_update_test.dart @@ -0,0 +1,221 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/src/app_settings/app_settings.dart'; +import 'package:hmi_core/src/app_settings/setting.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'fake_text_file.dart'; +/// +/// Fake implementation of [JsonMap] for tests +class FakeJsonMap implements JsonMap { + final ResultF> _map; + + /// + /// Creates [FakeJsonMap] with a given [map]. + const FakeJsonMap(ResultF> map) : _map = map; + // + @override + Future>> get decoded async { + return _map; + } +} +// +void main() { + Log.initialize(); + final testsData = [ + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":0}', + 'set_value': 0, + }, // same value + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":-27}', + 'set_value': -27, + }, // int + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":-27.0}', + 'set_value': -27.0, + }, // double + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":"abc"}', + 'set_value': 'abc', + }, // string + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":true}', + 'set_value': true, + }, // bool + { + 'key': 'test_write_setting', + 'map': {"test_write_setting": 0}, + 'write_json': '{"test_write_setting":null}', + 'set_value': null, + }, // null + ]; + group('Setting update', () { + test('write setting to file', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeJson = data['write_json'] as String; + String writeContent = ''; + final writeFile = FakeTextFile( + writeContent, + writeFuture: (value) async { + writeContent = value; + }, + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await Setting(key).update(setValue); + expect(writeJson, writeContent); + } + }); + test('update setting value', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.value(), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await Setting(key).update(setValue); + expect(AppSettings.getSetting(key), setValue); + } + }); + test('call onSuccess if setting saved successfully', () async { + for (final data in testsData) { + bool isSuccess = false; + bool isError = false; + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.value(), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await Setting(key).update( + setValue, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isTrue); + expect(isError, isFalse); + } + }); + test('call onError if setting saved with error', () async { + for (final data in testsData) { + bool isSuccess = false; + bool isError = false; + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await Setting(key).update( + setValue, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + } + }); + test('call onError if setting is not found', () async { + bool isSuccess = false; + bool isError = false; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writeFile: writeFile, + ); + await const Setting('test_not_found_setting').update( + 10, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + }); + test('call onError if setting is not writable', () async { + bool isSuccess = false; + bool isError = false; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + readOnly: const FakeJsonMap(Ok({'test_not_write_setting': 0})), + writeFile: writeFile, + ); + await const Setting('test_not_write_setting').update( + 10, + onSuccess: () { + isSuccess = true; + }, + onError: (_) { + isError = true; + }, + ); + expect(isSuccess, isFalse); + expect(isError, isTrue); + }); + test('returns setting to previous value on error', () async { + for (final data in testsData) { + final key = data['key'] as String; + final map = data['map'] as Map; + final setValue = data['set_value'] as dynamic; + final writeFile = FakeTextFile( + '', + writeFuture: (_) => Future.error('write error'), + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + writeFile: writeFile, + ); + await AppSettings.setSetting(key, setValue); + expect(AppSettings.getSetting(key), map[key]); + } + }); + }); +} From 76d63e303ac6c507074f235618241772c361c6b8 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 18:35:00 +0300 Subject: [PATCH 33/44] pubspec | update package version to 2.1.6 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 685aca9..4c19029 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hmi_core description: A Flutter core package of hmi_widgets package which is helps to visualize states, values, changes and other artefacts from technological processes. -version: 2.1.5 +version: 2.1.6 homepage: https://github.com/a-givertzman/hmi_core environment: From 18613e880614347b8c52ec08efc2e200d74fcd68 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 19:42:25 +0300 Subject: [PATCH 34/44] AppSettings | rename file parameter --- lib/src/app_settings/app_settings.dart | 23 +++++++++++-------- .../app_settings_set_setting_test.dart | 14 +++++------ .../app_settings/setting_update_test.dart | 15 ++++++------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 6aaa219..8419296 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -13,7 +13,7 @@ import 'package:hmi_core/src/core/result/result_transform_extension.dart'; /// for different groups of settings. E.g. `ui-padding`, `ui-font-size`, /// `api-host`, `api-port`, etc. class AppSettings { - static const _log = Log('AppSettings'); + static const _log = Log('AppSettings '); static final _settings = { 'displaySizeWidth': 1024, 'displaySizeHeight': 768, @@ -28,7 +28,7 @@ class AppSettings { 'floatingActionIconSize': 45.0, }; static final _writable = {}; - static TextFile _writeFile = const TextFile.path('stored_settings.json'); + static TextFile _store = const TextFile.path('stored_settings.json'); /// /// Initializes app settings with [readOnly] and [writable] settings. /// @@ -36,17 +36,18 @@ class AppSettings { /// /// [writable] settings are accessible for reading and updating (writing). /// - /// [writeFile] is used to write and restore [writable] settings. + /// [store] file is used to write and restore [writable] settings. /// If not passed, file with path `stored_settings.json` will be used /// by default. static Future initialize({ JsonMap readOnly = const JsonMap.empty(), JsonMap writable = const JsonMap.empty(), - TextFile? writeFile, + TextFile? store, }) async { - if (writeFile != null) { - _writeFile = writeFile; + if (store != null) { + _store = store; } + _log.info('Initializing read-only app settings...'); await _addSettings( readOnly, onSettingAdded: (entry) { @@ -61,6 +62,7 @@ class AppSettings { _writable[entry.key] = false; }, ); + _log.info('Initializing writable app settings...'); await _addSettings( writable, onSettingAdded: (entry) { @@ -75,8 +77,9 @@ class AppSettings { _writable[entry.key] = true; }, ); + _log.info('Restoring writable app settings...'); await _addSettings( - JsonMap.fromTextFile(_writeFile), + JsonMap.fromTextFile(_store), onSettingAdded: (entry) { _log.info( 'Restoring writeable setting "${entry.key}": ${entry.value}...', @@ -151,7 +154,7 @@ class AppSettings { _settings[settingName] = value; final storedMap = await _getWritableSettings(); storedMap[settingName] = value; - await _writeFile.write(json.encode(storedMap)).then( + await _store.write(json.encode(storedMap)).then( (_) { onSuccess?.call(); }, @@ -171,10 +174,10 @@ class AppSettings { /// /// Returns map of writable settings. /// - /// Gets entries from [_writeFile] if it exists, + /// Gets entries from [_store] if it exists, /// otherwise gets writable entries from [_settings]. static Future> _getWritableSettings() async { - final mapResult = await JsonMap.fromTextFile(_writeFile).decoded; + final mapResult = await JsonMap.fromTextFile(_store).decoded; return mapResult.mapOrElse( (_) { return Map.fromEntries( diff --git a/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart b/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart index 6dc211d..d2a0dda 100644 --- a/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart +++ b/test/unit/core/entities/app_settings/app_settings_set_setting_test.dart @@ -74,7 +74,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting(key, setValue); expect(writeJson, writeContent); @@ -91,7 +91,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting(key, setValue); expect(AppSettings.getSetting(key), setValue); @@ -110,7 +110,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting( key, @@ -139,7 +139,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting( key, @@ -163,7 +163,7 @@ void main() { writeFuture: (_) => Future.error('write error'), ); await AppSettings.initialize( - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting( 'test_not_found_setting', @@ -187,7 +187,7 @@ void main() { ); await AppSettings.initialize( readOnly: const FakeJsonMap(Ok({"test_not_write_setting": 0})), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting( 'test_not_write_setting', @@ -213,7 +213,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting(key, setValue); expect(AppSettings.getSetting(key), map[key]); diff --git a/test/unit/core/entities/app_settings/setting_update_test.dart b/test/unit/core/entities/app_settings/setting_update_test.dart index 9398b06..7321d55 100644 --- a/test/unit/core/entities/app_settings/setting_update_test.dart +++ b/test/unit/core/entities/app_settings/setting_update_test.dart @@ -9,7 +9,6 @@ import 'fake_text_file.dart'; /// Fake implementation of [JsonMap] for tests class FakeJsonMap implements JsonMap { final ResultF> _map; - /// /// Creates [FakeJsonMap] with a given [map]. const FakeJsonMap(ResultF> map) : _map = map; @@ -76,7 +75,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await Setting(key).update(setValue); expect(writeJson, writeContent); @@ -93,7 +92,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await Setting(key).update(setValue); expect(AppSettings.getSetting(key), setValue); @@ -112,7 +111,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await Setting(key).update( setValue, @@ -140,7 +139,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await Setting(key).update( setValue, @@ -163,7 +162,7 @@ void main() { writeFuture: (_) => Future.error('write error'), ); await AppSettings.initialize( - writeFile: writeFile, + store: writeFile, ); await const Setting('test_not_found_setting').update( 10, @@ -186,7 +185,7 @@ void main() { ); await AppSettings.initialize( readOnly: const FakeJsonMap(Ok({'test_not_write_setting': 0})), - writeFile: writeFile, + store: writeFile, ); await const Setting('test_not_write_setting').update( 10, @@ -211,7 +210,7 @@ void main() { ); await AppSettings.initialize( writable: FakeJsonMap(Ok(map)), - writeFile: writeFile, + store: writeFile, ); await AppSettings.setSetting(key, setValue); expect(AppSettings.getSetting(key), map[key]); From c13800b64604e0505f13e9386e14b1b6a8cbd3c5 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Mon, 21 Apr 2025 20:38:32 +0300 Subject: [PATCH 35/44] AppSettings | fix that non-writable settings initialized from store file --- lib/src/app_settings/app_settings.dart | 131 ++++++++++++------------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 8419296..bdd72f4 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -27,7 +27,7 @@ class AppSettings { 'floatingActionButtonSize': 60.0, 'floatingActionIconSize': 45.0, }; - static final _writable = {}; + static final _isWritable = {}; static TextFile _store = const TextFile.path('stored_settings.json'); /// /// Initializes app settings with [readOnly] and [writable] settings. @@ -48,66 +48,62 @@ class AppSettings { _store = store; } _log.info('Initializing read-only app settings...'); - await _addSettings( + await _parseSettings( readOnly, - onSettingAdded: (entry) { - _log.info( - 'Added read-only setting "${entry.key}": ${entry.value}...', - ); + onSettingParsed: (entry) { if (_settings.containsKey(entry.key)) { - _log.warning( - 'Setting with key "${entry.key}" was overwritten.', - ); + _log.warning('Setting with key "${entry.key}" will be overwritten.'); } - _writable[entry.key] = false; + _settings[entry.key] = entry.value; + _isWritable[entry.key] = false; + _log.info('Added read-only setting "${entry.key}": ${entry.value}...'); }, ); _log.info('Initializing writable app settings...'); - await _addSettings( + await _parseSettings( writable, - onSettingAdded: (entry) { - _log.info( - 'Added writeable setting "${entry.key}": ${entry.value}...', - ); + onSettingParsed: (entry) { if (_settings.containsKey(entry.key)) { _log.warning( 'Setting with key "${entry.key}" was overwritten.', ); } - _writable[entry.key] = true; + _settings[entry.key] = entry.value; + _isWritable[entry.key] = true; + _log.info('Added writeable setting "${entry.key}": ${entry.value}...'); }, ); _log.info('Restoring writable app settings...'); - await _addSettings( + await _parseSettings( JsonMap.fromTextFile(_store), - onSettingAdded: (entry) { - _log.info( - 'Restoring writeable setting "${entry.key}": ${entry.value}...', - ); - if (!_settings.containsKey(entry.key)) { - _settings.remove(entry.key); - _log.warning( - 'Setting with key "${entry.key}" does not exist and was ignored.', - ); + onSettingParsed: (entry) { + if (!_canWriteSetting(entry.key)) { + _log.warning('Writeable setting with key "${entry.key}" does not exist and was ignored.'); + } else { + _settings[entry.key] = entry.value; + _log.info('Restored writeable setting "${entry.key}": ${entry.value}...'); } }, ); } // - static Future _addSettings( + static Future _parseSettings( JsonMap settings, { - void Function(MapEntry)? onSettingAdded, + void Function(MapEntry)? onSettingParsed, }) async { final decodedResult = await settings.decoded; decodedResult.inspect((map) { for (final entry in map.entries) { - _settings[entry.key] = entry.value; - onSettingAdded?.call(entry); + onSettingParsed?.call(entry); } }).inspectErr((error) { _log.warning('Failed to initialize app settings, ${error.message}.'); }); } + // + static bool _canWriteSetting(String key) { + return _settings.containsKey(key) && _isWritable.containsKey(key) && (_isWritable[key] ?? false); + } /// /// Returns value of app setting by [key]. /// @@ -130,6 +126,26 @@ class AppSettings { return _settings[key]; } /// + /// Returns map of writable settings. + /// + /// Gets entries from [_store] if it exists, + /// otherwise gets writable entries from [_settings]. + static Future> _getWritableSettings() async { + final mapResult = await JsonMap.fromTextFile(_store).decoded; + return mapResult.mapOrElse( + (_) { + return Map.fromEntries( + _settings.entries.where( + (entry) => _isWritable.containsKey(entry.key), + ), + ); + }, + (map) { + return map; + }, + ); + } + /// /// Immediately updates app setting with key [settingName] to new [value] /// and save it asynchronously to file. /// @@ -141,9 +157,7 @@ class AppSettings { void Function(Failure error)? onError, void Function()? onSuccess, }) async { - if (!_settings.containsKey(settingName) || - !_writable.containsKey(settingName) || - !_writable[settingName]!) { + if (!_canWriteSetting(settingName)) { final failure = Failure( message: 'Cannot update setting with key "$settingName".', stackTrace: StackTrace.current, @@ -154,41 +168,22 @@ class AppSettings { _settings[settingName] = value; final storedMap = await _getWritableSettings(); storedMap[settingName] = value; - await _store.write(json.encode(storedMap)).then( - (_) { - onSuccess?.call(); - }, - ).catchError( - (error, stackTrace) { - final failure = Failure( - message: 'Failed to save setting "$settingName", $error.', - stackTrace: stackTrace, - ); - _log.warning(failure.message); - _settings[settingName] = valueBackup; - onError?.call(failure); - }, - ); - } - } - /// - /// Returns map of writable settings. - /// - /// Gets entries from [_store] if it exists, - /// otherwise gets writable entries from [_settings]. - static Future> _getWritableSettings() async { - final mapResult = await JsonMap.fromTextFile(_store).decoded; - return mapResult.mapOrElse( - (_) { - return Map.fromEntries( - _settings.entries.where( - (entry) => _writable.containsKey(entry.key), - ), + await _store + .write(json.encode(storedMap)).then( + (_) { + onSuccess?.call(); + }, + ).catchError( + (error, stackTrace) { + final failure = Failure( + message: 'Failed to save setting "$settingName", $error.', + stackTrace: stackTrace, + ); + _log.warning(failure.message); + _settings[settingName] = valueBackup; + onError?.call(failure); + }, ); - }, - (map) { - return map; - }, - ); + } } } From 630b0c53f4f1ce94c9eace68f7ce9ab882d5ab91 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Tue, 22 Apr 2025 01:02:52 +0300 Subject: [PATCH 36/44] AppSettings | fix logging messages --- lib/src/app_settings/app_settings.dart | 68 +++++++++++++------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index bdd72f4..6c14395 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -56,7 +56,7 @@ class AppSettings { } _settings[entry.key] = entry.value; _isWritable[entry.key] = false; - _log.info('Added read-only setting "${entry.key}": ${entry.value}...'); + _log.info('Added read-only setting "${entry.key}": ${entry.value}.'); }, ); _log.info('Initializing writable app settings...'); @@ -64,13 +64,11 @@ class AppSettings { writable, onSettingParsed: (entry) { if (_settings.containsKey(entry.key)) { - _log.warning( - 'Setting with key "${entry.key}" was overwritten.', - ); + _log.warning('Setting with key "${entry.key}" will be overwritten.'); } _settings[entry.key] = entry.value; _isWritable[entry.key] = true; - _log.info('Added writeable setting "${entry.key}": ${entry.value}...'); + _log.info('Added writeable setting "${entry.key}": ${entry.value}.'); }, ); _log.info('Restoring writable app settings...'); @@ -81,7 +79,7 @@ class AppSettings { _log.warning('Writeable setting with key "${entry.key}" does not exist and was ignored.'); } else { _settings[entry.key] = entry.value; - _log.info('Restored writeable setting "${entry.key}": ${entry.value}...'); + _log.info('Restored writeable setting "${entry.key}": ${entry.value}.'); } }, ); @@ -92,13 +90,14 @@ class AppSettings { void Function(MapEntry)? onSettingParsed, }) async { final decodedResult = await settings.decoded; - decodedResult.inspect((map) { - for (final entry in map.entries) { - onSettingParsed?.call(entry); - } - }).inspectErr((error) { - _log.warning('Failed to initialize app settings, ${error.message}.'); - }); + decodedResult + .inspect((map) { + for (final entry in map.entries) { + onSettingParsed?.call(entry); + } + }).inspectErr((error) { + _log.warning('Failed to initialize app settings, ${error.message}.'); + }); } // static bool _canWriteSetting(String key) { @@ -119,33 +118,12 @@ class AppSettings { ); if (onError != null) { return onError(err); - } else { - throw err; } + throw err; } return _settings[key]; } /// - /// Returns map of writable settings. - /// - /// Gets entries from [_store] if it exists, - /// otherwise gets writable entries from [_settings]. - static Future> _getWritableSettings() async { - final mapResult = await JsonMap.fromTextFile(_store).decoded; - return mapResult.mapOrElse( - (_) { - return Map.fromEntries( - _settings.entries.where( - (entry) => _isWritable.containsKey(entry.key), - ), - ); - }, - (map) { - return map; - }, - ); - } - /// /// Immediately updates app setting with key [settingName] to new [value] /// and save it asynchronously to file. /// @@ -186,4 +164,24 @@ class AppSettings { ); } } + /// + /// Returns map of writable settings. + /// + /// Gets entries from [_store] if it exists, + /// otherwise gets writable entries from [_settings]. + static Future> _getWritableSettings() async { + final decodedResult = await JsonMap.fromTextFile(_store).decoded; + return decodedResult.mapOrElse( + (_) { + return Map.fromEntries( + _settings.entries.where( + (entry) => _canWriteSetting(entry.key), + ), + ); + }, + (map) { + return map; + }, + ); + } } From c6292a433afd74bd113d8a668df81d1df1e48a82 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Tue, 22 Apr 2025 16:40:52 +0300 Subject: [PATCH 37/44] AppSettings, Setting | add tests for cases with writable settings --- ...app_settings_initialize_writable_test.dart | 234 ++++++++++++++++++ .../app_settings/setting_writable_test.dart | 202 +++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 test/unit/core/entities/app_settings/app_settings_initialize_writable_test.dart create mode 100644 test/unit/core/entities/app_settings/setting_writable_test.dart diff --git a/test/unit/core/entities/app_settings/app_settings_initialize_writable_test.dart b/test/unit/core/entities/app_settings/app_settings_initialize_writable_test.dart new file mode 100644 index 0000000..07b5574 --- /dev/null +++ b/test/unit/core/entities/app_settings/app_settings_initialize_writable_test.dart @@ -0,0 +1,234 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/hmi_core_app_settings.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +import 'package:hmi_core/src/core/error/failure.dart'; +import 'fake_text_file.dart'; +/// +/// Fake implementation of [JsonMap] for tests +class FakeJsonMap implements JsonMap { + final ResultF> _map; + /// + /// Creates [FakeJsonMap] with a given [map]. + const FakeJsonMap(ResultF> map) : _map = map; + // + @override + Future>> get decoded async { + return _map; + } +} +// +void main() { + Log.initialize(); + final validReadOnlyMaps = [ + { + 'test_setting_1': 0, + 'test_setting_2': 27, + 'test_setting_3': -123, + }, // int + { + 'test_setting_1': 0.0, + 'test_setting_2': 27.0, + 'test_setting_3': -123.0, + }, // double + { + 'test_setting_1': '0', + 'test_setting_2': '27', + 'test_setting_3': '-123', + }, // string + { + 'test_setting_1': true, + 'test_setting_2': false, + 'test_setting_3': true, + }, // bool + ]; + final validWritableMaps = [ + { + 'test_writable_setting_1': 0, + 'test_writable_setting_2': 27, + 'test_writable_setting_3': -123, + }, // int + { + 'test_writable_setting_1': 0.0, + 'test_writable_setting_2': 27.0, + 'test_writable_setting_3': -123.0, + }, // double + { + 'test_writable_setting_1': '0', + 'test_writable_setting_2': '27', + 'test_writable_setting_3': '-123', + }, // string + { + 'test_writable_setting_1': true, + 'test_writable_setting_2': false, + 'test_writable_setting_3': true, + }, // bool + ]; + group('AppSettings with writable initialize', () { + test('sets data normally with valid writable json map', () async { + for (final map in validWritableMaps) { + await AppSettings.initialize(writable: FakeJsonMap(Ok(map))); + expect( + AppSettings.getSetting('test_writable_setting_1', onError: ((err) => null)), + map['test_writable_setting_1'], + ); + expect( + AppSettings.getSetting('test_writable_setting_2', onError: ((err) => null)), + map['test_writable_setting_2'], + ); + expect( + AppSettings.getSetting('test_writable_setting_3', onError: ((err) => null)), + map['test_writable_setting_3'], + ); + } + }); + test('sets data normally with valid readOnly and writable json maps', () async { + // + for (final readOnly in validReadOnlyMaps) { + for (final writable in validWritableMaps) { + await AppSettings.initialize( + readOnly: FakeJsonMap(Ok(readOnly)), + writable: FakeJsonMap(Ok(writable)), + ); + expect( + AppSettings.getSetting('test_setting_1', onError: ((err) => null)), + readOnly['test_setting_1'], + ); + expect( + AppSettings.getSetting('test_setting_2', onError: ((err) => null)), + readOnly['test_setting_2'], + ); + expect( + AppSettings.getSetting('test_setting_3', onError: ((err) => null)), + readOnly['test_setting_3'], + ); + expect( + AppSettings.getSetting('test_writable_setting_1', onError: ((err) => null)), + writable['test_writable_setting_1'], + ); + expect( + AppSettings.getSetting('test_writable_setting_2', onError: ((err) => null)), + writable['test_writable_setting_2'], + ); + expect( + AppSettings.getSetting('test_writable_setting_3', onError: ((err) => null)), + writable['test_writable_setting_3'], + ); + } + } + }); + test('writable settings overwrite readOnly settings', () async { + await AppSettings.initialize( + readOnly: const FakeJsonMap(Ok({ + 'test_overwrite_setting_1': 0, + 'test_overwrite_setting_2': 27, + 'test_overwrite_setting_3': -123, + })), + writable: const FakeJsonMap(Ok({ + 'test_overwrite_setting_1': -123, + 'test_overwrite_setting_2': 0, + 'test_overwrite_setting_3': 27, + })), + ); + expect(AppSettings.getSetting('test_overwrite_setting_1'), -123); + expect(AppSettings.getSetting('test_overwrite_setting_2'), 0); + expect(AppSettings.getSetting('test_overwrite_setting_3'), 27); + }); + test('restore writable setting from store file', () async { + for (final map in validWritableMaps) { + final storeFile = FakeTextFile( + '{"test_writable_setting_1":"rewritten","test_writable_setting_2":"rewritten","test_writable_setting_3":"rewritten"}', + ); + await AppSettings.initialize( + writable: FakeJsonMap(Ok(map)), + store: storeFile, + ); + expect(AppSettings.getSetting('test_writable_setting_1'), "rewritten"); + expect(AppSettings.getSetting('test_writable_setting_2'), "rewritten"); + expect(AppSettings.getSetting('test_writable_setting_3'), "rewritten"); + } + }); + test('ignore read only settings in store file', () async { + for (final map in validReadOnlyMaps) { + final storeFile = FakeTextFile( + '{"test_setting_1":"rewritten","test_setting_2":"rewritten","test_setting_3":"rewritten"}', + ); + await AppSettings.initialize( + readOnly: FakeJsonMap(Ok(map)), + store: storeFile, + ); + expect(AppSettings.getSetting('test_setting_1'), map['test_setting_1']); + expect(AppSettings.getSetting('test_setting_2'), map['test_setting_2']); + expect(AppSettings.getSetting('test_setting_3'), map['test_setting_3']); + } + }); + test('ignore not found settings in store file', () async { + final storeFile = FakeTextFile( + '{"test_not_found_setting_1":"rewritten","test_not_found_setting_2":"rewritten","test_not_found_setting_3":"rewritten"}', + ); + await AppSettings.initialize(store: storeFile); + expect( + AppSettings.getSetting('test_not_found_setting_1', onError: ((err) => null)), + null, + ); + expect( + AppSettings.getSetting('test_not_found_setting_2', onError: ((err) => null)), + null, + ); + expect( + AppSettings.getSetting('test_not_found_setting_3', onError: ((err) => null)), + null, + ); + }); + test('getSetting onError passes new value case error', () async { + final notFoundMaps = [ + { + 'key': 'not_found_1', + 'map_value': 0, + }, // int + { + 'key': 'not_found_2', + 'map_value': 0.0, + }, // int + { + 'key': 'not_found_3', + 'map_value': false, + }, // bool + { + 'key': 'not_found_4', + 'map_value': '0', + }, // string + { + 'key': 'not_found_5', + 'map_value': null, + }, // null + ]; + for (final map in notFoundMaps) { + await AppSettings.initialize(); + final key = map['key'] as String; + final mapValue = map['map_value'] as dynamic; + expect( + AppSettings.getSetting(key, onError: ((err) => mapValue)), + mapValue, + ); + } + }); + test('does not throw error with invalid json map', () async { + final mapWithError = FakeJsonMap( + Err(Failure( + message: 'invalid json map', + stackTrace: StackTrace.current, + )), + ); + await expectLater( + AppSettings.initialize(writable: mapWithError), + completes, + ); + await expectLater( + AppSettings.initialize(readOnly: mapWithError, writable: mapWithError), + completes, + ); + }); + }); +} diff --git a/test/unit/core/entities/app_settings/setting_writable_test.dart b/test/unit/core/entities/app_settings/setting_writable_test.dart new file mode 100644 index 0000000..76ecb7c --- /dev/null +++ b/test/unit/core/entities/app_settings/setting_writable_test.dart @@ -0,0 +1,202 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmi_core/hmi_core_app_settings.dart'; +import 'package:hmi_core/src/core/error/failure.dart'; +import 'package:hmi_core/src/core/json/json_map.dart'; +import 'package:hmi_core/src/core/log/log.dart'; +import 'package:hmi_core/src/core/result/result.dart'; +/// +/// Fake implementation of [JsonMap] for tests +class FakeJsonMap implements JsonMap { + final ResultF> _map; + /// + /// Creates [FakeJsonMap] with a given [map]. + const FakeJsonMap(ResultF> map) : _map = map; + // + @override + Future>> get decoded async { + return _map; + } +} +// +void main() { + final List> validReadOnlyMaps = [ + { + 'test_setting_1': 0, + 'test_setting_2': 27, + 'test_setting_3': -123, + }, // int + { + 'test_setting_1': 0.0, + 'test_setting_2': 27.0, + 'test_setting_3': -123.0, + }, // double + ]; + final List> validWritableMaps = [ + { + 'test_writable_setting_1': 0, + 'test_writable_setting_2': 27, + 'test_writable_setting_3': -123, + }, // int + { + 'test_writable_setting_1': 0.0, + 'test_writable_setting_2': 27.0, + 'test_writable_setting_3': -123.0, + }, // double + ]; + group('Setting with writable', () { + Log.initialize(); + const log = Log('Setting '); + test('valid writable data', () async { + for (final writable in validWritableMaps) { + await AppSettings.initialize(writable: FakeJsonMap(Ok(writable))); + for (final setting in writable.entries) { + final int testSetting = Setting(setting.key).toInt; + log.debug('as Int | ${setting.key}: $testSetting'); + } + for (final setting in writable.entries) { + final double testSetting = Setting(setting.key).toDouble; + log.debug('as Double | ${setting.key}: $testSetting'); + } + for (final setting in writable.entries) { + final String testSetting = Setting(setting.key).toString(); + log.debug('as String | ${setting.key}: $testSetting'); + } + expect(const Setting('test_writable_setting_1').toInt, writable['test_writable_setting_1']!.toInt()); + expect(const Setting('test_writable_setting_2').toInt, writable['test_writable_setting_2']!.toInt()); + expect(const Setting('test_writable_setting_3').toInt, writable['test_writable_setting_3']!.toInt()); + expect(const Setting('test_writable_setting_1').toDouble, writable['test_writable_setting_1']!.toDouble()); + expect(const Setting('test_writable_setting_2').toDouble, writable['test_writable_setting_2']!.toDouble()); + expect(const Setting('test_writable_setting_3').toDouble, writable['test_writable_setting_3']!.toDouble()); + expect(const Setting('test_writable_setting_1').toString(), writable['test_writable_setting_1']!.toString()); + expect(const Setting('test_writable_setting_2').toString(), writable['test_writable_setting_2']!.toString()); + expect(const Setting('test_writable_setting_3').toString(), writable['test_writable_setting_3']!.toString()); + } + }); + test('valid read only and writable data', () async { + for (final readOnly in validReadOnlyMaps) { + for (final writable in validWritableMaps) { + await AppSettings.initialize( + readOnly: FakeJsonMap(Ok(readOnly)), + writable: FakeJsonMap(Ok(writable)), + ); + for (final setting in readOnly.entries) { + final int testSetting = Setting(setting.key).toInt; + log.debug('as Int | ${setting.key}: $testSetting'); + } + for (final setting in readOnly.entries) { + final double testSetting = Setting(setting.key).toDouble; + log.debug('as Double | ${setting.key}: $testSetting'); + } + for (final setting in readOnly.entries) { + final String testSetting = Setting(setting.key).toString(); + log.debug('as String | ${setting.key}: $testSetting'); + } + expect(const Setting('test_setting_1').toInt, readOnly['test_setting_1']!.toInt()); + expect(const Setting('test_setting_2').toInt, readOnly['test_setting_2']!.toInt()); + expect(const Setting('test_setting_3').toInt, readOnly['test_setting_3']!.toInt()); + expect(const Setting('test_setting_1').toDouble, readOnly['test_setting_1']!.toDouble()); + expect(const Setting('test_setting_2').toDouble, readOnly['test_setting_2']!.toDouble()); + expect(const Setting('test_setting_3').toDouble, readOnly['test_setting_3']!.toDouble()); + expect(const Setting('test_setting_1').toString(), readOnly['test_setting_1']!.toString()); + expect(const Setting('test_setting_2').toString(), readOnly['test_setting_2']!.toString()); + expect(const Setting('test_setting_3').toString(), readOnly['test_setting_3']!.toString()); + for (final setting in writable.entries) { + final int testSetting = Setting(setting.key).toInt; + log.debug('as Int | ${setting.key}: $testSetting'); + } + for (final setting in writable.entries) { + final double testSetting = Setting(setting.key).toDouble; + log.debug('as Double | ${setting.key}: $testSetting'); + } + for (final setting in writable.entries) { + final String testSetting = Setting(setting.key).toString(); + log.debug('as String | ${setting.key}: $testSetting'); + } + expect(const Setting('test_writable_setting_1').toInt, writable['test_writable_setting_1']!.toInt()); + expect(const Setting('test_writable_setting_2').toInt, writable['test_writable_setting_2']!.toInt()); + expect(const Setting('test_writable_setting_3').toInt, writable['test_writable_setting_3']!.toInt()); + expect(const Setting('test_writable_setting_1').toDouble, writable['test_writable_setting_1']!.toDouble()); + expect(const Setting('test_writable_setting_2').toDouble, writable['test_writable_setting_2']!.toDouble()); + expect(const Setting('test_writable_setting_3').toDouble, writable['test_writable_setting_3']!.toDouble()); + expect(const Setting('test_writable_setting_1').toString(), writable['test_writable_setting_1']!.toString()); + expect(const Setting('test_writable_setting_2').toString(), writable['test_writable_setting_2']!.toString()); + expect(const Setting('test_writable_setting_3').toString(), writable['test_writable_setting_3']!.toString()); + } + } + }); + test('writable key not found', () async { + for (final settings in validWritableMaps) { + await AppSettings.initialize(writable: FakeJsonMap(Ok(settings))); + for (final setting in settings.entries) { + final int testSetting = Setting(setting.key).toInt; + log.debug('as Int | ${setting.key}: $testSetting'); + } + for (final setting in settings.entries) { + final double testSetting = Setting(setting.key).toDouble; + log.debug('as Double | ${setting.key}: $testSetting'); + } + for (final setting in settings.entries) { + final String testSetting = Setting(setting.key).toString(); + log.debug('as String | ${setting.key}: $testSetting'); + } + expect(Setting('test_writable_setting_1111', onError: (err) => 111).toInt, 111); + expect(Setting('test_writable_setting_1222', onError: (err) => -222).toInt, -222); + expect(Setting('test_writable_setting_1.10', onError: (err) => 1.10).toDouble, 1.10); + expect(Setting('test_writable_setting_2.22', onError: (err) => 2.22).toDouble, 2.22); + expect(Setting('test_writable_setting_1234', onError: (err) => '1234').toString(), '1234'); + expect(Setting('test_writable_setting_2345', onError: (err) => '2345').toString(), '2345'); + } + }); + test('valid writable data with factor', () async { + const factors = [0.123, -0.123, 3.54, -5.12]; + for (final factor in factors) { + for (final settings in validWritableMaps) { + await AppSettings.initialize(writable: FakeJsonMap(Ok(settings))); + for (final setting in settings.entries) { + final int testSetting = Setting(setting.key, factor: factor).toInt; + log.debug('as Int | ${setting.key} * $factor: $testSetting'); + } + for (final setting in settings.entries) { + final double testSetting = Setting(setting.key, factor: factor).toDouble; + log.debug('as Double | ${setting.key} * $factor: $testSetting'); + } + const factorConst = 0.123; + const setting = Setting('test_writable_setting_1', factor: factorConst); + expect(setting.toInt, (settings['test_writable_setting_1']! * factor).toInt()); + expect(Setting('test_writable_setting_1', factor: factor).toInt, (settings['test_writable_setting_1']! * factor).toInt()); + expect(Setting('test_writable_setting_2', factor: factor).toInt, (settings['test_writable_setting_2']! * factor).toInt()); + expect(Setting('test_writable_setting_3', factor: factor).toInt, (settings['test_writable_setting_3']! * factor).toInt()); + expect(Setting('test_writable_setting_1', factor: factor).toDouble, (settings['test_writable_setting_1']! * factor).toDouble()); + expect(Setting('test_writable_setting_2', factor: factor).toDouble, (settings['test_writable_setting_2']! * factor).toDouble()); + expect(Setting('test_writable_setting_3', factor: factor).toDouble, (settings['test_writable_setting_3']! * factor).toDouble()); + } + } + }); + test('does not throw with invalid map', () { + final invalidJsonMaps = [ + FakeJsonMap(Err(Failure( + message: 'error', + stackTrace: StackTrace.current, + ))), + FakeJsonMap(Err(Failure( + message: null, + stackTrace: StackTrace.current, + ))), + FakeJsonMap(Err(Failure( + message: '{"validJson":true}', + stackTrace: StackTrace.current, + ))), + ]; + for (final invalidJsonMap in invalidJsonMaps) { + expectLater( + AppSettings.initialize(writable: invalidJsonMap), + completes, + ); + expectLater( + AppSettings.initialize(readOnly: invalidJsonMap, writable: invalidJsonMap), + completes, + ); + } + }); + }); +} From 7e6b15494d267e9dd3e4e0821e195b7cd58f6c42 Mon Sep 17 00:00:00 2001 From: nyaneet Date: Tue, 22 Apr 2025 16:50:21 +0300 Subject: [PATCH 38/44] AppSettings | refactor logging, naming, and remove backup on update --- lib/src/app_settings/app_settings.dart | 20 +++++++++----------- lib/src/app_settings/setting.dart | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 6c14395..25cb54f 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -124,41 +124,39 @@ class AppSettings { return _settings[key]; } /// - /// Immediately updates app setting with key [settingName] to new [value] + /// Updates app setting with key [key] to new [value] /// and save it asynchronously to file. /// - /// Calls [onSuccess] or [onError] when setting will be saved successfully - /// or with error. In case of error returns app setting to its previous value. + /// Calls [onSuccess] or [onError] when setting will be updated + /// and saved successfully or with error. static Future setSetting( - String settingName, + String key, dynamic value, { void Function(Failure error)? onError, void Function()? onSuccess, }) async { - if (!_canWriteSetting(settingName)) { + if (!_canWriteSetting(key)) { final failure = Failure( - message: 'Cannot update setting with key "$settingName".', + message: 'Setting with key "$key" - is not writable, can\'t be updated', stackTrace: StackTrace.current, ); onError?.call(failure); } else { - final valueBackup = _settings[settingName]; - _settings[settingName] = value; final storedMap = await _getWritableSettings(); - storedMap[settingName] = value; + storedMap[key] = value; await _store .write(json.encode(storedMap)).then( (_) { + _settings[key] = value; onSuccess?.call(); }, ).catchError( (error, stackTrace) { final failure = Failure( - message: 'Failed to save setting "$settingName", $error.', + message: 'Failed to save setting "$key", $error.', stackTrace: stackTrace, ); _log.warning(failure.message); - _settings[settingName] = valueBackup; onError?.call(failure); }, ); diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index 13db9c6..9019372 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -44,10 +44,10 @@ class Setting { return double.parse('$value') * _factor; } /// - /// Immediately updates app setting to new [value] and save it asynchronously. + /// Updates app setting to new [value] and save it asynchronously to file. /// - /// Calls [onSuccess] or [onError] when setting will be saved successfully - /// or with error. In case of error returns app setting to its previous value. + /// Calls [onSuccess] or [onError] when setting will be updated + /// and saved successfully or with error. Future update( dynamic value, { void Function(Failure error)? onError, From 886b28a6fd88eea7ed20c71b32b27a471ec5c6cb Mon Sep 17 00:00:00 2001 From: nyaneet Date: Tue, 22 Apr 2025 17:38:31 +0300 Subject: [PATCH 39/44] AppSettings | make sure that saved settings are always taken from runtime values --- lib/src/app_settings/app_settings.dart | 30 ++++++++------------------ 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index 25cb54f..b32d80f 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -142,10 +142,10 @@ class AppSettings { ); onError?.call(failure); } else { - final storedMap = await _getWritableSettings(); - storedMap[key] = value; + final writableSettings = _getWritableSettings(); + writableSettings[key] = value; await _store - .write(json.encode(storedMap)).then( + .write(json.encode(writableSettings)).then( (_) { _settings[key] = value; onSuccess?.call(); @@ -162,24 +162,12 @@ class AppSettings { ); } } - /// - /// Returns map of writable settings. - /// - /// Gets entries from [_store] if it exists, - /// otherwise gets writable entries from [_settings]. - static Future> _getWritableSettings() async { - final decodedResult = await JsonMap.fromTextFile(_store).decoded; - return decodedResult.mapOrElse( - (_) { - return Map.fromEntries( - _settings.entries.where( - (entry) => _canWriteSetting(entry.key), - ), - ); - }, - (map) { - return map; - }, + // + static Map _getWritableSettings() { + return Map.fromEntries( + _settings.entries.where( + (entry) => _canWriteSetting(entry.key), + ), ); } } From 517b5d56853e5b0f28a4faa846f570bb311f8f22 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 25 Jun 2025 16:35:57 +0300 Subject: [PATCH 40/44] Failure | child --- lib/src/app_settings/app_settings.dart | 17 +--- lib/src/core/entities/ds_data_class.dart | 5 +- lib/src/core/entities/ds_data_point.dart | 6 +- lib/src/core/entities/ds_data_type.dart | 5 +- lib/src/core/entities/ds_point_name.dart | 6 +- lib/src/core/entities/ds_status.dart | 12 +-- lib/src/core/entities/ds_timestamp.dart | 5 +- lib/src/core/error/failure.dart | 83 ++++++------------- lib/src/core/json/json_list.dart | 7 +- lib/src/core/json/json_map.dart | 7 +- .../core/result/result_extract_extension.dart | 12 +-- lib/src/core/text_file.dart | 10 +-- .../core/error/failure_conctructor_test.dart | 61 +------------- .../core/error/failure_to_string_test.dart | 5 +- .../core/result/result_constructor_test.dart | 2 +- .../result_old/result_constructor_test.dart | 2 +- .../core/result_old/result_data_test.dart | 2 +- .../core/result_old/result_error_test.dart | 2 +- .../core/result_old/result_fold_test.dart | 4 +- .../core/result_old/result_has_data_test.dart | 2 +- .../result_old/result_has_error_test.dart | 2 +- 21 files changed, 62 insertions(+), 195 deletions(-) diff --git a/lib/src/app_settings/app_settings.dart b/lib/src/app_settings/app_settings.dart index b32d80f..8de4ad5 100644 --- a/lib/src/app_settings/app_settings.dart +++ b/lib/src/app_settings/app_settings.dart @@ -112,10 +112,7 @@ class AppSettings { dynamic Function(Failure err)? onError, }) { if (!_settings.containsKey(key)) { - final err = Failure( - message: '$AppSettings.getSetting | Not found key "$key"', - stackTrace: StackTrace.current, - ); + final err = Failure('$AppSettings.getSetting | Not found key "$key"'); if (onError != null) { return onError(err); } @@ -136,10 +133,7 @@ class AppSettings { void Function()? onSuccess, }) async { if (!_canWriteSetting(key)) { - final failure = Failure( - message: 'Setting with key "$key" - is not writable, can\'t be updated', - stackTrace: StackTrace.current, - ); + final failure = Failure('AppSettings.setSetting | Setting "$key" - is not writable, can\'t be updated'); onError?.call(failure); } else { final writableSettings = _getWritableSettings(); @@ -152,11 +146,8 @@ class AppSettings { }, ).catchError( (error, stackTrace) { - final failure = Failure( - message: 'Failed to save setting "$key", $error.', - stackTrace: stackTrace, - ); - _log.warning(failure.message); + final failure = Failure('AppSettings.setSetting | Failed to save setting "$key", $error.'); + _log.warning(failure); onError?.call(failure); }, ); diff --git a/lib/src/core/entities/ds_data_class.dart b/lib/src/core/entities/ds_data_class.dart index 844f8ba..11917c8 100644 --- a/lib/src/core/entities/ds_data_class.dart +++ b/lib/src/core/entities/ds_data_class.dart @@ -33,9 +33,8 @@ enum DsDataClass { factory DsDataClass.fromString(String value) { final dataClass = _valueMapping[value]; if (dataClass == null) { - throw Failure.connection( - message: 'Ошибка в методе $DsDataClass.fromString: неизвестный класс комманды "$value"', - stackTrace: StackTrace.current, + throw Failure( + 'Ошибка в методе $DsDataClass.fromString: неизвестный класс комманды "$value"', ); } return dataClass; diff --git a/lib/src/core/entities/ds_data_point.dart b/lib/src/core/entities/ds_data_point.dart index 57b33c6..748facf 100644 --- a/lib/src/core/entities/ds_data_point.dart +++ b/lib/src/core/entities/ds_data_point.dart @@ -76,11 +76,7 @@ class DsDataPoint implements IDataPoint { DsCot.reqCon || DsCot.actCon => Ok(this), DsCot.reqErr || DsCot.actErr => Err( - Failure( - message: value, - stackTrace: - StackTrace.current, - ), + Failure(value), ) }; } \ No newline at end of file diff --git a/lib/src/core/entities/ds_data_type.dart b/lib/src/core/entities/ds_data_type.dart index eed6604..e069208 100644 --- a/lib/src/core/entities/ds_data_type.dart +++ b/lib/src/core/entities/ds_data_type.dart @@ -41,9 +41,8 @@ enum DsDataType { final lvalue = value.toLowerCase(); final dataType = _valueMapping[lvalue]; if (dataType == null) { - throw Failure.connection( - message: 'Ошибка в методе $DsDataType._extract: неизвестный тип данных $value', - stackTrace: StackTrace.current, + throw Failure( + 'Ошибка в методе $DsDataType._extract: неизвестный тип данных $value', ); } return dataType; diff --git a/lib/src/core/entities/ds_point_name.dart b/lib/src/core/entities/ds_point_name.dart index 4ec824d..0bcc9e3 100644 --- a/lib/src/core/entities/ds_point_name.dart +++ b/lib/src/core/entities/ds_point_name.dart @@ -24,16 +24,14 @@ class DsPointName { } } throw Failure( - message: 'Ошибка в методе $DsPointName._findLastSeparatorIndex: неверный формат пути: "$source"', - stackTrace: StackTrace.current, + 'Ошибка в методе $DsPointName._findLastSeparatorIndex: неверный формат пути: "$source"', ); } /// static bool _validatePath(String source) { if (source[0] != '/') { throw Failure( - message: 'Ошибка в методе $DsPointName._validatePath: неверный формат пути: "$source"\n\tДолжен начинаться с "/"', - stackTrace: StackTrace.current, + 'Ошибка в методе $DsPointName._validatePath: неверный формат пути: "$source"\n\tДолжен начинаться с "/"', ); } return true; diff --git a/lib/src/core/entities/ds_status.dart b/lib/src/core/entities/ds_status.dart index 154b7ae..951305b 100644 --- a/lib/src/core/entities/ds_status.dart +++ b/lib/src/core/entities/ds_status.dart @@ -19,10 +19,8 @@ enum DsStatus { if (parsedValue != null) { return DsStatus.fromValue(parsedValue); } else { - throw Failure.connection( - message: - 'Ошибка в методе $DsStatus.fromString: int.parse.error значение: "$rawValue"', - stackTrace: StackTrace.current, + throw Failure( + 'Ошибка в методе $DsStatus.fromString: int.parse.error значение: "$rawValue"', ); } } @@ -30,10 +28,8 @@ enum DsStatus { factory DsStatus.fromValue(int code) { final status = _valueMapping[code]; if (status == null) { - throw Failure.connection( - message: - 'Ошибка в методе $DsStatus.fromValue: неизвестный статус "$code"', - stackTrace: StackTrace.current, + throw Failure( + 'Ошибка в методе $DsStatus.fromValue: неизвестный статус "$code"', ); } return status; diff --git a/lib/src/core/entities/ds_timestamp.dart b/lib/src/core/entities/ds_timestamp.dart index 232b18c..105f7d8 100644 --- a/lib/src/core/entities/ds_timestamp.dart +++ b/lib/src/core/entities/ds_timestamp.dart @@ -28,9 +28,8 @@ class DsTimeStamp { if (dateTime != null) { return DsTimeStamp(dateTime: dateTime); } else { - throw Failure.convertion( - message: 'Ошибка в методе $DsTimeStamp.parse: Недопустимый формат метки времени в:\n\t$value', - stackTrace: StackTrace.current, + throw Failure( + 'Ошибка в методе $DsTimeStamp.parse: Недопустимый формат метки времени в:\n\t$value', ); } } diff --git a/lib/src/core/error/failure.dart b/lib/src/core/error/failure.dart index 01109fe..3a1dee6 100644 --- a/lib/src/core/error/failure.dart +++ b/lib/src/core/error/failure.dart @@ -1,66 +1,33 @@ -import 'package:hmi_core/src/core/log/log.dart'; - -class Failure { - static const _log = Log('Failure'); - final T message; /// /// Ganeral Failures - Failure({ - required this.message, - required StackTrace stackTrace, - }) { - _log.warning(message, this, stackTrace); +/// - `Failure('Me.area | Error message')` - error happens locally +/// - `Failure.pass('Me.area |', error)` - error in some dependency returned from Me.area +/// - `Failure.pass('Me.area | message', error)` - error in some dependency returned from Me.area with additional message +class Failure { + // static const _log = Log('Failure'); + final T message; + final dynamic _child; + /// + /// Ganeral Failures + Failure(this.message) : _child = null; + /// + /// Ganeral Failures passing incoming error with message + Failure.pass(this.message, dynamic err) : _child = err; + /// + /// Converts all children errors into single string + String _join(int depth) { + if (_child != null) { + if (_child is Failure) { + final tab = List.filled(depth, '\t').join(); + return '$message \n$tab${_child?._join(depth + 1)}'; + } + return '$message \n\t$_child'; + } + return ''; } // - // dataSource failure - factory Failure.dataSource({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // dataObject failure - factory Failure.dataObject({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // dataCollection failure - factory Failure.dataCollection({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // auth failure - factory Failure.auth({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // convertion failure - factory Failure.convertion({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // Connection failure - factory Failure.connection({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // Translation failure - factory Failure.translation({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - // - // Unexpected failure - factory Failure.unexpected({ - required T message, - required StackTrace stackTrace, - }) => Failure(message: message, stackTrace: stackTrace); - @override String toString() { - return message.toString(); + return '$message${_join(0)}'; } } diff --git a/lib/src/core/json/json_list.dart b/lib/src/core/json/json_list.dart index c7f758a..a23de82 100644 --- a/lib/src/core/json/json_list.dart +++ b/lib/src/core/json/json_list.dart @@ -55,11 +55,8 @@ class JsonList { try { final decodedJson = const JsonCodec().decode(content) as List; return Ok(list..addAll(decodedJson.cast())); - } catch (error, stackTrace) { - return Err(Failure( - message: '$error', - stackTrace: stackTrace, - )); + } catch (error, _) { + return Err(Failure('$runtimeType.get | $error')); } }), ), diff --git a/lib/src/core/json/json_map.dart b/lib/src/core/json/json_map.dart index 6d9abc5..cfb870f 100644 --- a/lib/src/core/json/json_map.dart +++ b/lib/src/core/json/json_map.dart @@ -55,11 +55,8 @@ class JsonMap { try { final decodedJson = const JsonCodec().decode(content) as Map; return Ok(map..addAll(decodedJson.cast())); - } catch (error, stackTrace) { - return Err(Failure( - message: '$error', - stackTrace: stackTrace, - )); + } catch (error, _) { + return Err(Failure('$runtimeType.get | $error')); } }), ), diff --git a/lib/src/core/result/result_extract_extension.dart b/lib/src/core/result/result_extract_extension.dart index e3bc745..12812f7 100644 --- a/lib/src/core/result/result_extract_extension.dart +++ b/lib/src/core/result/result_extract_extension.dart @@ -34,8 +34,7 @@ extension ResultExtract on Result { return switch (this) { Ok(:final V value) => value, Err(:final E error) => throw Failure( - message: "Called unwrap() on Result with error: $error", - stackTrace: StackTrace.current, + 'Called unwrap() on Result with error: $error', ), }; } @@ -70,8 +69,7 @@ extension ResultExtract on Result { return switch (this) { Ok(:final V value) => value, Err(:final E error) => throw Failure( - message: "$message: $error", - stackTrace: StackTrace.current, + '$message: $error', ), }; } @@ -98,8 +96,7 @@ extension ResultExtract on Result { E unwrapErr() { return switch (this) { Ok(:final V value) => throw Failure( - message: "Called unwrapErr() on Result with value: $value", - stackTrace: StackTrace.current, + 'Called unwrapErr() on Result with value: $value', ), Err(:final E error) => error, }; @@ -130,8 +127,7 @@ extension ResultExtract on Result { E expectErr(String message) { return switch (this) { Ok(:final V value) => throw Failure( - message: "$message: $value", - stackTrace: StackTrace.current, + '$message: $value', ), Err(:final E error) => error, }; diff --git a/lib/src/core/text_file.dart b/lib/src/core/text_file.dart index bfbe959..d8694a8 100644 --- a/lib/src/core/text_file.dart +++ b/lib/src/core/text_file.dart @@ -37,10 +37,7 @@ class _PathTextFile implements TextFile { case false: _log.warning('Failed to read from file.'); return Err( - Failure( - message: 'File $_filePath does not exist.', - stackTrace: StackTrace.current, - ), + Failure('File $_filePath does not exist.'), ); } } @@ -61,10 +58,7 @@ class _AssetTextFile implements TextFile { .catchError((error) { _log.warning('Failed to read from asset.'); return Err( - Failure( - message: error.toString(), - stackTrace: StackTrace.current, - ), + Failure.pass('$runtimeType.get |', error), ); }); } diff --git a/test/unit/core/error/failure_conctructor_test.dart b/test/unit/core/error/failure_conctructor_test.dart index fecee37..b20ef8e 100644 --- a/test/unit/core/error/failure_conctructor_test.dart +++ b/test/unit/core/error/failure_conctructor_test.dart @@ -4,66 +4,7 @@ import 'package:hmi_core/src/core/error/failure.dart'; void main() { test('Failure creates normally', () { expect( - () => Failure( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.dataSource( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.dataObject( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.dataCollection( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.auth( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.convertion( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.connection( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.translation( - message: '', - stackTrace: StackTrace.current, - ), - returnsNormally, - ); - expect( - () => Failure.unexpected( - message: '', - stackTrace: StackTrace.current, - ), + () => Failure(''), returnsNormally, ); }); diff --git a/test/unit/core/error/failure_to_string_test.dart b/test/unit/core/error/failure_to_string_test.dart index 88cfa01..bc9d462 100644 --- a/test/unit/core/error/failure_to_string_test.dart +++ b/test/unit/core/error/failure_to_string_test.dart @@ -13,10 +13,7 @@ void main() { ]; for(final message in errorMessages) { expect( - Failure( - message: message, - stackTrace: StackTrace.current, - ).toString(), + Failure(message).toString(), equals(message.toString()), ); } diff --git a/test/unit/core/result/result_constructor_test.dart b/test/unit/core/result/result_constructor_test.dart index a6e1987..965a34a 100644 --- a/test/unit/core/result/result_constructor_test.dart +++ b/test/unit/core/result/result_constructor_test.dart @@ -15,7 +15,7 @@ void main() { for(final value in testData) { expect( () => Err( - Failure(message: value, stackTrace: StackTrace.current), + Failure(value), ), returnsNormally, ); diff --git a/test/unit/core/result_old/result_constructor_test.dart b/test/unit/core/result_old/result_constructor_test.dart index adff5e2..91f7935 100644 --- a/test/unit/core/result_old/result_constructor_test.dart +++ b/test/unit/core/result_old/result_constructor_test.dart @@ -14,7 +14,7 @@ void main() { test('completes if error is provided', () { expect( () => Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ), returnsNormally, ); diff --git a/test/unit/core/result_old/result_data_test.dart b/test/unit/core/result_old/result_data_test.dart index baf0f06..a4e5c76 100644 --- a/test/unit/core/result_old/result_data_test.dart +++ b/test/unit/core/result_old/result_data_test.dart @@ -8,7 +8,7 @@ void main() { group('Result data', () { const resultWithData = Result(data: 'test'); final resultWithError = Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); test('throws if no data', () { expect(() => resultWithError.data, throwsA(isA())); diff --git a/test/unit/core/result_old/result_error_test.dart b/test/unit/core/result_old/result_error_test.dart index 546511e..7c3fa6c 100644 --- a/test/unit/core/result_old/result_error_test.dart +++ b/test/unit/core/result_old/result_error_test.dart @@ -8,7 +8,7 @@ void main() { group('Result error', () { const resultWithData = Result(data: 'test'); final resultWithError = Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); test('throws if object created without error', () { expect(() => resultWithData.error, throwsA(isA())); diff --git a/test/unit/core/result_old/result_fold_test.dart b/test/unit/core/result_old/result_fold_test.dart index cdaef07..89c27ed 100644 --- a/test/unit/core/result_old/result_fold_test.dart +++ b/test/unit/core/result_old/result_fold_test.dart @@ -8,11 +8,11 @@ void main() { group('Result fold', () { const resultWithData = Result(data: 'test'); final resultWithError = Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); final resultWithDataAndError = Result( data: 'test', - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); const valueIfData = 1; const valueIfError = 0; diff --git a/test/unit/core/result_old/result_has_data_test.dart b/test/unit/core/result_old/result_has_data_test.dart index cf466d3..7cdf009 100644 --- a/test/unit/core/result_old/result_has_data_test.dart +++ b/test/unit/core/result_old/result_has_data_test.dart @@ -8,7 +8,7 @@ void main() { group('Result hasData', () { const resultWithData = Result(data: 'test'); final resultWithError = Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); test('returns true if data is provided', () { expect(resultWithData.hasData, true); diff --git a/test/unit/core/result_old/result_has_error_test.dart b/test/unit/core/result_old/result_has_error_test.dart index e5b12dd..a14d0fb 100644 --- a/test/unit/core/result_old/result_has_error_test.dart +++ b/test/unit/core/result_old/result_has_error_test.dart @@ -8,7 +8,7 @@ void main() { group('Result hasError', () { const resultWithData = Result(data: 'test'); final resultWithError = Result( - error: Failure(message: '', stackTrace: StackTrace.current), + error: Failure(''), ); test('returns true if error is provided', () { expect(resultWithError.hasError, true); From 774e723030576765c4b39c96db9addc5da2adc1c Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 25 Jun 2025 17:03:22 +0300 Subject: [PATCH 41/44] Failure | child --- lib/src/app_settings/setting.dart | 5 +---- lib/src/core/option/option_extract_extension.dart | 10 ++-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/src/app_settings/setting.dart b/lib/src/app_settings/setting.dart index 9019372..73943d7 100644 --- a/lib/src/app_settings/setting.dart +++ b/lib/src/app_settings/setting.dart @@ -105,10 +105,7 @@ class _SettingValue implements Setting { void Function(Failure error)? onError, void Function()? onSuccess, }) async { - onError?.call(Failure( - message: 'Cannot update setting created during app runtime', - stackTrace: StackTrace.current, - )); + onError?.call(Failure('$runtimeType.update | Cannot update setting created during app runtime')); } // @override diff --git a/lib/src/core/option/option_extract_extension.dart b/lib/src/core/option/option_extract_extension.dart index f10a848..9a9d7af 100644 --- a/lib/src/core/option/option_extract_extension.dart +++ b/lib/src/core/option/option_extract_extension.dart @@ -33,10 +33,7 @@ extension OptionExtract on Option { V unwrap() { return switch (this) { Some(:final V value) => value, - None() => throw Failure( - message: "Called unwrap() on None", - stackTrace: StackTrace.current, - ), + None() => throw Failure("$runtimeType.unwrap() | Called on None"), }; } /// @@ -64,10 +61,7 @@ extension OptionExtract on Option { V expect(String message) { return switch (this) { Some(:final V value) => value, - None() => throw Failure( - message: message, - stackTrace: StackTrace.current, - ), + None() => throw Failure(message), }; } From 0db849b7572b8ba458fd673d69fdd42476c9058a Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 25 Jun 2025 17:04:07 +0300 Subject: [PATCH 42/44] Failure | child --- lib/src/core/error/failure.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/core/error/failure.dart b/lib/src/core/error/failure.dart index 3a1dee6..023e94a 100644 --- a/lib/src/core/error/failure.dart +++ b/lib/src/core/error/failure.dart @@ -19,9 +19,9 @@ class Failure { if (_child != null) { if (_child is Failure) { final tab = List.filled(depth, '\t').join(); - return '$message \n$tab${_child?._join(depth + 1)}'; + return '$message |\n$tab${_child?._join(depth + 1)}'; } - return '$message \n\t$_child'; + return '$message |\n\t$_child'; } return ''; } From 545288bee07307fb7e5c5703dd0c07fbf4f675e3 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 25 Jun 2025 17:09:01 +0300 Subject: [PATCH 43/44] Failure | child --- lib/src/core/error/failure.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/error/failure.dart b/lib/src/core/error/failure.dart index 023e94a..c017dd2 100644 --- a/lib/src/core/error/failure.dart +++ b/lib/src/core/error/failure.dart @@ -28,6 +28,6 @@ class Failure { // @override String toString() { - return '$message${_join(0)}'; + return '$message${_join(1)}'; } } From c643690a81123bba93b84057a561f702acdf0738 Mon Sep 17 00:00:00 2001 From: Anton Lobanov Date: Wed, 25 Jun 2025 17:11:03 +0300 Subject: [PATCH 44/44] Failure | child --- lib/src/core/error/failure.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/core/error/failure.dart b/lib/src/core/error/failure.dart index c017dd2..5393abd 100644 --- a/lib/src/core/error/failure.dart +++ b/lib/src/core/error/failure.dart @@ -17,11 +17,11 @@ class Failure { /// Converts all children errors into single string String _join(int depth) { if (_child != null) { + final tab = List.filled(depth, '\t').join(); if (_child is Failure) { - final tab = List.filled(depth, '\t').join(); return '$message |\n$tab${_child?._join(depth + 1)}'; } - return '$message |\n\t$_child'; + return '$message |\n$tab$_child'; } return ''; }