v0.1.0
diff --git a/README.md b/README.md
index 02fae67..b34bf3e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,59 @@
-# when.dart
+when [![pub package](http://img.shields.io/pub/v/when.svg)](https://pub.dartlang.org/packages/when) [![Build Status](https://drone.io/github.com/seaneagan/when/status.png)](https://drone.io/github.com/seaneagan/when/latest)
+====
+
+It's often useful to provide sync (convenient) and async (concurrent) versions 
+of the same API.  `dart:io` does this with many APIs including [Process.run][] 
+and [Process.runSync][].  Since the sync and async versions do the same thing, 
+much of the logic is the same, with just a few small bits differing in their 
+sync vs. async implementation.
+
+The `when` function allows for registering `onSuccess`, `onError`, and 
+`onComplete` callbacks on another callback which represents that sync/async 
+dependent part of the API.  If the callback is sync (returns a non-`Future` or 
+throws), then the other callbacks are invoked synchronously, otherwise the 
+other callbacks are registered on the returned `Future`.
+
+For example, here's how it can be used to implement sync and async APIs for
+reading a JSON data structure from the file system with file absence handling:
+
+```dart
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:when/when.dart';
+
+/// Reads and decodes JSON from [path] asynchronously.
+///
+/// If [path] does not exist, returns the result of calling [onAbsent].
+Future readJsonFile(String path, {onAbsent()}) => _readJsonFile(
+    path, onAbsent, (file) => file.exists(), (file) => file.readAsString());
+
+/// Reads and decodes JSON from [path] synchronously.
+///
+/// If [path] does not exist, returns the result of calling [onAbsent].
+readJsonFileSync(String path, {onAbsent()}) => _readJsonFile(
+    path, onAbsent, (file) => file.existsSync(),
+    (file) => file.readAsStringSync());
+
+_readJsonFile(String path, onAbsent(), exists(File file), read(File file)) {
+  var file = new File(path);
+  return when(
+      () => exists(file),
+      (doesExist) => doesExist ?
+          when(() => read(file), JSON.decode) :
+          onAbsent());
+}
+
+main() {
+  var syncJson = readJsonFileSync('foo.json', onAbsent: () => {'foo': 'bar'});
+  print('Sync json: $syncJson');
+  readJsonFile('foo.json', onAbsent: () => {'foo': 'bar'}).then((asyncJson) {
+    print('Async json: $asyncJson');
+  });
+}
+
+```
+
+[Process.run]: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart:io.Process#id_run
+[Process.runSync]: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart:io.Process#id_runSync
diff --git a/drone.sh b/drone.sh
new file mode 100644
index 0000000..9a7f6fe
--- /dev/null
+++ b/drone.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -o xtrace
+
+pub get
+dart test/test_when.dart
+
+# TODO: dartanalyzer on all libraries
+
+# Install dart_coveralls; gather and send coverage data.
+if [ "$REPO_TOKEN" ]; then
+  export PATH="$PATH":"~/.pub-cache/bin"
+
+  echo
+  echo "Installing dart_coveralls"
+  pub global activate dart_coveralls
+
+  echo
+  echo "Running code coverage report"
+  # --debug for verbose logging
+  pub global run dart_coveralls report --token $REPO_TOKEN --retry 3 test/all_tests.dart
+fi
\ No newline at end of file
diff --git a/example/foo.json b/example/foo.json
new file mode 100644
index 0000000..1843818
--- /dev/null
+++ b/example/foo.json
@@ -0,0 +1,3 @@
+{
+  "some json": "from foo.json"
+}
\ No newline at end of file
diff --git a/example/read_json_file.dart b/example/read_json_file.dart
new file mode 100644
index 0000000..2b01b2b
--- /dev/null
+++ b/example/read_json_file.dart
@@ -0,0 +1,38 @@
+
+library when.example.read_json_file;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:when/when.dart';
+
+/// Reads and decodes JSON from [path] asynchronously.
+///
+/// If [path] does not exist, returns the result of calling [onAbsent].
+Future readJsonFile(String path, {onAbsent()}) => _readJsonFile(
+    path, onAbsent, (file) => file.exists(), (file) => file.readAsString());
+
+/// Reads and decodes JSON from [path] synchronously.
+///
+/// If [path] does not exist, returns the result of calling [onAbsent].
+readJsonFileSync(String path, {onAbsent()}) => _readJsonFile(
+    path, onAbsent, (file) => file.existsSync(),
+    (file) => file.readAsStringSync());
+
+_readJsonFile(String path, onAbsent(), exists(File file), read(File file)) {
+  var file = new File(path);
+  return when(
+      () => exists(file),
+      (doesExist) => doesExist ?
+          when(() => read(file), JSON.decode) :
+          onAbsent());
+}
+
+main() {
+  var syncJson = readJsonFileSync('foo.json', onAbsent: () => {'foo': 'bar'});
+  print('Sync json: $syncJson');
+  readJsonFile('foo.json', onAbsent: () => {'foo': 'bar'}).then((asyncJson) {
+    print('Async json: $asyncJson');
+  });
+}
diff --git a/lib/when.dart b/lib/when.dart
new file mode 100644
index 0000000..6c3b6c8
--- /dev/null
+++ b/lib/when.dart
@@ -0,0 +1,58 @@
+
+library when;
+
+import 'dart:async';
+
+/// Registers callbacks on the result of a [callback], which may or may not be
+/// a [Future].
+///
+/// If [callback] returns a future, any of [onSuccess], [onError], or
+/// [onComplete] that are provided are registered on the future,
+/// and the resulting future is returned.
+///
+/// Otherwise, if [callback] did not throw, [onSuccess] is called with the
+/// result of [callback], and the return value of [onSuccess] is captured.
+///
+/// Otherwise, if [onError] was provided, it is called.  It can take either
+/// just an error, or a stack trace as well.  If [onError] was not provided,
+/// the error is thrown not caught.
+///
+/// [onComplete] is then called synchronously.
+///
+/// The captured value is then returned.
+when(callback, onSuccess(result), {onError, onComplete}) {
+  var result, hasResult = false;
+
+  try {
+    result = callback();
+    hasResult = true;
+  } catch (e, s) {
+    if (onError != null) {
+      if (onError is _Unary) {
+        onError(e);
+      } else if (onError is _Binary) {
+        onError(e, s);
+      } else {
+        throw new ArgumentError(
+            '"onError" must accept 1 or 2 arguments: $onError');
+      }
+    } else {
+      rethrow;
+    }
+  } finally {
+    if (result is Future) {
+      result = result.then(onSuccess, onError: onError);
+      if (onComplete != null) result = result.whenComplete(onComplete);
+    } else {
+      if (hasResult) {
+        result = onSuccess(result);
+      }
+      if (onComplete != null) onComplete();
+    }
+  }
+
+  return result;
+}
+
+typedef _Unary(x);
+typedef _Binary(x, y);
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..f904287
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,7 @@
+name: when
+version: 0.1.0
+author: Sean Eagan <seaneagan1@gmail.com>
+description: Register callbacks on code which is conditionally sync or async.
+homepage: https://github.com/seaneagan/when.dart
+dev_dependencies:
+  unittest: '>=0.11.4 <0.12.0'
diff --git a/test/test_when.dart b/test/test_when.dart
new file mode 100644
index 0000000..c95ffa6
--- /dev/null
+++ b/test/test_when.dart
@@ -0,0 +1,180 @@
+
+library when.test;
+
+import 'dart:async';
+
+import 'package:unittest/unittest.dart';
+import 'package:when/when.dart';
+
+main() {
+  group('when', () {
+
+    test('on non-Future callback result should call onSuccess with result, then onComplete, and return onSuccess result', () {
+      var onSuccessCalled = false;
+      var onErrorCalled = false;
+      var onCompleteCalled = false;
+      var ret = when(
+          () => 5,
+          (x) {
+            expect(x, 5);
+            onSuccessCalled = true;
+            return 10;
+          },
+          onError: (e) => onErrorCalled = true,
+          onComplete: () {
+            expect(onSuccessCalled, isTrue);
+            onCompleteCalled = true;
+          });
+      expect(onErrorCalled, isFalse);
+      expect(onCompleteCalled, isTrue);
+      expect(ret, 10);
+    });
+
+    test('on callback failure should call onError with error, then onComplete', () {
+      var onSuccessCalled = false;
+      var onErrorCalled = false;
+      var onCompleteCalled = false;
+      var ret = when(
+          () => throw 'e',
+          (_) => onSuccessCalled = true,
+          onError: (e) {
+            expect(e, 'e');
+            onErrorCalled = true;
+          },
+          onComplete: () {
+            expect(onErrorCalled, isTrue);
+            onCompleteCalled = true;
+          });
+      expect(onSuccessCalled, isFalse);
+      expect(onCompleteCalled, isTrue);
+      expect(ret, isNull);
+    });
+
+    test('should pass stack trace to onError if binary', () {
+      var onErrorCalled = false;
+      when(
+          () => throw 'e',
+          (_) {},
+          onError: (e, s) {
+            onErrorCalled = true;
+            expect(s, isNotNull);
+          });
+      expect(onErrorCalled, isTrue);
+    });
+
+    test('should throw callback error if no onError provided', () {
+      try {
+        when(
+            () => throw 'e',
+            (_) {});
+        fail('callback error was swallowed');
+      } catch (e) {
+        expect(e, 'e');
+      }
+    });
+
+    test('should not swallow onComplete error', () {
+      try {
+        when(
+            () {},
+            (_) {},
+            onComplete: () => throw 'e');
+        fail('onComplete error was swallowed');
+      } catch (e) {
+        expect(e, 'e');
+      }
+    });
+
+    group('on Future callback result', () {
+
+      test('which succeeds should call onSuccess with result, then onComplete, and complete with onSuccess result', () {
+        var onSuccessCalled = false;
+        var onErrorCalled = false;
+        var onCompleteCalled = false;
+        var result = when(
+            () => new Future.value(5),
+            (x) {
+              expect(x, 5);
+              onSuccessCalled = true;
+              return 10;
+            },
+            onError: (e) => onErrorCalled = true,
+            onComplete: () {
+              expect(onSuccessCalled, isTrue);
+              onCompleteCalled = true;
+            });
+        expect(onSuccessCalled, isFalse);
+        expect(onCompleteCalled, isFalse);
+        expect(result, new isInstanceOf<Future>());
+        return result.then((ret) {
+          expect(onErrorCalled, isFalse);
+          expect(onCompleteCalled, isTrue);
+          expect(ret, 10);
+        });
+      });
+
+      test('which fails should call onError with error, then onComplete', () {
+        var onSuccessCalled = false;
+        var onErrorCalled = false;
+        var onCompleteCalled = false;
+        var result = when(
+            () => new Future.error('e'),
+            (_) => onSuccessCalled = true,
+            onError: (e) {
+              expect(e, 'e');
+              onErrorCalled = true;
+            },
+            onComplete: () {
+              onErrorCalled = true;
+              onCompleteCalled = true;
+            });
+        expect(onErrorCalled, isFalse);
+        expect(onCompleteCalled, isFalse);
+        expect(result, new isInstanceOf<Future>());
+        return result.then((ret) {
+          expect(ret, isNull);
+          expect(onSuccessCalled, isFalse);
+          expect(onCompleteCalled, isTrue);
+        });
+      });
+
+      test('should pass stack trace to onError if binary', () {
+        var onErrorCalled = false;
+        return when(
+            () => new Future.error('e'),
+            (_) {},
+            onError: (e, s) {
+              onErrorCalled = true;
+              // TODO: Why is the stack trace null?
+              // expect(s, isNotNull);
+        }).then((_) {
+          expect(onErrorCalled, isTrue);
+        });
+      });
+
+      test('should throw callback error if no onError provided', () {
+        return when(
+            () => new Future.error('e'),
+            (x) {}
+        ).then((_) {
+          fail('callback error was swallowed');
+        }, onError: (e) {
+          expect(e, 'e');
+        });
+      });
+
+      test('should not swallow onComplete error', () {
+        return when(
+            () => new Future.value(),
+            (x) {},
+            onComplete: () => throw 'e')
+            .then((ret) {
+              fail('onComplete error was swallowed');
+            }, onError: (e) {
+              expect(e, 'e');
+            });
+      });
+
+    });
+  });
+}
\ No newline at end of file