Skip to content
Merged
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
2 changes: 1 addition & 1 deletion example/config_file_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class TimeSeriesCommand extends BetterCommand<TimeSeriesOption, void> {

@override
Configuration<TimeSeriesOption> resolveConfiguration(ArgResults? argResults) {
return Configuration.resolve(
return Configuration.resolveNoExcept(
options: options,
argResults: argResults,
env: envVariables,
Expand Down
3 changes: 2 additions & 1 deletion lib/src/better_command_runner/better_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io' show Platform;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:cli_tools/config.dart';
import 'package:cli_tools/src/config/output_formatting.dart';

import 'better_command_runner.dart';

Expand Down Expand Up @@ -121,7 +122,7 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// This method can be overridden to change the configuration resolution
/// behavior.
Configuration<O> resolveConfiguration(ArgResults? argResults) {
return Configuration.resolve(
return Configuration.resolveNoExcept(
options: options,
argResults: argResults,
env: envVariables,
Expand Down
15 changes: 2 additions & 13 deletions lib/src/better_command_runner/better_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io' show Platform;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:cli_tools/config.dart';
import 'package:cli_tools/src/config/output_formatting.dart';

/// A function type for executing code before running a command.
typedef OnBeforeRunCommand = Future<void> Function(BetterCommandRunner runner);
Expand Down Expand Up @@ -308,7 +309,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// This method can be overridden to change the configuration resolution
/// behavior.
Configuration<O> resolveConfiguration(ArgResults? argResults) {
return Configuration.resolve(
return Configuration.resolveNoExcept(
options: _globalOptions,
argResults: argResults,
env: envVariables,
Expand Down Expand Up @@ -383,15 +384,3 @@ abstract class BetterCommandRunnerAnalyticsEvents {

/// An enum for the command runner log levels.
enum CommandRunnerLogLevel { quiet, verbose, normal }

/// Formats a configuration error message.
String formatConfigError(final String error) {
if (error.isEmpty) return error;
final suffix = _isPunctuation(error.substring(error.length - 1)) ? '' : '.';
return '${error[0].toUpperCase()}${error.substring(1)}$suffix';
}

/// Returns true if the character is a punctuation mark.
bool _isPunctuation(final String char) {
return RegExp(r'\p{P}', unicode: true).hasMatch(char);
}
8 changes: 0 additions & 8 deletions lib/src/config/config_parser.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:io' show Platform;

import 'package:args/args.dart';
import 'package:args/command_runner.dart' show UsageException;

import 'configuration.dart';
import 'configuration_broker.dart';
Expand Down Expand Up @@ -251,13 +250,6 @@ class ConfigParser implements ArgParser {
ignoreUnexpectedPositionalArgs: true,
);

if (configuration.errors.isNotEmpty) {
throw UsageException(
configuration.errors.join('\n'),
usage,
);
}

_invokeCallbacks(configuration);

return ConfigResults(
Expand Down
48 changes: 45 additions & 3 deletions lib/src/config/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'configuration_broker.dart';
import 'exceptions.dart';
import 'options.dart';
import 'option_resolution.dart';
import 'output_formatting.dart';
import 'source_type.dart';

/// A configuration object that holds the values for a set of configuration options.
Expand Down Expand Up @@ -51,7 +52,7 @@ class Configuration<O extends OptionDefinition> {
/// instead the caller is responsible for checking if [errors] is non-empty.
Configuration.fromValues({
required final Map<O, Object?> values,
}) : this.resolve(
}) : this.resolveNoExcept(
options: values.keys,
presetValues: values,
);
Expand All @@ -65,16 +66,18 @@ class Configuration<O extends OptionDefinition> {
_config = Map.from(configuration._config),
_errors = List.from(configuration._errors);

/// {@template Configuration.resolveNoExcept}
/// Creates a configuration with option values resolved from the provided context.
///
/// [argResults] is used if provided. Otherwise [args] is used if provided.
///
/// If [presetValues] is provided, the values present will override the other sources,
/// including if they are null.
/// {@endtemplate}
///
/// This does not throw upon value parsing or validation errors,
/// instead the caller is responsible for checking if [errors] is non-empty.
Configuration.resolve({
Configuration.resolveNoExcept({
required final Iterable<O> options,
ArgResults? argResults,
final Iterable<String>? args,
Expand Down Expand Up @@ -109,6 +112,45 @@ class Configuration<O extends OptionDefinition> {
);
}

/// {@macro Configuration.resolveNoExcept}
///
/// Throws a [UsageException] with error and correct usage information
/// if there were any errors during configuration resolution.
/// A caller can use [resolveNoExcept] instead to handle the errors themselves.
factory Configuration.resolve({
required final Iterable<O> options,
final ArgResults? argResults,
final Iterable<String>? args,
final Map<String, String>? env,
final ConfigurationBroker? configBroker,
final Map<O, Object?>? presetValues,
final bool ignoreUnexpectedPositionalArgs = false,
}) {
final config = Configuration.resolveNoExcept(
options: options,
argResults: argResults,
args: args,
env: env,
configBroker: configBroker,
presetValues: presetValues,
ignoreUnexpectedPositionalArgs: ignoreUnexpectedPositionalArgs,
);
config.throwExceptionOnErrors();
return config;
}

/// Throws a [UsageException] with error and correct usage information
/// if there were any errors during configuration resolution.
/// Can be overridden to change the exception or its content.
void throwExceptionOnErrors() {
if (_errors.isNotEmpty) {
final buffer = StringBuffer();
final errors = _errors.map(formatConfigError);
buffer.writeAll(errors, '\n');
throw UsageException(buffer.toString(), usage);
}
}

/// Returns the usage help text for the options of this configuration.
String get usage => _options.usage;

Expand Down Expand Up @@ -222,7 +264,7 @@ class Configuration<O extends OptionDefinition> {
final resolution = _config[option];

if (resolution == null) {
throw InvalidOptionConfigurationError(option,
throw OptionDefinitionError(option,
'Out-of-order dependency on not-yet-resolved ${option.qualifiedString()}');
}

Expand Down
8 changes: 4 additions & 4 deletions lib/src/config/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import 'package:cli_tools/src/config/options.dart';

/// Indicates that the option definition is invalid.
class InvalidOptionConfigurationError extends Error {
class OptionDefinitionError extends Error {
final OptionDefinition option;
final String? message;

InvalidOptionConfigurationError(this.option, [this.message]);
OptionDefinitionError(this.option, [this.message]);

@override
String toString() {
return message != null
? 'Invalid configuration for ${option.qualifiedString()}: $message'
: 'Invalid configuration for ${option.qualifiedString()}';
? 'Invalid definition for ${option.qualifiedString()}: $message'
: 'Invalid definition for ${option.qualifiedString()}';
}
}
2 changes: 1 addition & 1 deletion lib/src/config/option_groups.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class MutuallyExclusive extends OptionGroup {

for (final opt in options) {
if (opt.option.defaultValue() != null) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
opt,
'Option group `$name` does not allow defaults',
);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/config/option_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ class DurationParser extends ValueParser<Duration> {
@override
Duration parse(final String value) {
// integer followed by an optional unit (s, m, h, d, ms, us)
const pattern = r'^(-?\d+)([smhd]|ms|us)?$';
const pattern = r'^(-?\d+)(ms|us|[smhd])?$';
final regex = RegExp(pattern);
final match = regex.firstMatch(value);

Expand Down
14 changes: 7 additions & 7 deletions lib/src/config/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,12 @@ abstract class ConfigOptionBase<V> implements OptionDefinition<V> {
@mustCallSuper
void validateDefinition() {
if (argName == null && argAbbrev != null) {
throw InvalidOptionConfigurationError(this,
throw OptionDefinitionError(this,
"An argument option can't have an abbreviation but not a full name");
}

if ((fromDefault != null || defaultsTo != null) && mandatory) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
this, "Mandatory options can't have default values");
}
}
Expand Down Expand Up @@ -733,7 +733,7 @@ Iterable<OptionDefinition> validateOptions(
final argName = opt.option.argName;
if (argName != null) {
if (argNameOpts.containsKey(opt.option.argName)) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
opt, 'Duplicate argument name: ${opt.option.argName} for $opt');
}
argNameOpts[argName] = opt;
Expand All @@ -742,7 +742,7 @@ Iterable<OptionDefinition> validateOptions(
final argPos = opt.option.argPos;
if (argPos != null) {
if (argPosOpts.containsKey(opt.option.argPos)) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
opt, 'Duplicate argument position: ${opt.option.argPos} for $opt');
}
argPosOpts[argPos] = opt;
Expand All @@ -751,7 +751,7 @@ Iterable<OptionDefinition> validateOptions(
final envName = opt.option.envName;
if (envName != null) {
if (envNameOpts.containsKey(opt.option.envName)) {
throw InvalidOptionConfigurationError(opt,
throw OptionDefinitionError(opt,
'Duplicate environment variable name: ${opt.option.envName} for $opt');
}
envNameOpts[envName] = opt;
Expand All @@ -776,14 +776,14 @@ Iterable<OptionDefinition> validateOptions(
(final a, final b) => a.option.argPos!.compareTo(b.option.argPos!));

if (orderedPosOpts.first.option.argPos != 0) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
orderedPosOpts.first,
'First positional argument must have index 0.',
);
}

if (orderedPosOpts.last.option.argPos != orderedPosOpts.length - 1) {
throw InvalidOptionConfigurationError(
throw OptionDefinitionError(
orderedPosOpts.last,
'The positional arguments must have consecutive indices without gaps.',
);
Expand Down
11 changes: 11 additions & 0 deletions lib/src/config/output_formatting.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Formats a configuration error message.
String formatConfigError(final String error) {
if (error.isEmpty) return error;
final suffix = _isPunctuation(error.substring(error.length - 1)) ? '' : '.';
return '${error[0].toUpperCase()}${error.substring(1)}$suffix';
}

/// Returns true if the character is a punctuation mark.
bool _isPunctuation(final String char) {
return RegExp(r'\p{P}', unicode: true).hasMatch(char);
}
14 changes: 7 additions & 7 deletions test/config/config_source_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void main() {
test(
'when the YAML content option has data '
'then the correct value is retrieved', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--yaml-content',
Expand All @@ -65,7 +65,7 @@ project:
test(
'when the JSON content option has data '
'then the correct value is retrieved', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--json-content',
Expand All @@ -88,7 +88,7 @@ project:
test(
'when the YAML content option has data of the wrong type '
'then an appropriate error is registered', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--yaml-content',
Expand Down Expand Up @@ -117,7 +117,7 @@ project:
test(
'when the JSON content option has data of the wrong type '
'then an appropriate error is registered', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--json-content',
Expand Down Expand Up @@ -149,7 +149,7 @@ project:
test(
'when the YAML content option has malformed data '
'then an appropriate error is registered', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--yaml-content',
Expand All @@ -176,7 +176,7 @@ projectId:123
test(
'when the JSON content option has malformed data '
'then an appropriate error is registered', () async {
final config = Configuration.resolve(
final config = Configuration.resolveNoExcept(
options: options,
args: [
'--json-content',
Expand Down Expand Up @@ -242,7 +242,7 @@ projectId:123
'when creating the configuration '
'then the expected errors are registered', () async {
expect(
() => Configuration.resolve(
() => Configuration.resolveNoExcept(
options: options,
configBroker: configSource,
),
Expand Down
Loading