From 4c20fa7df44cb31d1f676bc18aee53d8476961ca Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Fri, 1 Nov 2024 11:35:58 +0100 Subject: [PATCH] feat: implement validators --- CHANGELOG.md | 10 ++ README.md | 57 ++++++- example/ask.dart | 12 +- lib/commander_ui.dart | 27 +++- lib/src/application/components/ask.dart | 17 +- .../application/terminals/unix_terminal.dart | 4 +- .../themes/default_checkbox_theme.dart | 1 - .../themes/default_select_theme.dart | 1 - .../themes/default_swap_theme.dart | 1 - .../themes/default_task_theme.dart | 1 - .../validators/chain_validator.dart | 150 ++++++++++++++++++ lib/src/commander.dart | 5 +- lib/src/domains/models/chain_validator.dart | 35 ++++ 13 files changed, 281 insertions(+), 40 deletions(-) create mode 100644 lib/src/application/validators/chain_validator.dart create mode 100644 lib/src/domains/models/chain_validator.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 481f74f..8b1ed79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ - Add `TaskTheme` property in `task` component - Add `SwapTheme` property in `swap` component - Remove useless `message` property in task component +- Implement validators + - `notEmpty` : Check if the value is not empty + - `empty` : Check if the value is empty + - `email` : Check if the value is an email + - `between` : Check if the value is between two values + - `lowerThan` : Check if the value is lower than a value + - `greaterThan` : Check if the value is greater than a value + - `minLength` : Check if the value length is greater than a value + - `maxLength` : Check if the value length is lower than a value + - `equals` : Check if the value is equals to a value # 2.2.4 - Make `task` component as windows compatible diff --git a/README.md b/README.md index 7323d1f..9d95131 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,24 @@ Then run `pub get` to install the dependencies. A simple example of using Commander to create an ask component : - ✅ Secure -- ✅ Validator with error message as callback +- ✅ Integrated or custom validators - ✅ Default value ```dart Future main() async { final commander = Commander(level: Level.verbose); + final value = await commander.ask('What is your email ?', + validate: (validator) => validator + ..notEmpty(message: 'Name cannot be empty :)') + ..email(message: 'Please enter a valid email')); + + // Custom validator final value = await commander.ask('What is your name ?', - defaultValue: 'John Doe', - validate: (value) { - return switch (value) { - String(:final isEmpty) when isEmpty => 'Name cannot be empty', - _ => null, - }; - }); + validate: (validator) => validator + ..validate((value) => value == 'Bob' + ? 'Bob is not allowed' + : null)); print(value); } @@ -202,3 +205,41 @@ Future main() async { Future wait() => Future.delayed(Duration(seconds: Random().nextInt(3) + 1)); ``` + +## Theming + +Commander provides a theming system to customize the appearance of the components. +It is possible to define a global theme for all components or a specific theme for each component. + +```dart +Future main() async { + final commander = Commander( + level: Level.verbose, + componentTheme: ComponentTheme( + askTheme: DefaultAskTheme.copyWith(askPrefix: '🤖') + )); + + final value = await commander.ask('What is your email ?', + validate: (validator) => validator + ..notEmpty(message: 'Name cannot be empty :)') + ..email(message: 'Please enter a valid email')); + + print(value); +} +``` + +Each component that interacts with the user has a `theme` property that allows the appearance to be customised. + +```dart +Future main() async { + final commander = Commander(level: Level.verbose); + + final value = await commander.ask('What is your email ?', + theme: DefaultAskTheme.copyWith(askPrefix: '🤖'), + validate: (validator) => validator + ..notEmpty(message: 'Name cannot be empty :)') + ..email(message: 'Please enter a valid email')); + + print(value); +} +``` diff --git a/example/ask.dart b/example/ask.dart index da74ecd..b4d02e5 100644 --- a/example/ask.dart +++ b/example/ask.dart @@ -1,19 +1,11 @@ import 'package:commander_ui/commander_ui.dart'; -import 'package:commander_ui/src/application/themes/default_ask_theme.dart'; Future main() async { - final theme = DefaultAskTheme.copyWith(askPrefix: '🤖'); - final commander = Commander(level: Level.verbose); final value = await commander.ask('What is your name ?', - // defaultValue: 'John Doe', - validate: (value) { - return switch (value) { - String(:final isEmpty) when isEmpty => 'Name cannot be empty', - _ => null, - }; - }); + validate: (validator) => + validator..notEmpty(message: 'Name cannot be empty :)')); print(value); } diff --git a/lib/commander_ui.dart b/lib/commander_ui.dart index f5b3d0c..4895322 100644 --- a/lib/commander_ui.dart +++ b/lib/commander_ui.dart @@ -1,15 +1,26 @@ -export 'package:commander_ui/src/commander.dart'; -export 'package:commander_ui/src/level.dart'; -export 'package:commander_ui/src/application/utils/terminal_tools.dart'; -export 'package:commander_ui/src/domains/models/commander_theme.dart'; -export 'package:commander_ui/src/domains/models/component.dart'; - -export 'package:commander_ui/src/application/components/select.dart'; export 'package:commander_ui/src/application/components/ask.dart'; export 'package:commander_ui/src/application/components/checkbox.dart'; export 'package:commander_ui/src/application/components/screen.dart'; +export 'package:commander_ui/src/application/components/select.dart'; export 'package:commander_ui/src/application/components/swap.dart'; export 'package:commander_ui/src/application/components/table.dart'; export 'package:commander_ui/src/application/components/task.dart'; - +export 'package:commander_ui/src/application/themes/default_ask_theme.dart'; +export 'package:commander_ui/src/application/themes/default_checkbox_theme.dart'; +export 'package:commander_ui/src/application/themes/default_select_theme.dart'; +export 'package:commander_ui/src/application/themes/default_swap_theme.dart'; +export 'package:commander_ui/src/application/themes/default_task_theme.dart'; +export 'package:commander_ui/src/application/utils/terminal_tools.dart'; +export 'package:commander_ui/src/application/validators/chain_validator.dart'; +export 'package:commander_ui/src/commander.dart'; +export 'package:commander_ui/src/domains/models/chain_validator.dart'; +export 'package:commander_ui/src/domains/models/commander_theme.dart'; +export 'package:commander_ui/src/domains/models/component.dart'; +export 'package:commander_ui/src/domains/models/component_theme.dart'; +export 'package:commander_ui/src/domains/themes/ask_theme.dart'; +export 'package:commander_ui/src/domains/themes/checkbox_theme.dart'; +export 'package:commander_ui/src/domains/themes/select_theme.dart'; +export 'package:commander_ui/src/domains/themes/swap_theme.dart'; +export 'package:commander_ui/src/domains/themes/task_theme.dart'; +export 'package:commander_ui/src/level.dart'; export 'package:mansion/mansion.dart'; diff --git a/lib/src/application/components/ask.dart b/lib/src/application/components/ask.dart index 5102eac..fff93dd 100644 --- a/lib/src/application/components/ask.dart +++ b/lib/src/application/components/ask.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:commander_ui/src/application/terminals/terminal.dart'; import 'package:commander_ui/src/application/themes/default_ask_theme.dart'; import 'package:commander_ui/src/application/utils/terminal_tools.dart'; +import 'package:commander_ui/src/application/validators/chain_validator.dart'; +import 'package:commander_ui/src/domains/models/chain_validator.dart'; import 'package:commander_ui/src/domains/models/component.dart'; import 'package:commander_ui/src/domains/themes/ask_theme.dart'; import 'package:mansion/mansion.dart'; @@ -15,10 +17,10 @@ final class Ask with TerminalTools implements Component> { final Terminal _terminal; final AskTheme _theme; - late final String _message; - late final String? _defaultValue; - late final bool _hidden; - late final String? Function(String value)? _validate; + final String _message; + final String? _defaultValue; + final bool _hidden; + final Function(TextualChainValidator)? _validate; /// Creates a new instance of [Ask]. bool get _hasDefault => _defaultValue != null && '$_defaultValue'.isNotEmpty; @@ -37,7 +39,7 @@ final class Ask with TerminalTools implements Component> { {required String message, String? defaultValue, bool hidden = false, - String? Function(String value)? validate, + Function(TextualChainValidator)? validate, AskTheme? theme}) : _message = message, _defaultValue = defaultValue, @@ -62,7 +64,10 @@ final class Ask with TerminalTools implements Component> { input == null || input.isEmpty ? _resolvedDefaultValue : input; if (_validate != null) { - final result = _validate!(response); + final validator = ValidatorChain(); + _validate!(validator); + + final result = validator.execute(response); if (result case String error) { _onError(error); diff --git a/lib/src/application/terminals/unix_terminal.dart b/lib/src/application/terminals/unix_terminal.dart index 09ccc2a..73e5633 100644 --- a/lib/src/application/terminals/unix_terminal.dart +++ b/lib/src/application/terminals/unix_terminal.dart @@ -2,11 +2,11 @@ import 'dart:ffi'; import 'dart:io'; import 'package:commander_ui/src/application/ffi/control_mode.dart'; +import 'package:commander_ui/src/application/ffi/input_mode.dart'; import 'package:commander_ui/src/application/ffi/local_mode.dart'; import 'package:commander_ui/src/application/ffi/output_mode.dart'; -import 'package:commander_ui/src/application/ffi/input_mode.dart'; -import 'package:ffi/ffi.dart'; import 'package:commander_ui/src/application/terminals/terminal.dart'; +import 'package:ffi/ffi.dart'; class UnixTerminal implements Terminal { late final DynamicLibrary _lib; diff --git a/lib/src/application/themes/default_checkbox_theme.dart b/lib/src/application/themes/default_checkbox_theme.dart index 3427345..b188be0 100644 --- a/lib/src/application/themes/default_checkbox_theme.dart +++ b/lib/src/application/themes/default_checkbox_theme.dart @@ -1,5 +1,4 @@ import 'package:commander_ui/commander_ui.dart'; -import 'package:commander_ui/src/domains/themes/checkbox_theme.dart'; final class DefaultCheckBoxTheme implements CheckboxTheme { @override diff --git a/lib/src/application/themes/default_select_theme.dart b/lib/src/application/themes/default_select_theme.dart index e265bf6..cfb4095 100644 --- a/lib/src/application/themes/default_select_theme.dart +++ b/lib/src/application/themes/default_select_theme.dart @@ -1,5 +1,4 @@ import 'package:commander_ui/commander_ui.dart'; -import 'package:commander_ui/src/domains/themes/select_theme.dart'; final class DefaultSelectTheme implements SelectTheme { @override diff --git a/lib/src/application/themes/default_swap_theme.dart b/lib/src/application/themes/default_swap_theme.dart index c05ed86..c196830 100644 --- a/lib/src/application/themes/default_swap_theme.dart +++ b/lib/src/application/themes/default_swap_theme.dart @@ -1,5 +1,4 @@ import 'package:commander_ui/commander_ui.dart'; -import 'package:commander_ui/src/domains/themes/swap_theme.dart'; final class DefaultSwapTheme implements SwapTheme { @override diff --git a/lib/src/application/themes/default_task_theme.dart b/lib/src/application/themes/default_task_theme.dart index 2403129..a342e1e 100644 --- a/lib/src/application/themes/default_task_theme.dart +++ b/lib/src/application/themes/default_task_theme.dart @@ -1,5 +1,4 @@ import 'package:commander_ui/commander_ui.dart'; -import 'package:commander_ui/src/domains/themes/task_theme.dart'; final class DefaultTaskTheme implements TaskTheme { @override diff --git a/lib/src/application/validators/chain_validator.dart b/lib/src/application/validators/chain_validator.dart new file mode 100644 index 0000000..11dee64 --- /dev/null +++ b/lib/src/application/validators/chain_validator.dart @@ -0,0 +1,150 @@ +import 'package:commander_ui/src/domains/models/chain_validator.dart'; + +final class ValidatorChain + implements ChainValidatorExecutor, ChainValidatorContract { + final List _validators = []; + + String? value; + + @override + void validate(String? Function(String? value) validator) { + _validators.add(validator); + } + + @override + void notEmpty({String? message = 'This field is required'}) { + _validators.add((value) { + return switch (value) { + String(:final isEmpty) when isEmpty => message, + _ => null, + }; + }); + } + + @override + void empty({String? message = 'This field should be empty'}) { + _validators.add((value) { + return switch (value) { + null => null, + _ => message, + }; + }); + } + + @override + void email({String? message = 'This field should be a valid email'}) { + _validators.add((value) { + final emailRegExp = RegExp(r'^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$'); + + if (value == null) { + return message; + } + + return switch (emailRegExp.hasMatch(value)) { + false => message, + _ => null, + }; + }); + } + + @override + void minLength(int count, {String? message}) { + _validators.add((value) { + if (value case String value when value.length < count) { + return message ?? 'This field should have at least $count characters'; + } + + return null; + }); + } + + @override + void maxLength(int count, {String? message}) { + _validators.add((value) { + if (value case String value when value.length > count) { + return message ?? 'This field should have at most $count characters'; + } + + return null; + }); + } + + @override + void equals(String str, {String? message}) { + _validators.add((value) { + if (value case String value when value != str) { + return message ?? 'This field should be equal to $str'; + } + + return null; + }); + } + + @override + void between(int min, int max, {String? message}) { + final errorMessage = + message ?? 'This field should be between $min and $max characters'; + _validators.add((value) { + if (value == null) { + return errorMessage; + } + + final length = value.length; + + if (length < min || length > max) { + return errorMessage; + } + + return null; + }); + } + + @override + void lowerThan(int value, {String? message}) { + final errorMessage = message ?? 'This field should be lower than $value'; + _validators.add((response) { + if (response == null) { + return errorMessage; + } + + final intValue = int.tryParse(response); + + if (intValue == null || intValue >= value) { + return errorMessage; + } + + return null; + }); + } + + @override + void greaterThan(int value, {String? message}) { + final errorMessage = message ?? 'This field should be greater than $value'; + _validators.add((response) { + if (response == null) { + return errorMessage; + } + + final intValue = int.tryParse(response); + + if (intValue == null || intValue <= value) { + return errorMessage; + } + + return null; + }); + } + + @override + String? execute(String? value) { + print('Executing validators'); + for (final validator in _validators) { + final result = validator(value); + if (result != null) { + return result; + } + } + + return null; + } +} diff --git a/lib/src/commander.dart b/lib/src/commander.dart index aed4948..ec72346 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -5,11 +5,12 @@ import 'package:commander_ui/src/application/components/ask.dart'; import 'package:commander_ui/src/application/components/checkbox.dart'; import 'package:commander_ui/src/application/components/screen.dart'; import 'package:commander_ui/src/application/components/select.dart'; +import 'package:commander_ui/src/application/components/swap.dart'; import 'package:commander_ui/src/application/components/table.dart'; import 'package:commander_ui/src/application/components/task.dart'; -import 'package:commander_ui/src/application/components/swap.dart'; import 'package:commander_ui/src/application/terminals/terminal.dart'; import 'package:commander_ui/src/application/utils/terminal_tools.dart'; +import 'package:commander_ui/src/domains/models/chain_validator.dart'; import 'package:commander_ui/src/domains/models/commander_theme.dart'; import 'package:commander_ui/src/domains/models/component_theme.dart'; import 'package:commander_ui/src/domains/themes/ask_theme.dart'; @@ -70,7 +71,7 @@ class Commander with TerminalTools { Future ask(String message, {String? defaultValue, bool hidden = false, - String? Function(String)? validate, + Function(TextualChainValidator)? validate, AskTheme? theme}) => Ask(_terminal, message: message, diff --git a/lib/src/domains/models/chain_validator.dart b/lib/src/domains/models/chain_validator.dart new file mode 100644 index 0000000..673aae7 --- /dev/null +++ b/lib/src/domains/models/chain_validator.dart @@ -0,0 +1,35 @@ +abstract interface class ChainValidatorExecutor { + String? execute(String? value); +} + +abstract interface class BaseChainValidator { + void validate(String? Function(String? value) validator); +} + +abstract interface class TextualChainValidator implements BaseChainValidator { + void notEmpty({String? message}); + + void empty({String? message}); + + void email({String? message}); + + void minLength(int count, {String? message}); + + void maxLength(int count, {String? message}); + + void equals(String value, {String? message}); +} + +abstract interface class NumberChainValidator implements BaseChainValidator { + void between(int min, int max, {String? message}); + + void lowerThan(int value, {String? message}); + + void greaterThan(int value, {String? message}); +} + +abstract interface class ChainValidatorContract + implements + BaseChainValidator, + TextualChainValidator, + NumberChainValidator {}