Skip to content

Commit

Permalink
feat: implement validators
Browse files Browse the repository at this point in the history
  • Loading branch information
LeadcodeDev committed Nov 1, 2024
1 parent 2375cbf commit 4c20fa7
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 40 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 49 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> 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);
}
Expand Down Expand Up @@ -202,3 +205,41 @@ Future<void> main() async {
Future<void> 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<void> 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<void> 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);
}
```
12 changes: 2 additions & 10 deletions example/ask.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import 'package:commander_ui/commander_ui.dart';
import 'package:commander_ui/src/application/themes/default_ask_theme.dart';

Future<void> 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);
}
27 changes: 19 additions & 8 deletions lib/commander_ui.dart
Original file line number Diff line number Diff line change
@@ -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';
17 changes: 11 additions & 6 deletions lib/src/application/components/ask.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,10 +17,10 @@ final class Ask<T> with TerminalTools implements Component<Future<T>> {
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;
Expand All @@ -37,7 +39,7 @@ final class Ask<T> with TerminalTools implements Component<Future<T>> {
{required String message,
String? defaultValue,
bool hidden = false,
String? Function(String value)? validate,
Function(TextualChainValidator)? validate,
AskTheme? theme})
: _message = message,
_defaultValue = defaultValue,
Expand All @@ -62,7 +64,10 @@ final class Ask<T> with TerminalTools implements Component<Future<T>> {
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);

Expand Down
4 changes: 2 additions & 2 deletions lib/src/application/terminals/unix_terminal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion lib/src/application/themes/default_checkbox_theme.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/src/application/themes/default_select_theme.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/src/application/themes/default_swap_theme.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/src/application/themes/default_task_theme.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down
150 changes: 150 additions & 0 deletions lib/src/application/validators/chain_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import 'package:commander_ui/src/domains/models/chain_validator.dart';

final class ValidatorChain
implements ChainValidatorExecutor, ChainValidatorContract {
final List<String? Function(String?)> _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;
}
}
Loading

0 comments on commit 4c20fa7

Please sign in to comment.