acyclic_steps library

The package:acyclic_steps/acyclic_steps.dart library enables the definition and execution of acyclic graphs of dependent steps.

A Step is a function that produces a result, and which may depend on results produced by other steps. The result of a Step can be computed using a Runner which caches the result of the step and all dependent steps.

Example using acyclic steps

import 'dart:io';
import 'dart:typed_data';
import 'package:acyclic_steps/acyclic_steps.dart';

/// A [Step] that provides a port we can listen on, this is a _virtual step_
/// because it doesn't have an implementation instead it throws an error. Hence,
/// to evaluate a ste that depends on [portStep] it is necessary to override
/// this step by injecting a value to replace it.
final Step<int> portStep = Step.define('port').build(() {
  throw UnimplementedError('port must be overriden with input');
});

/// A [Step] that loads content to be served from disk, this assumes we're
/// writing a server that always serves the same file.
final Step<Uint8List> contentSetup = Step.define('content').build(() async {
  return await File('index.html').readAsBytes();
});

/// A [Step] that sets up an [HttpServer] to listen to the port found in
/// [portStep], and serves content loaded by [contentSetup].
final Step<HttpServer> serverSetup = Step.define(
  'server-setup',
).dep(portStep).dep(contentSetup).build((
  port,
  content,
) async {
  // Listen to port
  final server = await HttpServer.bind(InternetAddress.anyIPv4, port);

  // Handle requests
  server.listen((request) {
    request.response
      ..statusCode = 200
      ..write(content)
      ..close();
  });
  return server;
});

Future<void> main() async {
  final r = Runner();
  // Override [portStep] to provide a port from environment variables.
  r.override(portStep, int.parse(Platform.environment['PORT']));
  // Evaluate [serverSetup] which in turn evaluates [contentSetup], and re-uses
  // the overridden value for [portStep].
  final server = await r.run(serverSetup);
  // Now we can close the server, maybe this is useful after SIGTERM
  await ProcessSignal.sigterm.watch().first;
  await server.close();

  // When testing it might be desirable to override the [contentSetup] to
  // produce content that is smaller/different from what is stored on disk.
  // To do this we must create a new runner:
  final testRunner = Runner();
  testRunner.override(portStep, 8080);
  testRunner.override(contentSetup, Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]));
  // This will create a server that listens on 8080 and serves the content
  // injected above.
  await testRunner.run(serverSetup);
  // ... run some tests ...
}

Classes

Runner
A Runner can run a Step and its dependencies, caching the results of each step.
Step<T>
A Step is a function that may depend on the result of other steps.

Typedefs

RunStepWrapper = Future<T> Function<T>(Step<T> step, Future<T> runStep())
A RunStepWrapper is a function that can wrap the runStep function.