From 252b6bb5836915102592346d4802c3903c593ac6 Mon Sep 17 00:00:00 2001 From: pseudonym Date: Wed, 19 Nov 2025 16:51:43 +0530 Subject: [PATCH 1/2] feat(config): Support for `num` Option Type - can parse any among {`int`, `double`, `num`} - corresponding new Test Cases - updated corresponding Documentation - fixed a few pre-existing typos --- packages/config/README.md | 5 +- .../config/lib/src/config/option_types.dart | 51 +++++- .../config/test/config/num_option_test.dart | 145 ++++++++++++++++++ 3 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 packages/config/test/config/num_option_test.dart 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..40e5928 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,51 @@ class IntOption extends ComparableValueOption { }) : super(valueParser: const IntParser()); } +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('Dart does not consider $T as a Type of num'); + } +} + +/// Number (int/double/num) value configuration option. +/// +/// Supports minimum and maximum range checking. +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() + : 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..c22aca4 --- /dev/null +++ b/packages/config/test/config/num_option_test.dart @@ -0,0 +1,145 @@ +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); + }); + }); + }); +} From 003008aae1f5d54839ad629cc80f2cf89a9fd4a4 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:30:11 +0530 Subject: [PATCH 2/2] fix(#95): More robustness - handles the `Never` case - better documentation --- .../config/lib/src/config/option_types.dart | 17 ++++++++++++++-- .../config/test/config/num_option_test.dart | 20 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/config/lib/src/config/option_types.dart b/packages/config/lib/src/config/option_types.dart index 40e5928..c421252 100644 --- a/packages/config/lib/src/config/option_types.dart +++ b/packages/config/lib/src/config/option_types.dart @@ -246,6 +246,12 @@ 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(); @@ -254,13 +260,18 @@ class NumParser extends ValueParser { 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('Dart does not consider $T as a Type of num'); + 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, @@ -287,7 +298,9 @@ class NumOption extends ComparableValueOption { ? const NumParser() : T == int ? const NumParser() - : const NumParser()) as NumParser, + : T == num + ? const NumParser() + : const NumParser()) as NumParser, ); } diff --git a/packages/config/test/config/num_option_test.dart b/packages/config/test/config/num_option_test.dart index c22aca4..ae684ba 100644 --- a/packages/config/test/config/num_option_test.dart +++ b/packages/config/test/config/num_option_test.dart @@ -92,8 +92,8 @@ void main() { }); }); }); - group('Given a NumOption', () { - const numOpt = NumOption(argName: 'num', mandatory: true); + group('Given a NumOption', () { + const numOpt = NumOption(argName: 'num', mandatory: true); group('when a fractional number is passed', () { final config = Configuration.resolveNoExcept( options: [numOpt], @@ -142,4 +142,20 @@ void main() { }); }); }); + 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, + ); + } + }); + }); + }); }