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, + ); + } + }); + }); + }); +}