From 61872ce1c6efec0435a6950fcd29816a11704ff5 Mon Sep 17 00:00:00 2001 From: jp Date: Tue, 22 Mar 2016 00:28:20 -0400 Subject: [PATCH 1/2] Implementing error handling as described in issue #35 --- ERRORS.md | 54 ++++++++++++++++++ lib/index.js | 149 +++++++++++++++++++++++++++++++++++++++++++++++--- test/index.js | 85 ++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 ERRORS.md diff --git a/ERRORS.md b/ERRORS.md new file mode 100644 index 0000000..4ba46cf --- /dev/null +++ b/ERRORS.md @@ -0,0 +1,54 @@ +# Errors in flyd + +## Ethos ++ It should not be the concern of `flyd` to handle exceptions for the user -- any `throw` should result in a hard failure. ++ Silent failures are bad (current way `flyd` handles Promise.reject) ++ Be as unopinionated in implementation as possible ++ Be functional in design ++ Be as backward compatible as possible with the current api + +## Concepts ++ The stream is of `events` ++ Each stream has a `left` and a `right` side (like an Either) ++ The right side is the domain objects ++ The left side is meta (in most cases errors) ++ By default the api operates on the `right` side + +## The Api +`s` is a stream + +### Setting data s(...) is overloaded ++ `s(value)` is the default case takes a value makes it a right and pushes it down the stream ++ `s(promise)` if the promise resolves pushes a right, otherwise pushes a left ++ `s(either)` pushes down right or left based on either.left either.right ++ `s.left(value)` sets the stream to a left of `value` + +### Getting data ++ `s()` get the last right value or throws an exception if there is a left value ++ `s.left()` get the last left value or throws an exception if there is a right value + +### Checking stream state ++ `s.isLeft()` return boolean so you know what the stream contains + +### Core functions ++ `.map()` works only on rights and ignores lefts ++ `.mapAll()` gets all events as an `Either` ++ `.combine()` and `.merge()` stay the same they work on streams ++ `ap()` works on `rights` only ++ `.scan()` works on `rights` only ++ `.on()` works on `rights` only + +### The Either implementation +There are no additional dependencies and we have provided a minimal implementation for basic use. If you plan on using `.mapAll` we recommend overriding the methods in flyd.Either. You can use [folktale/data.either](https://github.com/folktale/data.either) for example as shown below. +``` +var DE = require('data.either'); +flyd.Either.Right = DE.Right; +flyd.Either.Left = DE.Left; +flyd.Either.isEither = function(obj) { return obj instanceof DE; }; +flyd.Either.isRight = function(e) { return e.isRight; }; +flyd.Either.getRight = function(e) { return e.value; }; +flyd.Either.getLeft = function(e) { return e.value; }; +``` + +### Other functionality +Keeping with the ethos of flyd any further functions like `.swap` or `.onAll` should be implemented as modules. \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index d449d7e..cfc2b9c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,6 +6,11 @@ var curryN = require('ramda/src/curryN'); function isFunction(obj) { return !!(obj && obj.constructor && obj.call && obj.apply); } + +function isRight(n) { + return !flyd.Either.isEither(n) || flyd.Either.isRight(n); +} + function trueFn() { return true; } // Globals @@ -20,6 +25,44 @@ var flyd = {} // /////////////////////////// API ///////////////////////////////// // +/** + * Defines an Either type. + * @type {Object} + */ +flyd.Either = { + Right: function(val) { + return { + right: val, + left: undefined, + isRight: true + }; + }, + + Left: function(err) { + return { + right: undefined, + left: err, + isRight: false + }; + }, + + isEither: function(obj) { + return obj !== undefined && obj !== null && obj.hasOwnProperty('left') && obj.hasOwnProperty('right'); + }, + + isRight: function(either) { + return either.isRight; + }, + + getRight: function(either) { + return either.right; + }, + + getLeft: function(either) { + return either.left; + } +}; + /** * Creates a new stream * @@ -178,6 +221,27 @@ flyd.endsOn = function(endS, s) { */ // Library functions use self callback to accept (null, undefined) update triggers. flyd.map = curryN(2, function(f, s) { + return combine(function(s, self) { + if (isRight(s.val)) self(f(s())); + }, [s]); +}) + +/** + * Returns a new stream consisting of every value, both Lefts and Rights, from `s` + * passed through `f` + * __Signature__: `(a -> result) -> Stream a -> Stream result` + * + * @param {Function} f - the function that produces the elements of the new stream + * @param {stream} s - the stream to map + * @return {stream} - a new stream of mapped Either values + * + * @example + * var numbers = flyd.stream(flyd.Either.Right(1)); + * var doubledNumbers = numbers.mapAll(function(n) { + * return flyd.Either.Right(n.right*n.right); + * }); + */ +flyd.mapAll = curryN(2, function(f, s) { return combine(function(s, self) { self(f(s.val)); }, [s]); }) @@ -196,7 +260,9 @@ flyd.map = curryN(2, function(f, s) { * @return {stream} an empty stream (can be ended) */ flyd.on = curryN(2, function(f, s) { - return combine(function(s) { f(s.val); }, [s]); + return combine(function(s) { + if (isRight(s.val)) f(s()); + }, [s]); }) /** @@ -219,7 +285,7 @@ flyd.on = curryN(2, function(f, s) { */ flyd.scan = curryN(3, function(f, acc, s) { var ns = combine(function(s, self) { - self(acc = f(acc, s.val)); + if (isRight(s.val)) self(acc = f(acc, s())); }, [s]); if (!ns.hasVal) ns(acc); return ns; @@ -312,7 +378,7 @@ flyd.curryN = curryN /** * Returns a new stream identical to the original except every - * value will be passed through `f`. + * value will be passed through `f`. This only operates on values or Rights. * * _Note:_ This function is included in order to support the fantasy land * specification. @@ -329,6 +395,24 @@ flyd.curryN = curryN */ function boundMap(f) { return flyd.map(f, this); } +/** + * Returns a new stream identical to the original except every + * value or Either will be passed through `f`, even Lefts. + * + * __Signature__: Called bound to `Stream a`: `(a -> b) -> Stream b` + * + * @name stream.mapAll + * @param {Function} function - the function to apply + * @return {stream} a new stream with the values and Eithers mapped + * + * @example + * var numbers = flyd.stream(flyd.Either.Right(1)); + * var doubledNumbers = numbers.mapAll(function(n) { + * return flyd.Either.Right(n.right*n.right); + * }); + */ +function boundMapAll(f) { return flyd.mapAll(f, this); } + /** * Returns a new stream which is the result of applying the * functions from `this` stream to the values in `stream` parameter. @@ -353,7 +437,9 @@ function boundMap(f) { return flyd.map(f, this); } */ function ap(s2) { var s1 = this; - return combine(function(s1, s2, self) { self(s1.val(s2.val)); }, [s1, s2]); + return combine(function(s1, s2, self) { + if (isRight(s1.val) && isRight(s2.val)) self(s1()(s2())); + }, [s1, s2]); } /** @@ -365,6 +451,45 @@ function streamToString() { return 'stream(' + this.val + ')'; } +/** + * Returns true if `n` is a Left value. + * + * @name stream.isLeft + * @return {Boolean} + */ +function isLeft(n) { + return flyd.Either.isEither(n) && !flyd.Either.isRight(n); +} + +/** + * Returns true if the current value of a stream is a Left value. + * + * @name stream.isLeft + * @return {Boolean} + */ +function boundIsLeft() { + return isLeft(this.val); +} + +/** + * Returns the value in the stream if it's a not a Right, otherwise it throws a + * TypeError. + * + * @param {*} error - the error to wrap in a Left and set the stream to + * @throws {TypeError} - thrown if the value in the stream is a Right + * @return {stream} + */ +function left(n) { + if (arguments.length === 0) { + if (!isLeft(this.val)) { + throw new TypeError('Stream contains a Right where a Left is expected.'); + } + return flyd.Either.getLeft(this.val); + } + updateStreamValue(this, flyd.Either.Left(n)); + return this; +} + /** * @name stream.end * @memberof stream @@ -397,9 +522,14 @@ function streamToString() { */ function createStream() { function s(n) { - if (arguments.length === 0) return s.val - updateStreamValue(s, n) - return s + if (arguments.length === 0) { + if (flyd.Either.isEither(s.val) && isLeft(s.val)) { + throw new TypeError('Stream contains a Left where a Right is expected.'); + } + return flyd.Either.isEither(s.val) ? flyd.Either.getRight(s.val) : s.val; + } + updateStreamValue(s, n); + return s; } s.hasVal = false; s.val = undefined; @@ -411,6 +541,9 @@ function createStream() { s.ap = ap; s.of = flyd.stream; s.toString = streamToString; + s.left = left.bind(s); + s.isLeft = boundIsLeft; + s.mapAll = boundMapAll; return s; } @@ -534,7 +667,7 @@ function flushUpdate() { */ function updateStreamValue(s, n) { if (n !== undefined && n !== null && isFunction(n.then)) { - n.then(s); + n.then(s, s.left); return; } s.val = n; diff --git a/test/index.js b/test/index.js index 3829d71..1f82af8 100644 --- a/test/index.js +++ b/test/index.js @@ -6,6 +6,7 @@ var t = require('transducers.js'); var flyd = require('../lib'); var stream = flyd.stream; var combine = flyd.combine; +var Either = flyd.Either; // Some combinators function doubleFn(x) { return x() * 2; } @@ -342,6 +343,15 @@ describe('stream', function() { }, 20); })); }); + it('pushes promise.reject down the stream as and either.left', function(done) { + var s = stream(); + combine(function(s) { + assert(s.isLeft(), true); + assert.equal(s.left(), 12); + done(); + }, [s]); + s(Promise.reject(12)); + }); }); describe('on', function() { @@ -353,6 +363,14 @@ describe('stream', function() { s(1)(2); assert.deepEqual(result, [1, 2]); }); + it('is only invoked when the stream changes to a Right', function() { + var s = flyd.stream(); + var result = []; + var f = function(v) { result.push(v); }; + flyd.on(f, s); + s(Either.Right(1))(Either.Left(-1))(Either.Right(2)); + assert.deepEqual(result, [1, 2]); + }); }); describe('map', function() { @@ -405,6 +423,13 @@ describe('stream', function() { x(12); assert.equal(s1(), s2()); }); + it('only works on Rights', function() { + var result = [] + var s = stream(Either.Right(1)); + flyd.map(function(v) { result.push(v); }, s); + s(Either.Right(2))(Either.Left(-1))(Either.Right(3)); + assert.deepEqual(result, [1, 2, 3]); + }); }); describe('scan', function() { @@ -789,4 +814,64 @@ describe('stream', function() { ]); }); }); + + describe('right', function() { + it('throws if the current value in the stream is a Left', function() { + var s = stream(Either.Left(1)); + assert.throws(s, TypeError); + }); + it('can return a Right containing undefined', function() { + var s = stream(Either.Right(undefined)); + assert.equal(s(), undefined); + }) + }); + + describe('left', function() { + it('gets the current value in the stream if it is a Left', function() { + var s = stream(Either.Left(-1)); + assert.equal(s.left(), -1); + }); + it('throws if the current value in the stream is a Right', function() { + var s = stream(Either.Right(1)); + assert.throws(s.left, TypeError); + }); + it('can return a Left containing undefined', function() { + var s = stream(Either.Left(undefined)); + assert.equal(s.left(), undefined); + }); + it('can set a left value', function() { + var s = stream().left(0); + assert.throws(s, TypeError); + assert.equal(s.left(), 0); + }); + }); + + describe('isLeft', function() { + it('returns true if the value in the stream is a Left', function() { + var s = stream(Either.Left(0)); + assert.equal(s.isLeft(), true); + s(Either.Right(1)); + assert.equal(s.isLeft(), false); + }); + it('returns false if the value in the stream is a plain value', function() { + var s = stream(1); + assert.equal(s.isLeft(), false); + }); + }); + + describe('mapAll', function() { + it('works on both Lefts and Rights', function() { + var result = []; + var a = Either.Right(1); + var s = stream(a); + flyd.mapAll(function(v) { + result.push(v); + }, s); + var b = Either.Right(2); + var c = Either.Left(-1); + var d = Either.Right(3); + s(b)(c)(d); + assert.deepEqual(result, [a, b, c, d]); + }); + }); }); From a811386f496b3c71828d6f6ab1d8ed608767788b Mon Sep 17 00:00:00 2001 From: jp Date: Tue, 22 Mar 2016 11:54:34 -0400 Subject: [PATCH 2/2] changing `.mapAll()` to `.mapEither()`. Adding `.either()` and `.isRight()` --- ERRORS.md | 6 ++++-- lib/index.js | 20 ++++++++++++++------ test/index.js | 26 ++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/ERRORS.md b/ERRORS.md index 4ba46cf..e36f877 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -26,13 +26,15 @@ ### Getting data + `s()` get the last right value or throws an exception if there is a left value + `s.left()` get the last left value or throws an exception if there is a right value ++ `s.either()` get the last value out as an Either ### Checking stream state -+ `s.isLeft()` return boolean so you know what the stream contains ++ `s.isLeft()` return true if the stream contains a left value ++ `s.isRight()` return true if the stream contains a right value ### Core functions + `.map()` works only on rights and ignores lefts -+ `.mapAll()` gets all events as an `Either` ++ `.mapEither()` gets all events as an `Either` + `.combine()` and `.merge()` stay the same they work on streams + `ap()` works on `rights` only + `.scan()` works on `rights` only diff --git a/lib/index.js b/lib/index.js index cfc2b9c..0ef646e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -237,11 +237,11 @@ flyd.map = curryN(2, function(f, s) { * * @example * var numbers = flyd.stream(flyd.Either.Right(1)); - * var doubledNumbers = numbers.mapAll(function(n) { + * var doubledNumbers = numbers.mapEither(function(n) { * return flyd.Either.Right(n.right*n.right); * }); */ -flyd.mapAll = curryN(2, function(f, s) { +flyd.mapEither = curryN(2, function(f, s) { return combine(function(s, self) { self(f(s.val)); }, [s]); }) @@ -401,17 +401,17 @@ function boundMap(f) { return flyd.map(f, this); } * * __Signature__: Called bound to `Stream a`: `(a -> b) -> Stream b` * - * @name stream.mapAll + * @name stream.mapEither * @param {Function} function - the function to apply * @return {stream} a new stream with the values and Eithers mapped * * @example * var numbers = flyd.stream(flyd.Either.Right(1)); - * var doubledNumbers = numbers.mapAll(function(n) { + * var doubledNumbers = numbers.mapEither(function(n) { * return flyd.Either.Right(n.right*n.right); * }); */ -function boundMapAll(f) { return flyd.mapAll(f, this); } +function boundMapEither(f) { return flyd.mapEither(f, this); } /** * Returns a new stream which is the result of applying the @@ -471,6 +471,10 @@ function boundIsLeft() { return isLeft(this.val); } +function boundIsRight() { + return isRight(this.val); +} + /** * Returns the value in the stream if it's a not a Right, otherwise it throws a * TypeError. @@ -531,6 +535,9 @@ function createStream() { updateStreamValue(s, n); return s; } + s.either = function() { + return flyd.Either.isEither(s.val) ? s.val : flyd.Either.Right(s.val); + } s.hasVal = false; s.val = undefined; s.vals = []; @@ -543,7 +550,8 @@ function createStream() { s.toString = streamToString; s.left = left.bind(s); s.isLeft = boundIsLeft; - s.mapAll = boundMapAll; + s.isRight = boundIsRight; + s.mapEither = boundMapEither; return s; } diff --git a/test/index.js b/test/index.js index 1f82af8..04a1b01 100644 --- a/test/index.js +++ b/test/index.js @@ -859,12 +859,34 @@ describe('stream', function() { }); }); - describe('mapAll', function() { + describe('isRight', function() { + it('returns true if the value in the stream is a Right', function() { + var s = stream(Either.Left(0)); + assert.equal(s.isRight(), false); + s(Either.Right(1)); + assert.equal(s.isRight(), true); + }); + it('returns true if the value in the stream is a plain value', function() { + var s = stream(1); + assert.equal(s.isRight(), true); + }); + }); + + describe('either', function() { + it('can get stream values as Eithers', function() { + var s = stream(2); + assert.deepEqual(s.either(), Either.Right(2)); + s.left(1); + assert.deepEqual(s.either(), Either.Left(1)); + }); + }); + + describe('mapEither', function() { it('works on both Lefts and Rights', function() { var result = []; var a = Either.Right(1); var s = stream(a); - flyd.mapAll(function(v) { + flyd.mapEither(function(v) { result.push(v); }, s); var b = Either.Right(2);