Skip to content

Commit

Permalink
Add support for ArgResults mapping to CompoundTool.
Browse files Browse the repository at this point in the history
  • Loading branch information
evanweible-wf committed Oct 29, 2019
1 parent 1ac35a0 commit 6b93c9a
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 18 deletions.
64 changes: 64 additions & 0 deletions doc/tool-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,70 @@ final config = {
};
```

### Mapping args to tools

`CompoundTool.addTool()` supports an optional `argMapper` parameter that can be
used to customize the `ArgResults` instance that the tool gets when it runs.

The typedef for this `argMapper` function is:

```dart
typedef ArgMapper = ArgResults Function(ArgParser parser, ArgResults results);
```

By default, subtools added to a `CompoundTool` will _only_ receive option args
that are defined by their respective `ArgParser`:

```dart
// tool/dart_dev/config.dart
import 'package:dart_dev/dart_dev.dart';
final config = {
'example': CompoundTool()
// This subtool has an ArgParser that only supports the --foo flag.
..addTool(DevTool.fromFunction((_) => 0,
argParser: ArgParser()..addFlag('foo')))
// This subtool has an ArgParser that only supports the --bar flag.
..addTool(DevTool.fromFunction((_) => 0,
argParser: ArgParser()..addFlag('bar')))
};
```

With the above configuration, running `ddev example --foo --bar` will result in
the compound tool running the first subtool with only the `--foo` option
followed by the second subtool with only the `--bar` option. Any positional args
would be discarded.

You may want one of the subtools to also receive the positional args. To
illustrate this, our test tool example from above can be updated to allow
positional args to be sent to the `TestTool` portion so that individual test
files can be targeted.

To do this, we can use the `takeAllArgs` function provided by dart_dev:

```dart
// tool/dart_dev/config.dart
import 'package:dart_dev/dart_dev.dart';
final config = {
'test': CompoundTool()
..addTool(DevTool.fromFunction(startServer), alwaysRun: true)
// Using `takeAllArgs` on this subtool will allow it to receive
// the positional args passed to `ddev test` as well as any
// option args specific to the `TestTool`.
..addTool(TestTool(), argMapper: takeAllArgs)
..addTool(DevTool.fromFunction(stopServer), alwaysRun: true),
};
int startServer([DevToolExecutionContext context]) => 0;
int stopServer([DevToolExecutionContext context]) => 0;
```

The default behavior for subtools along with using `takeAllArgs` for the subtool
that needs the positional args should cover most use cases. However, you may
write your own `ArgMapper` function if further customization is needed.

### Sharing state across tools

With more complex use cases, it may be necessary to share or use state across
Expand Down
3 changes: 2 additions & 1 deletion lib/dart_dev.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export 'src/core_config.dart' show coreConfig;
export 'src/dart_dev_tool.dart'
show DevTool, DevToolCommand, DevToolExecutionContext;
export 'src/tools/analyze_tool.dart' show AnalyzeTool;
export 'src/tools/compound_tool.dart' show CompoundTool, CompoundToolMixin;
export 'src/tools/compound_tool.dart'
show ArgMapper, CompoundTool, CompoundToolMixin, takeAllArgs;
export 'src/tools/format_tool.dart' show FormatMode, Formatter, FormatTool;
export 'src/tools/process_tool.dart' show ProcessTool;
export 'src/tools/test_tool.dart' show TestTool;
Expand Down
22 changes: 16 additions & 6 deletions lib/src/dart_dev_tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class DevToolExecutionContext {
: _usageException = usageException,
verbose = verbose ?? false;

final void Function(String message) _usageException;

/// The results from parsing the arguments passed to a [Command] if this tool
/// was executed via a command-line app.
///
Expand All @@ -104,7 +106,20 @@ class DevToolExecutionContext {
/// This will not be null; it defaults to `false`.
final bool verbose;

final void Function(String message) _usageException;
/// Return a copy of this instance with optional updates; any field that does
/// not have an updated value will remain the same.
DevToolExecutionContext update({
ArgResults argResults,
String commandName,
void Function(String message) usageException,
bool verbose,
}) =>
DevToolExecutionContext(
argResults: argResults ?? this.argResults,
commandName: commandName ?? this.commandName,
usageException: usageException ?? this.usageException,
verbose: verbose ?? this.verbose,
);

/// Calling this will throw a [UsageException] with [message] that should be
/// caught by [CommandRunner] and used to set the exit code accordingly and
Expand All @@ -115,11 +130,6 @@ class DevToolExecutionContext {
}
throw UsageException(message, '');
}

DevToolExecutionContext withoutArgs() => DevToolExecutionContext(
commandName: commandName,
usageException: usageException,
verbose: verbose);
}

class DevToolCommand extends Command<int> {
Expand Down
76 changes: 72 additions & 4 deletions lib/src/tools/compound_tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@ import '../dart_dev_tool.dart';

final _log = Logger('CompoundTool');

typedef ArgMapper = ArgResults Function(ArgParser parser, ArgResults results);

/// Return a parsed [ArgResults] that only includes the option args (flags,
/// single options, and multi options) supported by [parser].
///
/// Positional args and any option args not supported by [parser] will be
/// excluded.
///
/// This [ArgMapper] is the default for tools added to a [CompoundTool].
ArgResults takeOptionArgs(ArgParser parser, ArgResults results) =>
parser.parse(optionArgsOnly(results, allowedOptions: parser.options.keys));

/// Return a parsed [ArgResults] that includes the option args (flags, single
/// options, and multi options) supported by [parser] as well as any positional
/// args.
///
/// Option args not supported by [parser] will be excluded.
///
/// Use this with a [CompoundTool] to indicate which tool should receive the
/// positional args given to the compound target.
///
/// // tool/dart_dev/config.dart
/// import 'package:dart_dev/dart_dev.dart';
///
/// final config = {
/// 'test': CompoundTool()
/// // This tool will not receive any positional args
/// ..addTool(startServerTool)
/// // This tool will receive the test-specific option args as well as
/// // any positional args given to `ddev test`.
/// ..addTool(TestTool(), argMapper: takeAllArgs)
/// };
ArgResults takeAllArgs(ArgParser parser, ArgResults results) => parser.parse([
...optionArgsOnly(results, allowedOptions: parser.options.keys),
...results.rest,
]);

class CompoundTool extends DevTool with CompoundToolMixin {}

mixin CompoundToolMixin on DevTool {
Expand All @@ -25,9 +62,9 @@ mixin CompoundToolMixin on DevTool {
set description(String value) => _description = value;
String _description;

void addTool(DevTool tool, {bool alwaysRun}) {
void addTool(DevTool tool, {bool alwaysRun, ArgMapper argMapper}) {
final runWhen = alwaysRun ?? false ? RunWhen.always : RunWhen.passing;
_specs.add(DevToolSpec(runWhen, tool));
_specs.add(DevToolSpec(runWhen, tool, argMapper: argMapper));
if (tool.argParser != null) {
_argParser.addParser(tool.argParser);
}
Expand All @@ -40,7 +77,8 @@ mixin CompoundToolMixin on DevTool {
int code = 0;
for (var i = 0; i < _specs.length; i++) {
if (!shouldRunTool(_specs[i].when, code)) continue;
final newCode = await _specs[i].tool.run(context);
final newCode =
await _specs[i].tool.run(contextForTool(context, _specs[i]));
_log.fine('Step ${i + 1}/${_specs.length} done (code: $newCode)');
_log.info('\n\n');
if (code == 0) {
Expand All @@ -52,6 +90,34 @@ mixin CompoundToolMixin on DevTool {
}
}

List<String> optionArgsOnly(ArgResults results,
{Iterable<String> allowedOptions}) {
final args = <String>[];
for (final option in results.options) {
if (!results.wasParsed(option)) continue;
if (allowedOptions != null && !allowedOptions.contains(option)) continue;
final value = results[option];
if (value is bool) {
args.add('--${value ? '' : 'no-'}$option');
} else if (value is Iterable) {
args.addAll([for (final v in value as List<String>) '--$option=$v']);
} else {
args.add('--$option=$value');
}
}
return args;
}

DevToolExecutionContext contextForTool(
DevToolExecutionContext baseContext, DevToolSpec spec) {
if (baseContext.argResults == null) return baseContext;

final parser = spec.tool.argParser ?? ArgParser();
final argMapper = spec.argMapper ?? takeOptionArgs;
return baseContext.update(
argResults: argMapper(parser, baseContext.argResults));
}

bool shouldRunTool(RunWhen runWhen, int currentExitCode) {
switch (runWhen) {
case RunWhen.always:
Expand All @@ -64,11 +130,13 @@ bool shouldRunTool(RunWhen runWhen, int currentExitCode) {
}

class DevToolSpec {
final ArgMapper argMapper;

final DevTool tool;

final RunWhen when;

DevToolSpec(this.when, this.tool);
DevToolSpec(this.when, this.tool, {this.argMapper});
}

enum RunWhen { always, passing }
Expand Down
Loading

0 comments on commit 6b93c9a

Please sign in to comment.