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.

example/example.dart

/// Opening a requisition: four steps, previewed and then performed.
///
/// It writes into a temporary directory and uses a stand-in for GitHub, so it
/// runs offline and cleans up after itself.
///
/// ```bash
/// dart run example/example.dart
/// ```
library;

import 'dart:io';

import 'package:preview_executor/preview_executor.dart';

Future<void> main() async {
  final workspace = Directory.systemTemp.createTempSync('preview_executor_');
  const executor = PreviewExecutor();

  // The steps are built once, here. Whatever they need — the template's
  // contents, the resolved paths — is settled now, so previewing and performing
  // describe the same change instead of deriving it twice.
  final publish = PublishIssue(title: 'Recurring payments');
  final steps = <Step>[
    WriteFile('${workspace.path}/requisition.md', '# Recurring payments\n'),
    WriteFile('${workspace.path}/issue.yaml', 'title: Recurring payments\n'),
    publish,
    RecordIssueNumber('${workspace.path}/issue.yaml', publish),
  ];

  // ── What would happen ─────────────────────────────────────────────────────
  stdout.writeln('Plan\n');
  for (final preview in executor.preview(steps)) {
    stdout.writeln('  ${_render(preview)}');
  }

  // ── The decision belongs to the host ──────────────────────────────────────
  //
  // A CLI reads --plan / --apply and may prompt. An HTTP handler reads a
  // request parameter. The engine has no opinion, and is given the answer.
  stdout.writeln('\n(approved)\n');

  // ── Doing it ──────────────────────────────────────────────────────────────
  final execution = await executor.perform(steps);

  stdout.writeln('Applied\n');
  for (final outcome in execution.outcomes) {
    stdout.writeln('  ${outcome.verb.padRight(8)} ${outcome.target}');
  }

  stdout.writeln('\n  complete: ${execution.isComplete}');
  stdout.writeln('  faithful: ${execution.isFaithful}');
  for (final discrepancy in execution.discrepancies) {
    stdout.writeln('  ! ${discrepancy.message}');
  }

  workspace.deleteSync(recursive: true);
}

/// `create docs/requisition.md`, with the pending values spelled out.
String _render(Preview preview) {
  final line = '${preview.verb.padRight(8)} ${preview.target}';
  final notes = [
    if (preview.detail != null) preview.detail!,
    for (final name in preview.pending) '$name: known once this runs',
  ];
  return notes.isEmpty ? line : '$line  (${notes.join('; ')})';
}

// ─── Steps ──────────────────────────────────────────────────────────────────

/// Writes a file, or keeps the one that is already there.
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);
  }
}

/// Creates the issue. Its number does not exist until it has been created, so
/// the preview says so rather than staying silent.
class PublishIssue implements Step {
  PublishIssue({required this.title});

  final String title;

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

  @override
  Future<Outcome> perform(StepContext context) async {
    // Stand-in for `gh issue create`.
    const number = 41;
    return Outcome(
      verb: 'create',
      target: 'issue "$title" in ccisnedev/macss',
      values: const {'number': number},
    );
  }
}

/// Writes the issue number beside the requisition — reading it from the step
/// that produced it, rather than asking GitHub a second time.
class RecordIssueNumber implements Step {
  RecordIssueNumber(this.path, this.source);

  final String path;
  final Step source;

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

  @override
  Future<Outcome> perform(StepContext context) async {
    final number = context.outcomeOf(source).values['number'];
    File(path).writeAsStringSync('issue: $number\n', mode: FileMode.append);
    return Outcome(
      verb: 'record',
      target: path,
      values: {'number': number},
    );
  }
}
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