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