preview_executor 0.1.0 copy "preview_executor: ^0.1.0" to clipboard
preview_executor: ^0.1.0 copied to clipboard

Execute an ordered list of steps, each of which states what it would do before anything runs — and is held to it. Transport-neutral engine behind CLI plan/apply and HTTP dry-run.

pub package license

preview_executor #

Execute an ordered list of steps, each of which states what it would do before anything runs — and is held to it.

This is the engine behind a CLI's --plan / --apply and an HTTP endpoint's dry-run. It knows about neither: no flags, no requests, no terminal, no rendering. A host composes those around it.

Built for modular_cli_sdk and modular_api, which share the same problem from opposite ends of the wire.


The problem #

You want to show a user what a command would do, take their approval, and then do exactly that. The obvious implementation is a dryRun flag threaded down into the work:

// Don't.
void deploy({bool dryRun = false}) {
  for (final file in files) {
    if (dryRun) { print('would write $file'); continue; }
    write(file);
  }
}

It works until it doesn't. Every branch has to remember the flag, the two passes drift apart one commit at a time, and nothing anywhere notices. It is the same weakness Ansible's check_mode has: the preview is a fake run, so its faithfulness rests on the discipline of whoever last edited the function.

The shape instead #

A step says what it would do through one method and does it through another. Neither is a mode of the other, so there is no flag to forget — and because the claim and the report are separate values, they can be compared.

import 'dart:io';
import 'package:preview_executor/preview_executor.dart';

class WriteFile implements Step {
  WriteFile(this.path, this.contents);
  final String path;
  final String contents;

  @override
  Preview preview() => File(path).existsSync()
      ? Preview(verb: 'keep', target: path, detail: 'already exists')
      : Preview(verb: 'create', target: path);

  @override
  Future<Outcome> perform(StepContext context) async {
    if (File(path).existsSync()) return Outcome(verb: 'keep', target: path);
    File(path).writeAsStringSync(contents);
    return Outcome(verb: 'create', target: path);
  }
}
const executor = PreviewExecutor();

final steps = [
  WriteFile('docs/requisition.md', template),
  WriteFile('docs/issue.yaml', metadata),
];

// Ask what would happen. Nothing happens.
for (final preview in executor.preview(steps)) {
  print('  ${preview.verb}  ${preview.target}');
}
//   create  docs/requisition.md
//   keep    docs/issue.yaml

// Decide however your host decides — a prompt, a flag, an HTTP parameter.
if (!await approved()) return;

final execution = await executor.perform(steps);
print(execution.isFaithful);  // every step did what it said
print(execution.isComplete);  // every step got to run

Note what the step does not do: recompute contents. It was resolved once, when the step was built. That is what keeps the preview and the work describing the same change, instead of deriving it twice and hoping the two agree.

Values that cannot be known yet #

Some steps produce something that does not exist until they run — an issue number, a generated id, the path a download landed at. A preview that stayed silent about those would read as complete when it was not, so they are declared:

@override
Preview preview() => const Preview(
  verb: 'create',
  target: 'issue in ccisnedev/macss',
  pending: ['number', 'url'],
);

@override
Future<Outcome> perform(StepContext context) async {
  final issue = await github.createIssue(title, body);
  return Outcome(
    verb: 'create',
    target: 'issue in ccisnedev/macss',
    values: {'number': issue.number, 'url': issue.url},
  );
}

The host renders those as known once this runs. The step is then held to producing every one of them: a pending value that never arrives is a discrepancy, because the step after it is about to read something that is not there.

And that step reads it rather than guessing:

class RecordIssueNumber implements Step {
  RecordIssueNumber(this.publish);
  final Step publish;

  @override
  Preview preview() =>
      const Preview(verb: 'record', target: 'issue.yaml', pending: ['number']);

  @override
  Future<Outcome> perform(StepContext context) async {
    final number = context.outcomeOf(publish).values['number'];
    // …
  }
}

A step sees only backwards. That is what makes an ordered list sufficient and a dependency graph unnecessary: you write the steps in the order they must run, and the order is the dependency.

What the run tells you #

perform answers two questions separately, because they fail for different reasons:

isComplete Did every step get to run? A step that throws stops the list; what already ran is kept, along with where it stopped and why.
isFaithful Did every step that ran do what it said it would? A step that contradicts its own preview does not stop the list — it did do something, and later steps may depend on it.

A run can be complete but unfaithful, or incomplete but faithful. Execution, Discrepancy and StepFailure all serialise with toJson(), so a host that speaks JSON renders the same run without reformatting it.

What this package is not #

It is a deliberately small piece of what Terraform does, and it leaves out the parts that belong to Terraform's own domain rather than to the pattern:

  • No state file. Steps read the world when they preview. There is no record of what ran last time, so there is no drift and no stale state to reconcile.
  • No declarative configuration. The steps are the intent.
  • No dependency graph. An ordered list, resolved by writing it in order.
  • No saved plan you apply later. preview produces a description for a human to read now; perform re-previews immediately before it acts. Nothing is serialised and replayed, so there is no plan that can go stale.

Installation #

dart pub add preview_executor

License #

MIT — see LICENSE.

0
likes
160
points
156
downloads

Documentation

API reference

Publisher

verified publisherccisne.dev

Weekly Downloads

Execute an ordered list of steps, each of which states what it would do before anything runs — and is held to it. Transport-neutral engine behind CLI plan/apply and HTTP dry-run.

Repository (GitHub)
View/report issues

Topics

#dry-run #plan-apply #macss

License

MIT (license)

More

Packages that depend on preview_executor