Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`<br>`max` | Minimum allowed value<br>Maximum allowed value |
| Num | `NumOption<T>` | `T`: {`int`,`double`,`num`}<br>`min`<br>`max` | Static type of parsed value<br>Minimum allowed value<br>Maximum allowed value |
| DateTime | `DateTimeOption` | `min`<br>`max` | Minimum allowed date/time<br>Maximum allowed date/time |
| Duration | `DurationOption` | `min`<br>`max` | Minimum allowed duration<br>Maximum allowed duration |
| Any Enum | `EnumOption<E>` | None | Typed enum values |
Expand Down
64 changes: 61 additions & 3 deletions packages/config/lib/src/config/option_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ class ComparableValueOption<V extends Comparable> extends ConfigOptionBase<V> {
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;
Expand Down Expand Up @@ -246,6 +246,64 @@ class IntOption extends ComparableValueOption<int> {
}) : 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<Never>.parse` always throws an [UnsupportedError].
class NumParser<T extends num> extends ValueParser<T> {
const NumParser();

@override
T parse(final String value) {
if (T == double) return double.parse(value) as T;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be written as a switch on T instead? If so it will perhaps let you avoid the fallback exception at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christerswahn sir, I too like the switch format more for such use-cases. Unfortunately, Dart does not yet support switching the static value of a generic type parameter.

I've rechecked its current status: dart-lang/language#4085

If you wish to have a better alternative here, I'd be happy to know and implement it alongside.

if (T == int) return int.parse(value) as T;
if (T == num) return num.parse(value) as T;
throw UnsupportedError('NumParser<Never> never parses anything.');
}
}

/// Number (int/double/num) value configuration option.
///
/// Supports minimum and maximum range checking.
///
/// **Note**:
/// * Usage of `NumOption<Never>` is unreasonable and not supported
/// * Reference for usage of `num`:
/// https://dart.dev/resources/language/number-representation
class NumOption<T extends num> extends ComparableValueOption<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, should IntOption now extend NumOption? Would there be any drawbacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christerswahn sir, I agree, as I'd have had similar thoughts for the future of IntOption from a maintenance perspective.

A drawback I'm seeing, as a nitpick, is the extremely slight overhead within NumOption.parse by 2-3 condition checks and a final typecast. However, I'd vote this off as negligible.

I'm not sure about it from a design perspective; for example, in extremely rare scenarios where downstream consumer(s) rely on certain reflection-based properties for their use-cases. However, I'd again vote this off for being too pedantic (as almost any change, in general, could then be seen as a breaking change).

With respect to a potential DoubleOption, the only reason I didn't bother making it directly to resolve my issue is because "Double Option" sounds odd to me even within that context. I'm not too concerned about it tho.

Please declare your decision, and I'd adapt the code accordingly:

  • whether or not to refactor IntOption
  • whether or not to add DoubleOption

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<double>()
: T == int
? const NumParser<int>()
: T == num
? const NumParser<num>()
: const NumParser<Never>()) as NumParser<T>,
);
}

/// Parses a date string into a [DateTime] object.
/// Throws [FormatException] if parsing failed.
///
Expand Down
161 changes: 161 additions & 0 deletions packages/config/test/config/num_option_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import 'package:config/config.dart' show NumOption, Configuration;
import 'package:test/test.dart';

void main() {
group('Given a NumOption<double>', () {
const numOpt = NumOption<double>(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<int>', () {
const numOpt = NumOption<int>(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<num>', () {
const numOpt = NumOption<num>(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<Never>', () {
const numOpt = NumOption<Never>(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,
);
}
});
});
});
}