Skip to content

Conversation

@indraneel12
Copy link
Contributor

@indraneel12 indraneel12 commented Nov 19, 2025

Resolves #94

Consumers of this package can now avail built-in support for num among Option Types via the new NumOption class.

Old Behavior

As described in the referenced issue, this feature is absent in the current version of this package.

New Behavior

The desired functionality is achieved, and all tests pass successfully. It is now possible to parse int or double or num via NumOption.

Screenshot_20251120_022917

Example

import 'package:cli_tools/better_command_runner.dart';
import 'package:config/config.dart';

enum MockOption<V extends Object> implements OptionDefinition<V> {
  age(
    NumOption<int>(
      argName: 'age',
      helpText: 'Present age of the person.',
      valueHelp: 'integer',
      min: 0,
      mandatory: true,
    ),
  ),
  victory(
    NumOption<double>(
      argName: 'victory',
      helpText: 'Chance of winning the game.',
      valueHelp: 'probability',
      min: 0.0,
      max: 1.0,
      mandatory: true,
    ),
  ),
  position(
    NumOption(
      argName: 'position',
      helpText: 'X-coordinate of the person.',
      valueHelp: 'abscissa',
      mandatory: true,
    ),
  ),
  ;

  const MockOption(this.option);

  @override
  final ConfigOptionBase<V> option;
}

final class PersonCommand extends BetterCommand<MockOption, void> {
  PersonCommand() : super(options: MockOption.values);

  @override
  String get name => 'person';

  @override
  String get description => 'A dummy command for the demonstration.';

  @override
  void runWithConfig(final Configuration<MockOption> config) {
    print('Age: ${config.value(MockOption.age)}');
    print('Victory: ${config.value(MockOption.victory)}');
    print('Position: ${config.value(MockOption.position)}');
  }
}

void main(final List<String> args) async {
  final runner = BetterCommandRunner(
    'mock',
    'A mock command to demonstrate the NumOption feature.',
  )..addCommand(PersonCommand());
  try {
    await runner.run(args);
  } on UsageException catch (e) {
    print(e.message);
  }
}

Demo

Screenshot_20251119_193028

Summary by CodeRabbit

  • Documentation

    • Corrected spelling in feature descriptions and default-values documentation for clarity.
  • New Features

    • Added a numeric option type that accepts integers, decimals, or generic numbers with optional min/max constraints and type-aware validation.
  • Tests

    • Added tests covering parsing, type outcomes, validation errors, and constraint behavior across the supported numeric type scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

- can parse any among {`int`, `double`, `num`}
- corresponding new Test Cases
- updated corresponding Documentation
- fixed a few pre-existing typos
@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

📝 Walkthrough

Walkthrough

Adds generic numeric option support to the config package: introduces NumParser<T extends num> and NumOption<T extends num> with type-aware parsing and min/max validation, fixes a few typos in docs and code, and adds tests covering double, int, num, and Never type behaviors.

Changes

Cohort / File(s) Change Summary
Documentation updates
packages/config/README.md
Fixed typos ("Tracability" → "Traceability", "specifed" → "specified") and added a Num row to the Supported option types table documenting NumOption<T> with min/max.
Core implementation
packages/config/lib/src/config/option_types.dart
Renamed local variable mininumminimum in ComparableValueOption.validateValue. Added NumParser<T extends num> with parsing logic for double, int, and num. Added NumOption<T extends num> extending ComparableValueOption<T> and selecting a NumParser<T> via a conditional expression (falls back to Never parser when T is unsupported).
Tests
packages/config/test/config/num_option_test.dart
Added tests exercising NumOption<double>, NumOption<int>, NumOption<num>, and NumOption<Never> for fractional, integer, and invalid inputs, and verifying parsed values, runtime types, and error behavior.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as User/CLI
    participant NumOption
    participant NumParser
    participant Validator as ComparableValueOption

    CLI->>NumOption: resolveNoExcept(args, config)
    NumOption->>NumOption: determine generic T (double/int/num/Never)
    alt T == double
        NumOption->>NumParser: use NumParser<double>
    else T == int
        NumOption->>NumParser: use NumParser<int>
    else T == num
        NumOption->>NumParser: use NumParser<num>
    else
        NumOption->>NumParser: use NumParser<Never> (unsupported)
    end
    NumParser->>NumParser: parse(string) -> T or throw
    NumParser->>Validator: return parsed value
    Validator->>Validator: validateValue(min/max)
    Validator-->>CLI: resolved typed value or errors
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review generic dispatch in NumOption initializer ensuring the conditional selection of NumParser<T> is type-safe and does not rely on runtime type checks that break under inference/obfuscation.
  • Inspect NumParser.parse logic for correct parsing and error messages for double/int/num and for edge cases (large integers, locale issues).
  • Verify ComparableValueOption.validateValue after the minimum rename still enforces bounds correctly across numeric types.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding support for num Option Type, which directly addresses the PR's primary objective.
Linked Issues check ✅ Passed The PR successfully implements built-in support for numeric types (int, double, num) via NumOption, fully resolving issue #94's request for double support.
Out of Scope Changes check ✅ Passed All changes are directly related to adding num Option Type support: documentation updates, NumParser/NumOption implementation, and comprehensive tests for the feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 252b6bb and 003008a.

📒 Files selected for processing (2)
  • packages/config/lib/src/config/option_types.dart (2 hunks)
  • packages/config/test/config/num_option_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/config/test/config/num_option_test.dart
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: indraneel12
Repo: serverpod/cli_tools PR: 0
File: :0-0
Timestamp: 2025-11-19T11:55:00.345Z
Learning: In Dart, the `num` class is sealed, meaning it can only be instantiated as `int` or `double`. This makes type checks like `T == int`, `T == double` exhaustive when `T extends num`, as documented in the Dart core library.
📚 Learning: 2025-05-02T12:13:11.821Z
Learnt from: Isakdl
Repo: serverpod/cli_tools PR: 41
File: lib/src/better_command_runner/better_command_runner.dart:164-172
Timestamp: 2025-05-02T12:13:11.821Z
Learning: Avoid generic type casting (e.g., `as List<O>`) when possible in Dart code to ensure better type safety. When dealing with generic types, prefer explicit type checking or type-safe alternatives to prevent runtime TypeErrors.

Applied to files:

  • packages/config/lib/src/config/option_types.dart
📚 Learning: 2025-11-19T11:55:00.345Z
Learnt from: indraneel12
Repo: serverpod/cli_tools PR: 0
File: :0-0
Timestamp: 2025-11-19T11:55:00.345Z
Learning: In Dart, the `num` class is sealed, meaning it can only be instantiated as `int` or `double`. This makes type checks like `T == int`, `T == double` exhaustive when `T extends num`, as documented in the Dart core library.

Applied to files:

  • packages/config/lib/src/config/option_types.dart
🔇 Additional comments (3)
packages/config/lib/src/config/option_types.dart (3)

196-201: Local minimum alias keeps range check clear and correct

The rename to minimum avoids confusion with the min field while preserving the original comparison semantics; the lower‑bound validation remains correct and behavior‑preserving.


249-265: NumParser exhaustively covers int/double/num with a clear Never fallback

The parse implementation correctly dispatches based on T for double, int, and num, and the UnsupportedError fallback is only reachable for the intentionally unsupported NumParser<Never> case, matching the doc comment. The as T casts are safe here given the sealed num hierarchy and explicit T == ... checks.

Based on learnings


275-305: NumOption wiring to NumParser is sound; cast is justified

The nested const conditional cleanly selects the appropriate NumParser for double, int, or num, with NumParser<Never> as the documented unsupported fallback. Given the sealed num types and explicit T checks, the final as NumParser<T> cast is a reasonable, type‑safe compromise to satisfy the const initializer and avoid more complex alternatives.

Based on learnings


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/config/README.md (1)

242-242: NumOption row matches implementation; consider clarifying default behavior

The new Num | NumOption<T> row correctly documents:

  • Supported type arguments T: {int, double, num}
  • min / max mirroring other comparable types.

Given that NumOption without an explicit type argument defaults to num and uses num.parse, where integer-like inputs become int and fractional inputs become double, you might optionally add a brief note to make that behavior explicit for readers.

packages/config/test/config/num_option_test.dart (1)

95-144: Generic NumOption tests capture num.parse behavior

The “Given a NumOption” group (defaulting to T == num) effectively verifies:

  • Fractional input parses and is a double.
  • Integer input parses and is an int.
  • Non-numeric input yields errors.

This locks in the desirable “int vs double based on input” semantics for the default NumOption case. If you want to make this more discoverable, you could reference this behavior briefly in the README as well.

packages/config/lib/src/config/option_types.dart (2)

249-259: NumParser<T> correctly dispatches by numeric type; minor error-message polish possible

The parser cleanly supports:

  • T == doubledouble.parse
  • T == intint.parse
  • T == numnum.parse (int-or-double depending on input)

This aligns with the new tests. For other T extends num (e.g., custom numeric types), parse throws UnsupportedError, which is appropriate given the README documents only int, double, and num.

If you expect people might instantiate NumParser<SomeCustomNum> directly, you could consider tightening the message to something like “NumParser only supports type arguments int, double, and num (got $T)” for extra clarity, but it’s not required.


261-292: NumOption<T> wiring looks correct and efficient; consider more explicit unsupported-T handling

The NumOption<T extends num> constructor:

  • Reuses ComparableValueOption<T> for min / max support, consistent with IntOption, DateTimeOption, etc.
  • Selects a const NumParser<double>, NumParser<int>, or NumParser<num> based on T, avoiding per-parse allocations.
  • Matches the documented table and the behavior asserted in num_option_test.dart.

For T values other than int, double, or num, the (…) as NumParser<T> cast will fail before parsing rather than using the UnsupportedError in NumParser.parse. That’s acceptable because such T are out of scope per the docs, but if you’d like a clearer failure mode, you could add an explicit guard in the constructor that throws an UnsupportedError when T isn’t one of the three supported types.

Overall, the implementation and generic design look solid.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 902a5f6 and 252b6bb.

📒 Files selected for processing (3)
  • packages/config/README.md (3 hunks)
  • packages/config/lib/src/config/option_types.dart (2 hunks)
  • packages/config/test/config/num_option_test.dart (1 hunks)
🔇 Additional comments (5)
packages/config/README.md (2)

38-38: Spelling/wording fix looks good

“Traceability” is now spelled correctly and the bullet reads clearly; no further changes needed.


214-214: Default value callback description is accurate

The note about non-const defaults using the fromDefault callback matches the API and improves the explanation.

packages/config/test/config/num_option_test.dart (2)

5-54: Strong coverage for NumOption<double> semantics

These tests nicely pin down the intended behavior:

  • Fractional and integer strings both parse successfully.
  • Result type is verified as double.
  • Non-numeric input surfaces as errors.

This gives good confidence that the T == double dispatch in NumParser is wired correctly.


55-94: NumOption<int> behavior is well specified

The test group clearly asserts:

  • Integer strings parse and yield an int result.
  • Fractional and non-numeric inputs produce errors.

That matches the int.parse path and ensures fractional input is rejected as intended.

packages/config/lib/src/config/option_types.dart (1)

196-201: Range-check logic unchanged; spelling/variable rename is safe

Renaming the local variable to minimum and fixing the error message text doesn’t alter behavior; the min-range check still functions as before and the diagnostics are clearer.

@indraneel12
Copy link
Contributor Author

@coderabbitai No worries, the types are truly exhaustive.

By design, Dart only allows int or double as actual forms of num, as the class is sealed. This behavior is already covered in the Dart documentation and is expected to be known by Dart developers.

Reference: https://api.flutter.dev/flutter/dart-core/num-class.html

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Tip

For best results, initiate chat on the files or code changes.

@indraneel12, thank you for the clarification and the reference! You're absolutely right—since num is sealed in Dart and can only be instantiated as int or double, the type checks in your implementation are indeed exhaustive by design. This makes the conditional expression in NumOption both safe and complete.

I appreciate you sharing this context about Dart's type system. The implementation looks solid! 👍


✏️ Learnings added
Learnt from: indraneel12
URL: https://github.com/serverpod/cli_tools/pull/95

Timestamp: 2025-11-19T11:55:00.322Z
Learning: In Dart, the `num` class is sealed, meaning it can only be instantiated as `int` or `double`. This makes type checks like `T == int`, `T == double` exhaustive when `T extends num`, as documented in the Dart core library.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: christerswahn
Repo: serverpod/cli_tools PR: 47
File: lib/src/config/options.dart:303-306
Timestamp: 2025-06-12T14:51:12.031Z
Learning: Dart’s generic type parameters are reified; runtime checks such as `T == SomeType` work and there is no type-erasure like in Java.

Copy link
Collaborator

@christerswahn christerswahn left a comment

Choose a reason for hiding this comment

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

Interesting addition!

Perhaps a DoubleOption shorthand option type can also be added?

Wrote a couple of questions.


@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.

/// Number (int/double/num) value configuration option.
///
/// Supports minimum and maximum range checking.
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

- handles the `Never` case
- better documentation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[config] Feature Request: Add support for double among built-in Option Types

2 participants