diff --git a/packages/config/README.md b/packages/config/README.md
index 409843a..7ecd4d6 100644
--- a/packages/config/README.md
+++ b/packages/config/README.md
@@ -35,7 +35,7 @@ The main features are:
- A group can specify mutually exclusive options.
- A group can be mandatory in that at least one of its options is set.
-- Tracability - the information on an option's value source is retained.
+- Traceability - the information on an option's value source is retained.
- The error handling is consistent, in contrast to the args package.
- Fail-fast, all validation is performed up-front.
@@ -211,7 +211,7 @@ The configuration library resolves each option value in a specific order, with e
5. **Default values**
- A default value guarantees that an option has a value
- Const values are specified using `defaultsTo`
- - Non-const values are specifed with a callback using `fromDefault`
+ - Non-const values are specified with a callback using `fromDefault`
This order ensures that:
- Command-line arguments always take precedence, allowing users to override any other settings
@@ -239,6 +239,7 @@ The library provides a rich set of typed options out of the box. All option type
| String | `StringOption` | None | String values |
| Boolean | `FlagOption` | `negatable` | Whether the flag can be negated |
| Integer | `IntOption` | `min`
`max` | Minimum allowed value
Maximum allowed value |
+| Num | `NumOption` | `T`: {`int`,`double`,`num`}
`min`
`max` | Static type of parsed value
Minimum allowed value
Maximum allowed value |
| DateTime | `DateTimeOption` | `min`
`max` | Minimum allowed date/time
Maximum allowed date/time |
| Duration | `DurationOption` | `min`
`max` | Minimum allowed duration
Maximum allowed duration |
| Any Enum | `EnumOption` | None | Typed enum values |
diff --git a/packages/config/lib/src/config/option_types.dart b/packages/config/lib/src/config/option_types.dart
index 7dabdc6..c421252 100644
--- a/packages/config/lib/src/config/option_types.dart
+++ b/packages/config/lib/src/config/option_types.dart
@@ -193,11 +193,11 @@ class ComparableValueOption extends ConfigOptionBase {
void validateValue(final V value) {
super.validateValue(value);
- final mininum = min;
- if (mininum != null && value.compareTo(mininum) < 0) {
+ final minimum = min;
+ if (minimum != null && value.compareTo(minimum) < 0) {
throw FormatException(
'${valueParser.format(value)} is below the minimum '
- '(${valueParser.format(mininum)})',
+ '(${valueParser.format(minimum)})',
);
}
final maximum = max;
@@ -246,6 +246,64 @@ class IntOption extends ComparableValueOption {
}) : super(valueParser: const IntParser());
}
+/// Converts a source string value to the chosen `num` type `T`.
+///
+/// Throws a [FormatException] with an appropriate message
+/// if the value cannot be parsed.
+///
+/// **Note**: `NumParser.parse` always throws an [UnsupportedError].
+class NumParser extends ValueParser {
+ const NumParser();
+
+ @override
+ T parse(final String value) {
+ if (T == double) return double.parse(value) as T;
+ if (T == int) return int.parse(value) as T;
+ if (T == num) return num.parse(value) as T;
+ throw UnsupportedError('NumParser never parses anything.');
+ }
+}
+
+/// Number (int/double/num) value configuration option.
+///
+/// Supports minimum and maximum range checking.
+///
+/// **Note**:
+/// * Usage of `NumOption` is unreasonable and not supported
+/// * Reference for usage of `num`:
+/// https://dart.dev/resources/language/number-representation
+class NumOption extends ComparableValueOption {
+ const NumOption({
+ super.argName,
+ super.argAliases,
+ super.argAbbrev,
+ super.argPos,
+ super.envName,
+ super.configKey,
+ super.fromCustom,
+ super.fromDefault,
+ super.defaultsTo,
+ super.helpText,
+ super.valueHelp = 'number',
+ super.allowedHelp,
+ super.group,
+ super.allowedValues,
+ super.customValidator,
+ super.mandatory,
+ super.hide,
+ super.min,
+ super.max,
+ }) : super(
+ valueParser: (T == double
+ ? const NumParser()
+ : T == int
+ ? const NumParser()
+ : T == num
+ ? const NumParser()
+ : const NumParser()) as NumParser,
+ );
+}
+
/// Parses a date string into a [DateTime] object.
/// Throws [FormatException] if parsing failed.
///
diff --git a/packages/config/test/config/num_option_test.dart b/packages/config/test/config/num_option_test.dart
new file mode 100644
index 0000000..ae684ba
--- /dev/null
+++ b/packages/config/test/config/num_option_test.dart
@@ -0,0 +1,161 @@
+import 'package:config/config.dart' show NumOption, Configuration;
+import 'package:test/test.dart';
+
+void main() {
+ group('Given a NumOption', () {
+ const numOpt = NumOption(argName: 'num', mandatory: true);
+ group('when a fractional number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '123.45'],
+ );
+ test('then it is parsed successfully', () {
+ expect(config.errors, isEmpty);
+ expect(
+ config.value(numOpt),
+ equals(123.45),
+ );
+ });
+ test('then the runtime type is double', () {
+ expect(
+ config.value(numOpt).runtimeType,
+ equals(double),
+ );
+ });
+ });
+ group('when an integer number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12345'],
+ );
+ test('then it is parsed successfully', () {
+ expect(config.errors, isEmpty);
+ expect(
+ config.value(numOpt),
+ equals(12345),
+ );
+ });
+ test('then the runtime type is double', () {
+ expect(
+ config.value(numOpt).runtimeType,
+ equals(double),
+ );
+ });
+ });
+ group('when a non-{double,int} value is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12i+345j'],
+ );
+ test('then it is not parsed successfully', () {
+ expect(config.errors, isNotEmpty);
+ });
+ });
+ });
+ group('Given a NumOption', () {
+ const numOpt = NumOption(argName: 'num', mandatory: true);
+ group('when an integer number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12345'],
+ );
+ test('then it is parsed successfully', () {
+ expect(config.errors, isEmpty);
+ expect(
+ config.value(numOpt),
+ equals(12345),
+ );
+ });
+ test('then the runtime type is int', () {
+ expect(
+ config.value(numOpt).runtimeType,
+ equals(int),
+ );
+ });
+ });
+ group('when a fractional number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '123.45'],
+ );
+ test('then it is not parsed successfully', () {
+ expect(config.errors, isNotEmpty);
+ });
+ });
+ group('when a non-{double,int} value is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12i+345j'],
+ );
+ test('then it is not parsed successfully', () {
+ expect(config.errors, isNotEmpty);
+ });
+ });
+ });
+ group('Given a NumOption', () {
+ const numOpt = NumOption(argName: 'num', mandatory: true);
+ group('when a fractional number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '123.45'],
+ );
+ test('then it is parsed successfully', () {
+ expect(config.errors, isEmpty);
+ expect(
+ config.value(numOpt),
+ equals(123.45),
+ );
+ });
+ test('then the runtime type is double', () {
+ expect(
+ config.value(numOpt).runtimeType,
+ equals(double),
+ );
+ });
+ });
+ group('when an integer number is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12345'],
+ );
+ test('then it is parsed successfully', () {
+ expect(config.errors, isEmpty);
+ expect(
+ config.value(numOpt),
+ equals(12345),
+ );
+ });
+ test('then the runtime type is int', () {
+ expect(
+ config.value(numOpt).runtimeType,
+ equals(int),
+ );
+ });
+ });
+ group('when a non-{double,int} value is passed', () {
+ final config = Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', '12i+345j'],
+ );
+ test('then it is not parsed successfully', () {
+ expect(config.errors, isNotEmpty);
+ });
+ });
+ });
+ group('Given a NumOption', () {
+ const numOpt = NumOption(argName: 'num', mandatory: true);
+ group('when any value is passed', () {
+ test('then it reports an UnsupportedError', () {
+ for (final val in ['123.45', '12345', '12i+345j']) {
+ expect(
+ () => Configuration.resolveNoExcept(
+ options: [numOpt],
+ args: ['--num', val],
+ ),
+ throwsUnsupportedError,
+ );
+ }
+ });
+ });
+ });
+}