From 283e63e0fd748b0da5679e2ce6921278ccb8432c Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Sun, 20 Oct 2013 13:29:12 -0700 Subject: [PATCH 01/45] README: first commit --- README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7bc01a7..06c8a7b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,123 @@ # Understream -underscore-like functionality for dealing with streams. +Understream is a stream platform to make constructing stream chains considerably easier. -# Wishlist +Understream is intended to be used with underscore: +```javascript +_ = require('underscore'); +understream = require('understream'); +_.mixin(understream.exports()); +``` -* support child_process.fork()ing streams to utilize > 1 cpu and more memory +Out of the box, it supports many underscore-like functions, but it also makes it very easy to mix in your own streams: + +```javascript +HOW DO I MAKE A STREAM CLASS IN JAVASCRIPT??? +class Math extends Transform + constructor: (stream_opts) -> super stream_opts + _transform: (num, enc, cb) -> cb null, num+10 +understream.mixin(Math, 'add10') +_.stream([3, 4, 5, 6]).add10().each(console.log).run(function (err) { + console.log("ERR:", err); +}); +# 13 +# 14 +# 15 +# 16 +``` + +## Methods + +### Run +### Duplex +### Readable +### Defaults +### Pipe + +## Default mixins + +### Batch (transform) +`.batch(size)` + +Creates batches out of the objects in your stream. Takes in objects, outputs arrays of those objects. + +```javascript +_.stream([3, 4, 5, 6]).batch(3).each(console.log).run() +# [3, 4, 5] +# [6] +``` + +### Each (transform) +`.each(iterator)` + +Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). + +```javascript +_.stream([3, 4, 5, 6]).each(console.log).run() +# 3 +# 4 +# 5 +# 6 +``` + +### File (transform) +`.file(filepath)` + +Streams the content out of a file. + +```javascript +_.stream().file(path_to_file).split('\n').each(console.log).run() +# line1 +# line2 +# line3 +# ... +``` + +### Filter +`.each(iterator)` + +Calls the iterator for each object in your stream, passing through only the values that pass a truth test (iterator). + +```javascript +_.stream([3, 4, 5, 6]).filter(function (n) { return n > 4 }).each(console.log).run() +# 5 +# 6 +``` + +### First +`.first([n])` + +Passes through only the first `n` objects. If called with no arguments, assumes `n` is `1`. + +Passes through only the first object in the stream. Passing `n` will pass through the first `n` objects. + +```javascript +_.stream([3, 4, 5, 6]).first(2).each(console.log).run() +# 3 +# 4 +``` + +### GroupBy + +### Invoke + +### Join + +### Map + +### Process + +### Progress + +### Queue + +### Reduce + +### Spawn + +### Split + +### Uniq + +### Where From 6b0c81f4b29f5a7a5a42a37d2da5c2d26481c386 Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Mon, 21 Oct 2013 10:25:44 -0700 Subject: [PATCH 02/45] README: fix js stream example --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 06c8a7b..07ac0b4 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,14 @@ _.mixin(understream.exports()); Out of the box, it supports many underscore-like functions, but it also makes it very easy to mix in your own streams: ```javascript -HOW DO I MAKE A STREAM CLASS IN JAVASCRIPT??? -class Math extends Transform - constructor: (stream_opts) -> super stream_opts - _transform: (num, enc, cb) -> cb null, num+10 +Transform = require('stream').Transform +function Math(stream_opts) { + Transform.call(this, stream_opts); +} +Math.prototype._transform = function (num, enc, cb) { + cb(null, num+10); +} +util.inherits(Math, Transform); understream.mixin(Math, 'add10') _.stream([3, 4, 5, 6]).add10().each(console.log).run(function (err) { console.log("ERR:", err); From 46e1f31c9179ef0a8c87e9c48e59cc7147253648 Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Mon, 21 Oct 2013 15:07:19 -0700 Subject: [PATCH 03/45] clarify intro --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07ac0b4..0def680 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Understream -Understream is a stream platform to make constructing stream chains considerably easier. +Understream is a Node.JS utility for manipulating streams in a functional way. It provides streaming versions of many of the same functions that [underscore](http://underscorejs.org) provides for arrays and objects. Understream is intended to be used with underscore: ```javascript From 46aee4fe4c4475cb859eadbc261022b3bc3f35cc Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Mon, 21 Oct 2013 15:13:19 -0700 Subject: [PATCH 04/45] README: start with map example --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0def680..5ed62ac 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,18 @@ understream = require('understream'); _.mixin(understream.exports()); ``` -Out of the box, it supports many underscore-like functions, but it also makes it very easy to mix in your own streams: +Out of the box, it supports many underscore-like functions: +```javascript +_.stream([3, 4, 5, 6]).map(function (num) { return num+10 }).each(console.log).run(function (err) { + console.log("ERR:", err); +}); +# 13 +# 14 +# 15 +# 16 +``` + +It also makes it very easy to mix in your own streams: ```javascript Transform = require('stream').Transform From 4d9b3e29867fae72bd907bd6f0fd9955416a943d Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Mon, 21 Oct 2013 15:24:49 -0700 Subject: [PATCH 05/45] README: nit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ed62ac..1cb3312 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Understream -Understream is a Node.JS utility for manipulating streams in a functional way. It provides streaming versions of many of the same functions that [underscore](http://underscorejs.org) provides for arrays and objects. +Understream is a Node.js utility for manipulating streams in a functional way. It provides streaming versions of many of the same functions that [underscore](http://underscorejs.org) provides for arrays and objects. Understream is intended to be used with underscore: ```javascript From 93a520ccc4b1b5654d26db78625c30e0d8da6025 Mon Sep 17 00:00:00 2001 From: Alex Zylman Date: Mon, 21 Oct 2013 15:26:17 -0700 Subject: [PATCH 06/45] README: better javascript --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1cb3312..f4477d0 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Understream is a Node.js utility for manipulating streams in a functional way. I Understream is intended to be used with underscore: ```javascript -_ = require('underscore'); -understream = require('understream'); +var _ = require('underscore'); +var understream = require('understream'); _.mixin(understream.exports()); ``` @@ -25,7 +25,7 @@ _.stream([3, 4, 5, 6]).map(function (num) { return num+10 }).each(console.log).r It also makes it very easy to mix in your own streams: ```javascript -Transform = require('stream').Transform +var Transform = require('stream').Transform function Math(stream_opts) { Transform.call(this, stream_opts); } From 308dc1aaf9177141f445ae9b925d85e8bc1735bd Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 10 Nov 2013 17:19:53 -0800 Subject: [PATCH 07/45] v1.0 barebones --- lib/{transforms => mixins}/each.coffee | 10 +- lib/mixins/fromArray.coffee | 10 ++ lib/mixins/fromString.coffee | 10 ++ lib/mixins/toArray.coffee | 30 +++++ lib/understream.coffee | 162 ++++++------------------ test/each.coffee | 9 +- test/fromArray.coffee | 12 ++ test/fromString.coffee | 12 ++ test/toArray.coffee | 10 ++ test/{ => todo}/batch.coffee | 0 test/{ => todo}/custom-transform.coffee | 0 test/{ => todo}/duplex.coffee | 0 test/{ => todo}/errors.coffee | 0 test/{ => todo}/file.coffee | 0 test/{ => todo}/file.txt | 0 test/{ => todo}/filter.coffee | 0 test/{ => todo}/first.coffee | 0 test/{ => todo}/flatten.coffee | 0 test/{ => todo}/groupBy.coffee | 0 test/{ => todo}/invoke.coffee | 0 test/{ => todo}/join.coffee | 0 test/{ => todo}/map.coffee | 0 test/{ => todo}/mixin.coffee | 0 test/{ => todo}/myawt.coffee | 0 test/{ => todo}/queue.coffee | 0 test/{ => todo}/reduce.coffee | 0 test/{ => todo}/rest.coffee | 0 test/{ => todo}/slice.coffee | 0 test/{ => todo}/spawn.coffee | 0 test/{ => todo}/split.coffee | 0 test/{ => todo}/stream.coffee | 0 test/{ => todo}/test.txt | 0 test/{ => todo}/uniq.coffee | 0 test/{ => todo}/where.coffee | 0 test/{ => todo}/wrap.coffee | 0 35 files changed, 137 insertions(+), 128 deletions(-) rename lib/{transforms => mixins}/each.coffee (63%) create mode 100644 lib/mixins/fromArray.coffee create mode 100644 lib/mixins/fromString.coffee create mode 100644 lib/mixins/toArray.coffee create mode 100644 test/fromArray.coffee create mode 100644 test/fromString.coffee create mode 100644 test/toArray.coffee rename test/{ => todo}/batch.coffee (100%) rename test/{ => todo}/custom-transform.coffee (100%) rename test/{ => todo}/duplex.coffee (100%) rename test/{ => todo}/errors.coffee (100%) rename test/{ => todo}/file.coffee (100%) rename test/{ => todo}/file.txt (100%) rename test/{ => todo}/filter.coffee (100%) rename test/{ => todo}/first.coffee (100%) rename test/{ => todo}/flatten.coffee (100%) rename test/{ => todo}/groupBy.coffee (100%) rename test/{ => todo}/invoke.coffee (100%) rename test/{ => todo}/join.coffee (100%) rename test/{ => todo}/map.coffee (100%) rename test/{ => todo}/mixin.coffee (100%) rename test/{ => todo}/myawt.coffee (100%) rename test/{ => todo}/queue.coffee (100%) rename test/{ => todo}/reduce.coffee (100%) rename test/{ => todo}/rest.coffee (100%) rename test/{ => todo}/slice.coffee (100%) rename test/{ => todo}/spawn.coffee (100%) rename test/{ => todo}/split.coffee (100%) rename test/{ => todo}/stream.coffee (100%) rename test/{ => todo}/test.txt (100%) rename test/{ => todo}/uniq.coffee (100%) rename test/{ => todo}/where.coffee (100%) rename test/{ => todo}/wrap.coffee (100%) diff --git a/lib/transforms/each.coffee b/lib/mixins/each.coffee similarity index 63% rename from lib/transforms/each.coffee rename to lib/mixins/each.coffee index 98151f8..e3d59bb 100644 --- a/lib/transforms/each.coffee +++ b/lib/mixins/each.coffee @@ -2,8 +2,8 @@ _ = require 'underscore' debug = require('debug') 'us:each' -module.exports = class Each extends Transform - constructor: (@stream_opts, @options) -> +class Each extends Transform + constructor: (@options, @stream_opts) -> super @stream_opts @options = { fn: @options } if _(@options).isFunction() @options._async = @options.fn.length is 2 @@ -17,3 +17,9 @@ module.exports = class Each extends Transform @options.fn chunk @push chunk cb() + +module.exports = + each: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + each = new Each options, stream_opts + readable.pipe each + each diff --git a/lib/mixins/fromArray.coffee b/lib/mixins/fromArray.coffee new file mode 100644 index 0000000..0fac312 --- /dev/null +++ b/lib/mixins/fromArray.coffee @@ -0,0 +1,10 @@ +{Readable} = require 'stream' + +class ArrayStream extends Readable + constructor: (@arr, @index=0) -> + super objectMode: true + _read: (size) => + @push @arr[@index++] # Note: push(undefined) signals the end of the stream, so this just works^tm + +module.exports = + fromArray: (arr) -> new ArrayStream arr diff --git a/lib/mixins/fromString.coffee b/lib/mixins/fromString.coffee new file mode 100644 index 0000000..3f2e7a6 --- /dev/null +++ b/lib/mixins/fromString.coffee @@ -0,0 +1,10 @@ +{Readable} = require 'stream' + +class StringStream extends Readable + constructor: (@str, @index=0) -> + super objectMode: true + _read: (size) => + @push @str[@index++] # Note: push(undefined) signals the end of the stream, so this just works^tm + +module.exports = + fromString: (str) -> new StringStream str diff --git a/lib/mixins/toArray.coffee b/lib/mixins/toArray.coffee new file mode 100644 index 0000000..9ddd47f --- /dev/null +++ b/lib/mixins/toArray.coffee @@ -0,0 +1,30 @@ +{Transform} = require('stream') +_ = require 'underscore' +domain = require 'domain' + +# Accumulates each group of `batchSize` items and outputs them as an array. +class Batch extends Transform + constructor: (@batchSize, @stream_opts) -> + super @stream_opts + @_buffer = [] + _transform: (chunk, encoding, cb) => + if @_buffer.length < @batchSize + @_buffer.push chunk + else + @push @_buffer + @_buffer = [chunk] + cb() + _flush: (cb) => + @push @_buffer if @_buffer.length > 0 + cb() + +module.exports = + toArray: (readable, cb) -> + dmn = domain.create() + batch = new Batch Infinity, {objectMode: readable._readableState.objectMode} + handler = (err) -> cb err, batch._buffer + batch.on 'finish', handler + dmn.on 'error', handler + dmn.add readable + dmn.add batch + dmn.run -> readable.pipe batch diff --git a/lib/understream.coffee b/lib/understream.coffee index b312baa..3af898d 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -1,128 +1,48 @@ {Readable, Writable, PassThrough} = require 'stream' fs = require('fs') _ = require 'underscore' -debug = require('debug') 'us' domain = require 'domain' -{EventEmitter} = require 'events' -_.mixin isPlainObject: (obj) -> obj.constructor is {}.constructor - -is_readable = (instance) -> - instance? and - _.isObject(instance) and - instance instanceof EventEmitter and - instance.pipe? and - (instance._read? or instance.read? or instance.readable) - -pipe_streams_together = (streams...) -> - return if streams.length < 2 - streams[i].pipe streams[i + 1] for i in [0..streams.length - 2] - -# Based on: http://stackoverflow.com/questions/17471659/creating-a-node-js-stream-from-two-piped-streams -# The version there was broken and needed some changes, we just kept the concept of using the 'pipe' -# event and overriding the pipe method -class StreamCombiner extends PassThrough - constructor: (streams...) -> - super objectMode: true - @head = streams[0] - @tail = streams[streams.length - 1] - pipe_streams_together streams... - @on 'pipe', (source) => source.unpipe(@).pipe @head - pipe: (dest, options) => @tail.pipe dest, options - -class ArrayStream extends Readable - constructor: (@options, @arr, @index=0) -> - super _(@options).extend objectMode: true - _read: (size) => - debug "_read #{size} #{JSON.stringify @arr[@index]}" - @push @arr[@index++] # Note: push(undefined) signals the end of the stream, so this just works^tm - -class DevNull extends Writable - constructor: -> super objectMode: true - _write: (chunk, encoding, cb) => cb() - -module.exports = class Understream - constructor: (head) -> - @_defaults = highWaterMark: 1000, objectMode: true - head = new ArrayStream {}, head if _(head).isArray() - if is_readable head - @_streams = [head] - else if not head? - @_streams = [] - else - throw new Error 'Understream expects a readable stream, an array, or nothing' - - defaults: (@_defaults) => @ - run: (cb) => - throw new Error 'Understream::run requires an error handler' unless _(cb).isFunction() - report = => - str = '' - _(@_streams).each (stream) -> - str += "#{stream.constructor.name}(#{stream._writableState?.length or ''} #{stream._readableState?.length or ''}) " - console.log str - interval = setInterval report, 5000 - # If the callback has arity 2, assume that they want us to aggregate all results in an array and - # pass that to the callback. - if cb.length is 2 - result = [] - @batch Infinity - batch_stream = _(@_streams).last() - batch_stream.on 'finish', -> result = batch_stream._buffer - # If the final stream is Readable, attach a dummy writer to receive its output - # and alleviate pressure in the pipe - @_streams.push new DevNull() if is_readable _(@_streams).last() - dmn = domain.create() - handler = (err) => - clearInterval interval - if cb.length is 1 - cb err - else - cb err, result - _(@_streams).last().on 'finish', handler - dmn.on 'error', handler - dmn.add stream for stream in @_streams - dmn.run => - debug 'running' - pipe_streams_together @_streams... - @ - readable: => # If you want to get out of understream and access the raw stream - pipe_streams_together @_streams... - @_streams[@_streams.length - 1] - duplex: => new StreamCombiner @_streams... - stream: => @readable() # Just an alias for compatibility purposes - pipe: (stream_instance) => # If you want to add an instance of a stream to the middle of your understream chain - @_streams.push stream_instance +class _s + constructor: (obj) -> + return obj if obj instanceof _s + return new _s(obj) if not (@ instanceof _s) + @_wrapped = obj + +_([ + "fromArray" + "fromString" + "toArray" + "each" +]).each (fn) -> _s[fn] = require("#{__dirname}/mixins/#{fn}")[fn] + +# Adapted from underscore's mixin +# Add your own custom functions to the Underscore object. +_s.mixin = (obj) -> + _(obj).chain().functions().each (name) -> + func = _s[name] = obj[name] + _s.prototype[name] = -> + args = [@_wrapped] + Array::push.apply args, arguments + res = result.call @, func.apply(_s, args) + return res + +# Add a "chain" function, which will delegate to the wrapper +_s.chain = (obj) -> _s(obj).chain() + +# Helper function to continue chaining intermediate results +result = (obj) -> + if @_chain then _s(obj).chain() else obj + +# Add all of the Understream functions to the wrapper object +_s.mixin _s + +_.extend _s.prototype, + # start chaining a wrapped understream object + chain: -> + @_chain = true @ - @mixin: (FunctionOrStreamKlass, name=(FunctionOrStreamKlass.name or Readable.name), fn=false) -> - if _(FunctionOrStreamKlass).isPlainObject() # underscore-style mixins - @_mixin_by_name klass, name for name, klass of FunctionOrStreamKlass - else - @_mixin_by_name FunctionOrStreamKlass, name, fn - @_mixin_by_name: (FunctionOrStreamKlass, name=(FunctionOrStreamKlass.name or Readable.name), fn=false) -> - Understream::[name] = (args...) -> - if fn - # Allow mixing in of functions like through() - instance = FunctionOrStreamKlass.apply null, args - else - # If this is a class and argument length is < constructor length, prepend defaults to arguments list - if args.length < FunctionOrStreamKlass.length - args.unshift _(@_defaults).clone() - else if args.length is FunctionOrStreamKlass.length - _(args[0]).defaults @_defaults - else - throw new Error "Expected #{FunctionOrStreamKlass.length} or #{FunctionOrStreamKlass.length-1} arguments to #{name}, got #{args.length}" - instance = new FunctionOrStreamKlass args... - @pipe instance - debug 'created', instance.constructor.name, @_streams.length - @ - # For backwards compatibility and easier mixing in to underscore - @exports: -> stream: (head) => new @ head + # Extracts the result from a wrapped and chained object. + value: -> @_wrapped -Understream.mixin _(["#{__dirname}/transforms", "#{__dirname}/readables"]).chain() - .map (dir) -> - _(fs.readdirSync(dir)).map (filename) -> - name = filename.match(/^([^\.]\S+)\.js$/)?[1] - return unless name # Exclude hidden files - [name, require("#{dir}/#{filename}")] - .flatten(true) - .object().value() +module.exports = _s diff --git a/test/each.coffee b/test/each.coffee index 446faba..b8806cf 100644 --- a/test/each.coffee +++ b/test/each.coffee @@ -1,17 +1,16 @@ assert = require 'assert' async = require 'async' -_ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" sinon = require 'sinon' describe '_.each', -> it 'accepts fn (sync/async), calls it on each chunk, then passes the original chunk along', (done) -> input = [{a:'1', b:'2'},{c:'2', d:'3'}] synch = (chunk) -> null - asynch = (chunk, cb) -> cb null, chunk # TODO: error handling - async.forEach [synch, asynch], (fn, cb_fe) -> + asynch = (chunk, cb) -> cb null, chunk + async.forEach [synch], (fn, cb_fe) -> spy = sinon.spy fn - _(input).stream().each(spy).run (err, result) -> + _s(input).chain().fromArray(input).each(spy).toArray (err, result) -> assert.ifError err assert.deepEqual input, result assert.equal spy.callCount, 2 diff --git a/test/fromArray.coffee b/test/fromArray.coffee new file mode 100644 index 0000000..d7c1c31 --- /dev/null +++ b/test/fromArray.coffee @@ -0,0 +1,12 @@ +assert = require 'assert' +_s = require "#{__dirname}/../index" +_ = require 'underscore' + +describe '_s.fromArray', -> + it 'turns an array into a readable stream', -> + arr = ['a', 'b', 'c', 'd'] + readable = _s.fromArray _(arr).clone() # we'll be modifying arr here + # TODO: readable = _s(['a', 'b', 'c', 'd']).fromArray() + assert readable._readableState.objectMode, "Expected fromArray to produce an objectMode stream" + while el = readable.read() + assert.equal el, arr.shift() diff --git a/test/fromString.coffee b/test/fromString.coffee new file mode 100644 index 0000000..1e96d3d --- /dev/null +++ b/test/fromString.coffee @@ -0,0 +1,12 @@ +assert = require 'assert' +_s = require "#{__dirname}/../index" + +describe '_s.fromString', -> + it 'turns a string into a readable stream', -> + str_in = 'abcd' + readable = _s.fromString str_in + # TODO: readable = _s('abcd').fromString() + assert readable._readableState.objectMode, "Expected fromArray to produce an objectMode stream" + str_out = '' + str_out += el while el = readable.read() + assert.equal str_in, str_out diff --git a/test/toArray.coffee b/test/toArray.coffee new file mode 100644 index 0000000..7d56d6a --- /dev/null +++ b/test/toArray.coffee @@ -0,0 +1,10 @@ +assert = require 'assert' +_s = require "#{__dirname}/../index" + +describe '_s.toArray', -> + it 'turns a readable stream into an array', -> + arr_in = ['a', 'b', 'c', 'd'] + readable = _s.fromArray arr_in + _s.toArray readable, (err, arr_out) -> + assert.ifError err + assert.deepEqual arr_in, arr_out diff --git a/test/batch.coffee b/test/todo/batch.coffee similarity index 100% rename from test/batch.coffee rename to test/todo/batch.coffee diff --git a/test/custom-transform.coffee b/test/todo/custom-transform.coffee similarity index 100% rename from test/custom-transform.coffee rename to test/todo/custom-transform.coffee diff --git a/test/duplex.coffee b/test/todo/duplex.coffee similarity index 100% rename from test/duplex.coffee rename to test/todo/duplex.coffee diff --git a/test/errors.coffee b/test/todo/errors.coffee similarity index 100% rename from test/errors.coffee rename to test/todo/errors.coffee diff --git a/test/file.coffee b/test/todo/file.coffee similarity index 100% rename from test/file.coffee rename to test/todo/file.coffee diff --git a/test/file.txt b/test/todo/file.txt similarity index 100% rename from test/file.txt rename to test/todo/file.txt diff --git a/test/filter.coffee b/test/todo/filter.coffee similarity index 100% rename from test/filter.coffee rename to test/todo/filter.coffee diff --git a/test/first.coffee b/test/todo/first.coffee similarity index 100% rename from test/first.coffee rename to test/todo/first.coffee diff --git a/test/flatten.coffee b/test/todo/flatten.coffee similarity index 100% rename from test/flatten.coffee rename to test/todo/flatten.coffee diff --git a/test/groupBy.coffee b/test/todo/groupBy.coffee similarity index 100% rename from test/groupBy.coffee rename to test/todo/groupBy.coffee diff --git a/test/invoke.coffee b/test/todo/invoke.coffee similarity index 100% rename from test/invoke.coffee rename to test/todo/invoke.coffee diff --git a/test/join.coffee b/test/todo/join.coffee similarity index 100% rename from test/join.coffee rename to test/todo/join.coffee diff --git a/test/map.coffee b/test/todo/map.coffee similarity index 100% rename from test/map.coffee rename to test/todo/map.coffee diff --git a/test/mixin.coffee b/test/todo/mixin.coffee similarity index 100% rename from test/mixin.coffee rename to test/todo/mixin.coffee diff --git a/test/myawt.coffee b/test/todo/myawt.coffee similarity index 100% rename from test/myawt.coffee rename to test/todo/myawt.coffee diff --git a/test/queue.coffee b/test/todo/queue.coffee similarity index 100% rename from test/queue.coffee rename to test/todo/queue.coffee diff --git a/test/reduce.coffee b/test/todo/reduce.coffee similarity index 100% rename from test/reduce.coffee rename to test/todo/reduce.coffee diff --git a/test/rest.coffee b/test/todo/rest.coffee similarity index 100% rename from test/rest.coffee rename to test/todo/rest.coffee diff --git a/test/slice.coffee b/test/todo/slice.coffee similarity index 100% rename from test/slice.coffee rename to test/todo/slice.coffee diff --git a/test/spawn.coffee b/test/todo/spawn.coffee similarity index 100% rename from test/spawn.coffee rename to test/todo/spawn.coffee diff --git a/test/split.coffee b/test/todo/split.coffee similarity index 100% rename from test/split.coffee rename to test/todo/split.coffee diff --git a/test/stream.coffee b/test/todo/stream.coffee similarity index 100% rename from test/stream.coffee rename to test/todo/stream.coffee diff --git a/test/test.txt b/test/todo/test.txt similarity index 100% rename from test/test.txt rename to test/todo/test.txt diff --git a/test/uniq.coffee b/test/todo/uniq.coffee similarity index 100% rename from test/uniq.coffee rename to test/todo/uniq.coffee diff --git a/test/where.coffee b/test/todo/where.coffee similarity index 100% rename from test/where.coffee rename to test/todo/where.coffee diff --git a/test/wrap.coffee b/test/todo/wrap.coffee similarity index 100% rename from test/wrap.coffee rename to test/todo/wrap.coffee From 9a9a223540df06834babbf29e94d1a43b425a5f5 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 10 Nov 2013 17:24:11 -0800 Subject: [PATCH 08/45] underscore/understream --- lib/understream.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/understream.coffee b/lib/understream.coffee index 3af898d..a7cac16 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -17,7 +17,7 @@ _([ ]).each (fn) -> _s[fn] = require("#{__dirname}/mixins/#{fn}")[fn] # Adapted from underscore's mixin -# Add your own custom functions to the Underscore object. +# Add your own custom functions to the Understream object. _s.mixin = (obj) -> _(obj).chain().functions().each (name) -> func = _s[name] = obj[name] From 52f12ed38e4f9000ec557327c7565baa70fc23ce Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 10 Nov 2013 17:25:26 -0800 Subject: [PATCH 09/45] unused requires --- lib/understream.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/understream.coffee b/lib/understream.coffee index a7cac16..8c924ce 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -1,7 +1,4 @@ -{Readable, Writable, PassThrough} = require 'stream' -fs = require('fs') _ = require 'underscore' -domain = require 'domain' class _s constructor: (obj) -> From 1f95dff580b32c4fc9ee351be863b3b56358c96b Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 11:06:37 -0800 Subject: [PATCH 10/45] clarify with more comments, move static method defs --- lib/understream.coffee | 50 +++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/understream.coffee b/lib/understream.coffee index 8c924ce..5757a81 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -1,11 +1,36 @@ -_ = require 'underscore' - +_ = require 'underscore' + +# The understream object can be used as an object with static methods: +# _s.each a, b +# It can also be used as a function: +# _s(a).each b +# In order to accomodate the latter case, all static methods also exist on _s.prototype +# Thus, in the constructor we detect if called as a function and return a properly new'd +# instance of _s containing all the prototype methods. class _s constructor: (obj) -> - return obj if obj instanceof _s return new _s(obj) if not (@ instanceof _s) @_wrapped = obj + # Adapted from underscore's mixin + # Add your own custom functions to the Understream object. + @mixin: (obj) -> + _(obj).chain().functions().each (name) -> + func = _s[name] = obj[name] + _s.prototype[name] = -> + args = [@_wrapped] + args.push arguments... + res = result.call @, func.apply(_s, args) + res + + # Add a "chain" function, which will delegate to the wrapper + @chain: (obj) -> _s(obj).chain() + + # Extracts the result from a wrapped and chained object. + value: -> @_wrapped + + +# Fill static methods on _s _([ "fromArray" "fromString" @@ -13,20 +38,6 @@ _([ "each" ]).each (fn) -> _s[fn] = require("#{__dirname}/mixins/#{fn}")[fn] -# Adapted from underscore's mixin -# Add your own custom functions to the Understream object. -_s.mixin = (obj) -> - _(obj).chain().functions().each (name) -> - func = _s[name] = obj[name] - _s.prototype[name] = -> - args = [@_wrapped] - Array::push.apply args, arguments - res = result.call @, func.apply(_s, args) - return res - -# Add a "chain" function, which will delegate to the wrapper -_s.chain = (obj) -> _s(obj).chain() - # Helper function to continue chaining intermediate results result = (obj) -> if @_chain then _s(obj).chain() else obj @@ -34,12 +45,11 @@ result = (obj) -> # Add all of the Understream functions to the wrapper object _s.mixin _s +# _s.mixin just copied the static _s.chain to the prototype, which is incorrect +# Fill in the correct method now _.extend _s.prototype, - # start chaining a wrapped understream object chain: -> @_chain = true @ - # Extracts the result from a wrapped and chained object. - value: -> @_wrapped module.exports = _s From f9be53ec8cc7451bbab06b6e06827fec5590f942 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 11:22:29 -0800 Subject: [PATCH 11/45] move mixin --- lib/understream.coffee | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/understream.coffee b/lib/understream.coffee index 5757a81..3024e75 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -26,30 +26,23 @@ class _s # Add a "chain" function, which will delegate to the wrapper @chain: (obj) -> _s(obj).chain() + # Start accumulating results + chain: -> + @_chain = true + @ + # Extracts the result from a wrapped and chained object. value: -> @_wrapped +# Private helper function to continue chaining intermediate results +result = (obj) -> + if @_chain then _s(obj).chain() else obj -# Fill static methods on _s _([ "fromArray" "fromString" "toArray" "each" -]).each (fn) -> _s[fn] = require("#{__dirname}/mixins/#{fn}")[fn] - -# Helper function to continue chaining intermediate results -result = (obj) -> - if @_chain then _s(obj).chain() else obj - -# Add all of the Understream functions to the wrapper object -_s.mixin _s - -# _s.mixin just copied the static _s.chain to the prototype, which is incorrect -# Fill in the correct method now -_.extend _s.prototype, - chain: -> - @_chain = true - @ +]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") module.exports = _s From fa85a76447d4bc1d6b4fd977a025c4dd1d44f574 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 11:25:53 -0800 Subject: [PATCH 12/45] spelling --- lib/understream.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/understream.coffee b/lib/understream.coffee index 3024e75..e26aa7e 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -4,7 +4,7 @@ _ = require 'underscore' # _s.each a, b # It can also be used as a function: # _s(a).each b -# In order to accomodate the latter case, all static methods also exist on _s.prototype +# In order to accommodate the latter case, all static methods also exist on _s.prototype # Thus, in the constructor we detect if called as a function and return a properly new'd # instance of _s containing all the prototype methods. class _s From 05e42be6db016b45cae69ec8e8f192efc418bb89 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 14:54:20 -0800 Subject: [PATCH 13/45] test: chain, wrap --- test/chain.coffee | 19 +++++++++++++++++++ test/wrap.coffee | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/chain.coffee create mode 100644 test/wrap.coffee diff --git a/test/chain.coffee b/test/chain.coffee new file mode 100644 index 0000000..8e651e1 --- /dev/null +++ b/test/chain.coffee @@ -0,0 +1,19 @@ +assert = require 'assert' +async = require 'async' +_s = require "#{__dirname}/../index" +sinon = require 'sinon' + +describe '_s(a).chain().fn1(b).fn2(c).value()', -> + it 'is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> + spy1 = sinon.spy -> 1 + spy2 = sinon.spy -> 2 + _s.mixin {fn1: spy1, fn2: spy2} + val = _s('a').chain().fn1('b').fn2('c').value() + assert.equal val, 2 + assert.equal val, _s.fn2(_s.fn1('a', 'b'), 'c') + assert.equal spy1.callCount, 2 + assert.equal spy2.callCount, 2 + assert.deepEqual spy1.args[0], ['a', 'b'] + assert.deepEqual spy1.args[1], ['a', 'b'] + assert.deepEqual spy2.args[0], [1, 'c'] + assert.deepEqual spy2.args[1], [1, 'c'] diff --git a/test/wrap.coffee b/test/wrap.coffee new file mode 100644 index 0000000..dc42d5f --- /dev/null +++ b/test/wrap.coffee @@ -0,0 +1,14 @@ +assert = require 'assert' +async = require 'async' +_s = require "#{__dirname}/../index" +sinon = require 'sinon' + +describe '_s(a).fn(b)', -> + it 'is equivalent to calling _s.fn(a,b)', -> + spy = sinon.spy -> + _s.mixin fn: spy + _s.fn 'a', 'b' + _s('a').fn('b') + assert.equal spy.callCount, 2 + assert.deepEqual spy.args[0], ['a', 'b'] + assert.deepEqual spy.args[0], spy.args[1] From b2808b9303492c4d1357767808338f4d1b83e3eb Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 17:08:29 -0800 Subject: [PATCH 14/45] test: more chain/wrap behavior tests --- test/chain.coffee | 12 ++++++++++-- test/wrap.coffee | 31 +++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/test/chain.coffee b/test/chain.coffee index 8e651e1..e67a8d7 100644 --- a/test/chain.coffee +++ b/test/chain.coffee @@ -3,8 +3,16 @@ async = require 'async' _s = require "#{__dirname}/../index" sinon = require 'sinon' -describe '_s(a).chain().fn1(b).fn2(c).value()', -> - it 'is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> +describe '_s(a).chain()', -> + it "has the methods of _s", -> + assert _s().chain().each? + assert _s('a').chain().each? + + it '.value() returns a', -> + assert.equal _s().chain().value(), undefined + assert.equal _s('a').chain().value(), 'a' + + it '.fn1(b).fn2(c).value() is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> spy1 = sinon.spy -> 1 spy2 = sinon.spy -> 2 _s.mixin {fn1: spy1, fn2: spy2} diff --git a/test/wrap.coffee b/test/wrap.coffee index dc42d5f..536af09 100644 --- a/test/wrap.coffee +++ b/test/wrap.coffee @@ -3,8 +3,31 @@ async = require 'async' _s = require "#{__dirname}/../index" sinon = require 'sinon' -describe '_s(a).fn(b)', -> - it 'is equivalent to calling _s.fn(a,b)', -> +describe '_s(a)', -> + it 'has the methods of _s', -> + assert _s().each? + assert _s('a').each? + + it 'binds a as the first argument to the next method invoked', -> + spy = sinon.spy -> + _s.mixin fn: spy + _s().fn(10) + _s().fn(10, 20) + _s('a').fn(10) + _s('a').fn(10, 20) + _s('a', 'b').fn(10) + _s('a', 'b').fn(10, 20) + assert.equal spy.callCount, 6 + assert.deepEqual spy.args, [ + [undefined, 10] + [undefined, 10, 20] + ['a', 10] + ['a', 10, 20] + ['a', 10] # ignores > 1 argument + ['a', 10, 20] # ignores > 1 argument + ] + + it '.fn(b) is equivalent to calling _s.fn(a,b)', -> spy = sinon.spy -> _s.mixin fn: spy _s.fn 'a', 'b' @@ -12,3 +35,7 @@ describe '_s(a).fn(b)', -> assert.equal spy.callCount, 2 assert.deepEqual spy.args[0], ['a', 'b'] assert.deepEqual spy.args[0], spy.args[1] + + it '.missing() throws', -> + assert.throws () -> + _s('a').missing() From 93d9edc2d4c765c490ef146530ef266bd550988d Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 17:22:18 -0800 Subject: [PATCH 15/45] test: more better for methods on wrapped/chained --- test/chain.coffee | 10 +++++++--- test/wrap.coffee | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/chain.coffee b/test/chain.coffee index e67a8d7..b7299b7 100644 --- a/test/chain.coffee +++ b/test/chain.coffee @@ -2,11 +2,15 @@ assert = require 'assert' async = require 'async' _s = require "#{__dirname}/../index" sinon = require 'sinon' +_ = require 'underscore' describe '_s(a).chain()', -> - it "has the methods of _s", -> - assert _s().chain().each? - assert _s('a').chain().each? + it 'has the methods of _s excluding value/mixin', -> + for obj in [_s().chain(), _s('a').chain()] + assert.deepEqual( + _.chain().functions(obj).without('value').value() + _.chain().functions(_s).without('mixin').value() + ) it '.value() returns a', -> assert.equal _s().chain().value(), undefined diff --git a/test/wrap.coffee b/test/wrap.coffee index 536af09..57d63a6 100644 --- a/test/wrap.coffee +++ b/test/wrap.coffee @@ -2,11 +2,15 @@ assert = require 'assert' async = require 'async' _s = require "#{__dirname}/../index" sinon = require 'sinon' +_ = require 'underscore' describe '_s(a)', -> - it 'has the methods of _s', -> - assert _s().each? - assert _s('a').each? + it 'has the methods of _s excluding value/mixin', -> + for obj in [_s(), _s('a')] + assert.deepEqual( + _.chain().functions(obj).without('value').value() + _.chain().functions(_s).without('mixin').value() + ) it 'binds a as the first argument to the next method invoked', -> spy = sinon.spy -> From 83e51d4b39e46e1b5c75f7b371ed593cb38b44ba Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 17:42:22 -0800 Subject: [PATCH 16/45] test: combine chain/wrap into chain-wrap --- test/{wrap.coffee => chain-wrap.coffee} | 33 +++++++++++++++++++------ test/chain.coffee | 31 ----------------------- 2 files changed, 26 insertions(+), 38 deletions(-) rename test/{wrap.coffee => chain-wrap.coffee} (50%) delete mode 100644 test/chain.coffee diff --git a/test/wrap.coffee b/test/chain-wrap.coffee similarity index 50% rename from test/wrap.coffee rename to test/chain-wrap.coffee index 57d63a6..04f01e9 100644 --- a/test/wrap.coffee +++ b/test/chain-wrap.coffee @@ -4,14 +4,14 @@ _s = require "#{__dirname}/../index" sinon = require 'sinon' _ = require 'underscore' -describe '_s(a)', -> - it 'has the methods of _s excluding value/mixin', -> - for obj in [_s(), _s('a')] - assert.deepEqual( - _.chain().functions(obj).without('value').value() - _.chain().functions(_s).without('mixin').value() - ) +describe 'methods on wrapped/chained objects are the same as methods on _s', -> + for obj in [_s(), _s('a'), _s().chain(), _s('a').chain()] + assert.deepEqual( + _.chain().functions(obj).without('value').value() + _.chain().functions(_s).without('mixin').value() + ) +describe '_s(a)', -> it 'binds a as the first argument to the next method invoked', -> spy = sinon.spy -> _s.mixin fn: spy @@ -43,3 +43,22 @@ describe '_s(a)', -> it '.missing() throws', -> assert.throws () -> _s('a').missing() + +describe '_s(a).chain()', -> + it '.value() returns a', -> + assert.equal _s().chain().value(), undefined + assert.equal _s('a').chain().value(), 'a' + + it '.fn1(b).fn2(c).value() is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> + spy1 = sinon.spy -> 1 + spy2 = sinon.spy -> 2 + _s.mixin {fn1: spy1, fn2: spy2} + val = _s('a').chain().fn1('b').fn2('c').value() + assert.equal val, 2 + assert.equal val, _s.fn2(_s.fn1('a', 'b'), 'c') + assert.equal spy1.callCount, 2 + assert.equal spy2.callCount, 2 + assert.deepEqual spy1.args[0], ['a', 'b'] + assert.deepEqual spy1.args[1], ['a', 'b'] + assert.deepEqual spy2.args[0], [1, 'c'] + assert.deepEqual spy2.args[1], [1, 'c'] diff --git a/test/chain.coffee b/test/chain.coffee deleted file mode 100644 index b7299b7..0000000 --- a/test/chain.coffee +++ /dev/null @@ -1,31 +0,0 @@ -assert = require 'assert' -async = require 'async' -_s = require "#{__dirname}/../index" -sinon = require 'sinon' -_ = require 'underscore' - -describe '_s(a).chain()', -> - it 'has the methods of _s excluding value/mixin', -> - for obj in [_s().chain(), _s('a').chain()] - assert.deepEqual( - _.chain().functions(obj).without('value').value() - _.chain().functions(_s).without('mixin').value() - ) - - it '.value() returns a', -> - assert.equal _s().chain().value(), undefined - assert.equal _s('a').chain().value(), 'a' - - it '.fn1(b).fn2(c).value() is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> - spy1 = sinon.spy -> 1 - spy2 = sinon.spy -> 2 - _s.mixin {fn1: spy1, fn2: spy2} - val = _s('a').chain().fn1('b').fn2('c').value() - assert.equal val, 2 - assert.equal val, _s.fn2(_s.fn1('a', 'b'), 'c') - assert.equal spy1.callCount, 2 - assert.equal spy2.callCount, 2 - assert.deepEqual spy1.args[0], ['a', 'b'] - assert.deepEqual spy1.args[1], ['a', 'b'] - assert.deepEqual spy2.args[0], [1, 'c'] - assert.deepEqual spy2.args[1], [1, 'c'] From 85b745f1d70b294779827f75f11a37cf23342697 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 11 Nov 2013 18:20:58 -0800 Subject: [PATCH 17/45] test: chain().chain() --- test/chain-wrap.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/chain-wrap.coffee b/test/chain-wrap.coffee index 04f01e9..c2b8631 100644 --- a/test/chain-wrap.coffee +++ b/test/chain-wrap.coffee @@ -5,7 +5,7 @@ sinon = require 'sinon' _ = require 'underscore' describe 'methods on wrapped/chained objects are the same as methods on _s', -> - for obj in [_s(), _s('a'), _s().chain(), _s('a').chain()] + for obj in [_s(), _s('a'), _s().chain(), _s('a').chain(), _s().chain().chain()] assert.deepEqual( _.chain().functions(obj).without('value').value() _.chain().functions(_s).without('mixin').value() From 04977f590f97f4d561e52c8de5b30da9721e70a6 Mon Sep 17 00:00:00 2001 From: jonahkagan Date: Tue, 12 Nov 2013 15:34:31 -0800 Subject: [PATCH 18/45] test/chain-wrap: abstracted equivalence-testing pattern --- test/chain-wrap.coffee | 102 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/test/chain-wrap.coffee b/test/chain-wrap.coffee index c2b8631..cc6ec2f 100644 --- a/test/chain-wrap.coffee +++ b/test/chain-wrap.coffee @@ -4,61 +4,57 @@ _s = require "#{__dirname}/../index" sinon = require 'sinon' _ = require 'underscore' -describe 'methods on wrapped/chained objects are the same as methods on _s', -> - for obj in [_s(), _s('a'), _s().chain(), _s('a').chain(), _s().chain().chain()] - assert.deepEqual( - _.chain().functions(obj).without('value').value() - _.chain().functions(_s).without('mixin').value() - ) +# Takes an array and returns an array of adjacent pairs of elements in the +# array, wrapping around at the end. +adjacent = (arr) -> + _.zip arr, _.rest(arr).concat [_.first arr] -describe '_s(a)', -> - it 'binds a as the first argument to the next method invoked', -> - spy = sinon.spy -> - _s.mixin fn: spy - _s().fn(10) - _s().fn(10, 20) - _s('a').fn(10) - _s('a').fn(10, 20) - _s('a', 'b').fn(10) - _s('a', 'b').fn(10, 20) - assert.equal spy.callCount, 6 - assert.deepEqual spy.args, [ - [undefined, 10] - [undefined, 10, 20] - ['a', 10] - ['a', 10, 20] - ['a', 10] # ignores > 1 argument - ['a', 10, 20] # ignores > 1 argument - ] +methods = (obj) -> + _.chain().functions(obj).without('value', 'mixin').value() - it '.fn(b) is equivalent to calling _s.fn(a,b)', -> - spy = sinon.spy -> - _s.mixin fn: spy - _s.fn 'a', 'b' - _s('a').fn('b') - assert.equal spy.callCount, 2 - assert.deepEqual spy.args[0], ['a', 'b'] - assert.deepEqual spy.args[0], spy.args[1] +idSpy = -> sinon.spy (x) -> x - it '.missing() throws', -> - assert.throws () -> - _s('a').missing() +testEquivalent = (exp1, exp2) -> + [spy1, spy2] = [idSpy(), idSpy()] + _s.mixin fn: spy1 + v1 = exp1() + _s.mixin fn: spy2 # Relies on _s.mixin overwriting fn + v2 = exp2() + it 'return the same result', -> assert.deepEqual v1, v2 + it 'have the same methods available on the result', -> assert.deepEqual methods(v1), methods(v2) + it 'call the method the same number of times', -> assert.equal spy1.callCount, spy2.callCount + it 'call the method with the same args', -> assert.deepEqual spy1.args, spy2.args -describe '_s(a).chain()', -> - it '.value() returns a', -> - assert.equal _s().chain().value(), undefined - assert.equal _s('a').chain().value(), 'a' +_.each + 'no-op': + 'plain' : -> 'a' + 'unwrapped chained' : -> _s.chain('a').value() + 'wrapped chained' : -> _s('a').chain().value() + 'no-arg': + 'unwrapped' : -> _s.fn() + 'wrapped' : -> _s().fn() + 'unwrapped chained' : -> _s.chain().fn().value() + 'wrapped chained' : -> _s().chain().fn().value() + 'one-arg': + 'unwrapped' : -> _s.fn('a') + 'wrapped' : -> _s('a').fn() + 'unwrapped chained' : -> _s.chain('a').fn().value() + 'wrapped chained' : -> _s('a').chain().fn().value() + 'multi-arg': + 'unwrapped' : -> _s.fn('a', {b:1}, 2) + 'wrapped' : -> _s('a').fn({b:1}, 2) + 'unwrapped chained' : -> _s.chain('a').fn({b:1}, 2).value() + 'wrapped chained' : -> _s('a').chain().fn({b:1}, 2).value() + 'multiple functions': + 'unwrapped' : -> _s.fn _s.fn('a', 'b'), 'c' + 'wrapped' : -> _s(_s('a').fn('b')).fn('c') + 'unwrapped chained' : -> _s.chain('a').fn('b').fn('c').value() + 'wrapped chained' : -> _s('a').chain().fn('b').fn('c').value() - it '.fn1(b).fn2(c).value() is equivalent to calling _s.fn2(_s.fn1(a, b), c)', -> - spy1 = sinon.spy -> 1 - spy2 = sinon.spy -> 2 - _s.mixin {fn1: spy1, fn2: spy2} - val = _s('a').chain().fn1('b').fn2('c').value() - assert.equal val, 2 - assert.equal val, _s.fn2(_s.fn1('a', 'b'), 'c') - assert.equal spy1.callCount, 2 - assert.equal spy2.callCount, 2 - assert.deepEqual spy1.args[0], ['a', 'b'] - assert.deepEqual spy1.args[1], ['a', 'b'] - assert.deepEqual spy2.args[0], [1, 'c'] - assert.deepEqual spy2.args[1], [1, 'c'] +, (exps, desc) -> + describe desc, -> + # Since equivalence is transitive, to assert that a group of expressions + # are equivalent, we can assert that each one is equivalent to one other + # one. + _.each adjacent(_.pairs(exps)), ([[name1, exp1], [name2, exp2]]) -> + describe "#{name1}/#{name2}", -> testEquivalent exp1, exp2 From b64a45ec73248feb5293549fa1d7aa55dac2341e Mon Sep 17 00:00:00 2001 From: jonahkagan Date: Tue, 12 Nov 2013 16:43:30 -0800 Subject: [PATCH 19/45] test/chain-wrap: use a new _s object for each equivalent expression --- index.js | 2 +- lib/understream.coffee | 94 +++++++++++++++++++++--------------------- test/chain-wrap.coffee | 52 +++++++++++------------ 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/index.js b/index.js index 715768a..64a1ea7 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ var path = __dirname + '/' + (process.env.TEST_UNDERSTREAM_COV ? 'lib-js-cov' : 'lib-js') + '/understream'; -module.exports = require(path); +module.exports = require(path)(); diff --git a/lib/understream.coffee b/lib/understream.coffee index e26aa7e..3741085 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -1,48 +1,50 @@ _ = require 'underscore' -# The understream object can be used as an object with static methods: -# _s.each a, b -# It can also be used as a function: -# _s(a).each b -# In order to accommodate the latter case, all static methods also exist on _s.prototype -# Thus, in the constructor we detect if called as a function and return a properly new'd -# instance of _s containing all the prototype methods. -class _s - constructor: (obj) -> - return new _s(obj) if not (@ instanceof _s) - @_wrapped = obj - - # Adapted from underscore's mixin - # Add your own custom functions to the Understream object. - @mixin: (obj) -> - _(obj).chain().functions().each (name) -> - func = _s[name] = obj[name] - _s.prototype[name] = -> - args = [@_wrapped] - args.push arguments... - res = result.call @, func.apply(_s, args) - res - - # Add a "chain" function, which will delegate to the wrapper - @chain: (obj) -> _s(obj).chain() - - # Start accumulating results - chain: -> - @_chain = true - @ - - # Extracts the result from a wrapped and chained object. - value: -> @_wrapped - -# Private helper function to continue chaining intermediate results -result = (obj) -> - if @_chain then _s(obj).chain() else obj - -_([ - "fromArray" - "fromString" - "toArray" - "each" -]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") - -module.exports = _s +module.exports = -> + + # The understream object can be used as an object with static methods: + # _s.each a, b + # It can also be used as a function: + # _s(a).each b + # In order to accommodate the latter case, all static methods also exist on _s.prototype + # Thus, in the constructor we detect if called as a function and return a properly new'd + # instance of _s containing all the prototype methods. + class _s + constructor: (obj) -> + return new _s(obj) if not (@ instanceof _s) + @_wrapped = obj + + # Adapted from underscore's mixin + # Add your own custom functions to the Understream object. + @mixin: (obj) -> + _(obj).chain().functions().each (name) -> + func = _s[name] = obj[name] + _s.prototype[name] = -> + args = [@_wrapped] + args.push arguments... + res = result.call @, func.apply(_s, args) + res + + # Add a "chain" function, which will delegate to the wrapper + @chain: (obj) -> _s(obj).chain() + + # Start accumulating results + chain: -> + @_chain = true + @ + + # Extracts the result from a wrapped and chained object. + value: -> @_wrapped + + # Private helper function to continue chaining intermediate results + result = (obj) -> + if @_chain then _s(obj).chain() else obj + + _([ + "fromArray" + "fromString" + "toArray" + "each" + ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") + + _s diff --git a/test/chain-wrap.coffee b/test/chain-wrap.coffee index cc6ec2f..41a87e7 100644 --- a/test/chain-wrap.coffee +++ b/test/chain-wrap.coffee @@ -1,6 +1,6 @@ assert = require 'assert' async = require 'async' -_s = require "#{__dirname}/../index" +_sMaker = require "../lib/understream" sinon = require 'sinon' _ = require 'underscore' @@ -12,14 +12,12 @@ adjacent = (arr) -> methods = (obj) -> _.chain().functions(obj).without('value', 'mixin').value() -idSpy = -> sinon.spy (x) -> x - testEquivalent = (exp1, exp2) -> - [spy1, spy2] = [idSpy(), idSpy()] - _s.mixin fn: spy1 - v1 = exp1() - _s.mixin fn: spy2 # Relies on _s.mixin overwriting fn - v2 = exp2() + [[v1, spy1], [v2, spy2]] = _.map [exp1, exp2], (exp) -> + spy = sinon.spy (x) -> x + _s = _sMaker() + _s.mixin fn: spy + [exp(_s), spy] it 'return the same result', -> assert.deepEqual v1, v2 it 'have the same methods available on the result', -> assert.deepEqual methods(v1), methods(v2) it 'call the method the same number of times', -> assert.equal spy1.callCount, spy2.callCount @@ -27,29 +25,29 @@ testEquivalent = (exp1, exp2) -> _.each 'no-op': - 'plain' : -> 'a' - 'unwrapped chained' : -> _s.chain('a').value() - 'wrapped chained' : -> _s('a').chain().value() + 'plain' : (_s) -> 'a' + 'unwrapped chained' : (_s) -> _s.chain('a').value() + 'wrapped chained' : (_s) -> _s('a').chain().value() 'no-arg': - 'unwrapped' : -> _s.fn() - 'wrapped' : -> _s().fn() - 'unwrapped chained' : -> _s.chain().fn().value() - 'wrapped chained' : -> _s().chain().fn().value() + 'unwrapped' : (_s) -> _s.fn() + 'wrapped' : (_s) -> _s().fn() + 'unwrapped chained' : (_s) -> _s.chain().fn().value() + 'wrapped chained' : (_s) -> _s().chain().fn().value() 'one-arg': - 'unwrapped' : -> _s.fn('a') - 'wrapped' : -> _s('a').fn() - 'unwrapped chained' : -> _s.chain('a').fn().value() - 'wrapped chained' : -> _s('a').chain().fn().value() + 'unwrapped' : (_s) -> _s.fn('a') + 'wrapped' : (_s) -> _s('a').fn() + 'unwrapped chained' : (_s) -> _s.chain('a').fn().value() + 'wrapped chained' : (_s) -> _s('a').chain().fn().value() 'multi-arg': - 'unwrapped' : -> _s.fn('a', {b:1}, 2) - 'wrapped' : -> _s('a').fn({b:1}, 2) - 'unwrapped chained' : -> _s.chain('a').fn({b:1}, 2).value() - 'wrapped chained' : -> _s('a').chain().fn({b:1}, 2).value() + 'unwrapped' : (_s) -> _s.fn('a', {b:1}, 2) + 'wrapped' : (_s) -> _s('a').fn({b:1}, 2) + 'unwrapped chained' : (_s) -> _s.chain('a').fn({b:1}, 2).value() + 'wrapped chained' : (_s) -> _s('a').chain().fn({b:1}, 2).value() 'multiple functions': - 'unwrapped' : -> _s.fn _s.fn('a', 'b'), 'c' - 'wrapped' : -> _s(_s('a').fn('b')).fn('c') - 'unwrapped chained' : -> _s.chain('a').fn('b').fn('c').value() - 'wrapped chained' : -> _s('a').chain().fn('b').fn('c').value() + 'unwrapped' : (_s) -> _s.fn _s.fn('a', 'b'), 'c' + 'wrapped' : (_s) -> _s(_s('a').fn('b')).fn('c') + 'unwrapped chained' : (_s) -> _s.chain('a').fn('b').fn('c').value() + 'wrapped chained' : (_s) -> _s('a').chain().fn('b').fn('c').value() , (exps, desc) -> describe desc, -> From b17cda0e9264fa05fa48e0aa3a260a8d88cbd42d Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 10:49:10 -0800 Subject: [PATCH 20/45] fix broken tests cc @jonahkagan --- lib/understream.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/understream.coffee b/lib/understream.coffee index 3741085..1c75d49 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -22,6 +22,8 @@ module.exports = -> _s.prototype[name] = -> args = [@_wrapped] args.push arguments... + # pop undefineds so that _s.fn() is equivalent to _s().fn() + args.pop() while args.length and _(args).last() is undefined res = result.call @, func.apply(_s, args) res From fb225b71ee2a9c19b7079e8adbdd4b117ab27d64 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 18:03:57 -0800 Subject: [PATCH 21/45] readme: update preamble --- README.md | 51 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f4477d0..74a5eec 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,29 @@ # Understream -Understream is a Node.js utility for manipulating streams in a functional way. It provides streaming versions of many of the same functions that [underscore](http://underscorejs.org) provides for arrays and objects. +Understream is a Node utility for manipulating streams in a functional way. +It provides three classes of functionality: -Understream is intended to be used with underscore: -```javascript -var _ = require('underscore'); -var understream = require('understream'); -_.mixin(understream.exports()); -``` +1. Functions to convert data to [Readable](http://nodejs.org/api/stream.html#stream_class_stream_readable) streams and vice versa: + * [`fromArray`](#fromArray) + * [`fromString`](#fromArray) + * [`toArray`](#toArray) + +2. Functions that take a Readable stream and transform its data: + + * [`each`](#each) + +3. Functions that allow you to create chains of transformations + + * [`chain`](#chain) + * [`value`](#value) + +The library has underscore-like usage: -Out of the box, it supports many underscore-like functions: ```javascript -_.stream([3, 4, 5, 6]).map(function (num) { return num+10 }).each(console.log).run(function (err) { - console.log("ERR:", err); -}); +var _s = require('understream'); +input = _.fromArray([3, 4, 5, 6]); +_s.chain(input).map(function(num) {return num+10}).each(console.log); # 13 # 14 # 15 @@ -26,17 +35,27 @@ It also makes it very easy to mix in your own streams: ```javascript var Transform = require('stream').Transform +var util = require('util'); +var _s = require('understream'); + +util.inherits(Math, Transform); + function Math(stream_opts) { Transform.call(this, stream_opts); } + Math.prototype._transform = function (num, enc, cb) { cb(null, num+10); -} -util.inherits(Math, Transform); -understream.mixin(Math, 'add10') -_.stream([3, 4, 5, 6]).add10().each(console.log).run(function (err) { - console.log("ERR:", err); +}; + +_s.mixin({ + add10: function(readable, stream_opts) { + return readable.pipe(new Math(stream_opts)); + } }); + +input = _s.fromArray([3, 4, 5, 6]); +_s(input).chain().add10({objectMode:true}).each(console.log); # 13 # 14 # 15 From 1f0f71ceaec98226653cdc91fa5e3dca0c40209c Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 18:05:17 -0800 Subject: [PATCH 22/45] readme: javascript comments --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 74a5eec..77d61cc 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ The library has underscore-like usage: var _s = require('understream'); input = _.fromArray([3, 4, 5, 6]); _s.chain(input).map(function(num) {return num+10}).each(console.log); -# 13 -# 14 -# 15 -# 16 +// 13 +// 14 +// 15 +// 16 ``` It also makes it very easy to mix in your own streams: @@ -56,10 +56,10 @@ _s.mixin({ input = _s.fromArray([3, 4, 5, 6]); _s(input).chain().add10({objectMode:true}).each(console.log); -# 13 -# 14 -# 15 -# 16 +// 13 +// 14 +// 15 +// 16 ``` ## Methods @@ -83,13 +83,13 @@ _.stream([3, 4, 5, 6]).batch(3).each(console.log).run() # [6] ``` -### Each (transform) -`.each(iterator)` +### Each +`_s.each(readable, iterator)` Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). ```javascript -_.stream([3, 4, 5, 6]).each(console.log).run() +_s(_.fromArray([3, 4, 5, 6])).each(console.log) # 3 # 4 # 5 From 0e05f50695a94f3ade3e229b0e98925106c1a50f Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 18:38:35 -0800 Subject: [PATCH 23/45] readme: fromArray, fromString, toArray, chain, value cc @understream-has-no-docs-haters --- README.md | 106 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 77d61cc..b5e1718 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ util.inherits(Math, Transform); function Math(stream_opts) { Transform.call(this, stream_opts); -} +}; Math.prototype._transform = function (num, enc, cb) { cb(null, num+10); @@ -64,14 +64,100 @@ _s(input).chain().add10({objectMode:true}).each(console.log); ## Methods +### fromArray +`_s.fromArray(array)` + +Turns an array into a readable stream of the objects within the array. + +```javascript +var readable = _s.fromArray([3, 4, 5, 6]); +console.log(readable.read()); +// 3 +console.log(readable.read()); +// 4 +console.log(readable.read()); +// 5 +console.log(readable.read()); +// 6 +``` + +### fromString +`_s.fromString(string)` + +Turns a string into a readable stream of the characters within the string. + +```javascript +var readable = _s.fromString("3456"); +readable.on("data", console.log); +// 3 +// 4 +// 5 +// 6 +``` + +### toArray +`_s.toArray(readable, cb)` + +Reads a stream into an array of the data emitted by the stream. +Calls `cb(err, arr)` when finished. + +```javascript +var readable = _s.fromArray([3, 4, 5, 6]); +_s.toArray(readable, function(err, arr) { + console.log(arr); +}); +// [ 3, 4, 5, 6 ] +``` + +### Each +`_s.each(readable, iterator)` + +Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). + +```javascript +var readable = _s(_s.fromArray([3, 4, 5, 6])).value(); +readable.on("data", console.log); +// 3 +// 4 +// 5 +// 6 +``` + +### chain +`_s.chain(obj)` + +Analagous to underscore's `chain`: returns a wrapped object with all the methods of understream. + +```javascript +_s.chain(_s.fromArray([3, 4, 5, 6])).each(console.log) +// 3 +// 4 +// 5 +// 6 +``` + +### value +`_s.chain(obj)` + +Analagous to underscore's `value`: exits a chain and returns the return value of the last method called. + +```javascript +var readable = _s.chain(_s.fromArray([3, 4, 5, 6])).value(); +// 3 +// 4 +// 5 +// 6 +``` + + + + \ No newline at end of file From a1fd6f2e645d44afe160bcfdf00731ee93b565b2 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 18:42:27 -0800 Subject: [PATCH 24/45] readme: move fn signature to heading line --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b5e1718..c4551c0 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,7 @@ _s(input).chain().add10({objectMode:true}).each(console.log); ## Methods -### fromArray -`_s.fromArray(array)` +### fromArray `_s.fromArray(array)` Turns an array into a readable stream of the objects within the array. @@ -81,8 +80,7 @@ console.log(readable.read()); // 6 ``` -### fromString -`_s.fromString(string)` +### fromString `_s.fromString(string)` Turns a string into a readable stream of the characters within the string. @@ -95,8 +93,7 @@ readable.on("data", console.log); // 6 ``` -### toArray -`_s.toArray(readable, cb)` +### toArray `_s.toArray(readable, cb)` Reads a stream into an array of the data emitted by the stream. Calls `cb(err, arr)` when finished. @@ -109,8 +106,7 @@ _s.toArray(readable, function(err, arr) { // [ 3, 4, 5, 6 ] ``` -### Each -`_s.each(readable, iterator)` +### each `_s.each(readable, iterator)` Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). @@ -123,8 +119,7 @@ readable.on("data", console.log); // 6 ``` -### chain -`_s.chain(obj)` +### chain `_s.chain(obj)` Analagous to underscore's `chain`: returns a wrapped object with all the methods of understream. @@ -136,8 +131,7 @@ _s.chain(_s.fromArray([3, 4, 5, 6])).each(console.log) // 6 ``` -### value -`_s.chain(obj)` +### value `_s.chain(obj)` Analagous to underscore's `value`: exits a chain and returns the return value of the last method called. From 8c09e079abe2379515dca47a37b66a40891c3cf8 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 18:48:19 -0800 Subject: [PATCH 25/45] readme: smaller headings, less console.log --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c4551c0..c923c91 100644 --- a/README.md +++ b/README.md @@ -64,23 +64,20 @@ _s(input).chain().add10({objectMode:true}).each(console.log); ## Methods -### fromArray `_s.fromArray(array)` +#### fromArray `_s.fromArray(array)` Turns an array into a readable stream of the objects within the array. ```javascript -var readable = _s.fromArray([3, 4, 5, 6]); -console.log(readable.read()); +var readable = _s.fromArray([3, 4, 5, 6]);c +readable.on("data", console.log); // 3 -console.log(readable.read()); // 4 -console.log(readable.read()); // 5 -console.log(readable.read()); // 6 ``` -### fromString `_s.fromString(string)` +#### fromString `_s.fromString(string)` Turns a string into a readable stream of the characters within the string. @@ -93,7 +90,7 @@ readable.on("data", console.log); // 6 ``` -### toArray `_s.toArray(readable, cb)` +#### toArray `_s.toArray(readable, cb)` Reads a stream into an array of the data emitted by the stream. Calls `cb(err, arr)` when finished. @@ -106,7 +103,7 @@ _s.toArray(readable, function(err, arr) { // [ 3, 4, 5, 6 ] ``` -### each `_s.each(readable, iterator)` +#### each `_s.each(readable, iterator)` Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). @@ -119,7 +116,7 @@ readable.on("data", console.log); // 6 ``` -### chain `_s.chain(obj)` +#### chain `_s.chain(obj)` Analagous to underscore's `chain`: returns a wrapped object with all the methods of understream. @@ -131,7 +128,7 @@ _s.chain(_s.fromArray([3, 4, 5, 6])).each(console.log) // 6 ``` -### value `_s.chain(obj)` +#### value `_s.chain(obj)` Analagous to underscore's `value`: exits a chain and returns the return value of the last method called. From f80fe7bb9b041f589719c7a298d052b3595e333a Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 19:19:15 -0800 Subject: [PATCH 26/45] readme: tweaks, add map --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c923c91..eda520e 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ It provides three classes of functionality: * [`fromString`](#fromArray) * [`toArray`](#toArray) -2. Functions that take a Readable stream and transform its data: +2. Functions that take a Readable stream and transform its data, returning a new readable stream: * [`each`](#each) -3. Functions that allow you to create chains of transformations +3. Functions that allow you to create chains of transformations: * [`chain`](#chain) * [`value`](#value) @@ -77,6 +77,7 @@ readable.on("data", console.log); // 6 ``` +--- #### fromString `_s.fromString(string)` Turns a string into a readable stream of the characters within the string. @@ -90,6 +91,7 @@ readable.on("data", console.log); // 6 ``` +--- #### toArray `_s.toArray(readable, cb)` Reads a stream into an array of the data emitted by the stream. @@ -103,19 +105,40 @@ _s.toArray(readable, function(err, arr) { // [ 3, 4, 5, 6 ] ``` +--- #### each `_s.each(readable, iterator)` -Calls the iterator function on each object in your stream, and passes the same object through when your interator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). +Calls the iterator function on each object in your stream, and emits the same object when your interator function is done. +If the iterator function has one argument (`(element)`), it is assumed to be synchronous. +If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). ```javascript -var readable = _s(_s.fromArray([3, 4, 5, 6])).value(); -readable.on("data", console.log); +var readable = _s.fromArray([3, 4, 5, 6]); +_s.each(readable console.log); +// 3 +// 4 +// 5 +// 6 +``` + +--- +#### map `_s.map(readable, iterator)` + +Makes a new stream that is the result of calling `iterator` on each piece of data in `readable`. +If the iterator function has one argument (`(element)`), it is assumed to be synchronous. +If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). + +```javascript +var readable = _s.fromArray([3.3, 4.1, 5.2, 6.4])); +var mapped = _s.map(readable, Math.floor); +mapped.on("data", console.log); // 3 // 4 // 5 // 6 ``` +--- #### chain `_s.chain(obj)` Analagous to underscore's `chain`: returns a wrapped object with all the methods of understream. @@ -128,6 +151,7 @@ _s.chain(_s.fromArray([3, 4, 5, 6])).each(console.log) // 6 ``` +--- #### value `_s.chain(obj)` Analagous to underscore's `value`: exits a chain and returns the return value of the last method called. @@ -203,8 +227,6 @@ _.stream([3, 4, 5, 6]).first(2).each(console.log).run() ### Join -### Map - ### Process ### Progress From 55549861ee17ed3164e11ac3873006c3b5c726ea Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 19:24:47 -0800 Subject: [PATCH 27/45] map --- README.md | 3 ++- lib/{transforms => mixins}/map.coffee | 6 +++++- lib/understream.coffee | 1 + test/{todo => }/map.coffee | 8 ++++---- 4 files changed, 12 insertions(+), 6 deletions(-) rename lib/{transforms => mixins}/map.coffee (72%) rename test/{todo => }/map.coffee (73%) diff --git a/README.md b/README.md index eda520e..150fd64 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It provides three classes of functionality: 2. Functions that take a Readable stream and transform its data, returning a new readable stream: * [`each`](#each) + * [`map`](#map) 3. Functions that allow you to create chains of transformations: @@ -114,7 +115,7 @@ If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). ```javascript var readable = _s.fromArray([3, 4, 5, 6]); -_s.each(readable console.log); +_s.each(readable, console.log); // 3 // 4 // 5 diff --git a/lib/transforms/map.coffee b/lib/mixins/map.coffee similarity index 72% rename from lib/transforms/map.coffee rename to lib/mixins/map.coffee index 9871fab..7cfe4bf 100644 --- a/lib/transforms/map.coffee +++ b/lib/mixins/map.coffee @@ -3,7 +3,7 @@ _ = require 'underscore' debug = require('debug') 'us:map' module.exports = class Map extends Transform - constructor: (@stream_opts, @options) -> + constructor: (@options, @stream_opts) -> super @stream_opts @options = { fn: @options } if _(@options).isFunction() @options._async = @options.fn.length is 2 @@ -17,3 +17,7 @@ module.exports = class Map extends Transform else @push @options.fn(chunk) cb() + +module.exports = + map: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Map options, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index 1c75d49..5951370 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -47,6 +47,7 @@ module.exports = -> "fromString" "toArray" "each" + "map" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/map.coffee b/test/map.coffee similarity index 73% rename from test/todo/map.coffee rename to test/map.coffee index 7835c95..cd64039 100644 --- a/test/todo/map.coffee +++ b/test/map.coffee @@ -1,18 +1,18 @@ assert = require 'assert' async = require 'async' -_ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_ = require 'underscore' +_s = require "#{__dirname}/../index" sinon = require 'sinon' describe '_.map', -> it 'accepts fn (sync/async), calls it on each chunk, then passes the fn result along', (done) -> input = [{a:'1', b:'2'},{c:'2', d:'3'}] synch = (chunk) -> _(chunk).keys() - asynch = (chunk, cb) -> cb null, _(chunk).keys() # TODO: error handling + asynch = (chunk, cb) -> cb null, _(chunk).keys() expected = _(input).map(_.keys) async.forEach [synch, asynch], (fn, cb_fe) -> spy = sinon.spy fn - _(input).stream().map(spy).run (err, result) -> + _s(input).chain().fromArray(input).map(spy).toArray (err, result) -> assert.ifError err assert.deepEqual expected, result assert.equal spy.callCount, 2 From a3f286353c09c8d80dc4188955b0625f294bfb60 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 22:20:16 -0800 Subject: [PATCH 28/45] reduce --- README.md | 48 ++++++++++++++++++++++++ lib/{transforms => mixins}/reduce.coffee | 6 ++- lib/understream.coffee | 1 + test/{todo => }/reduce.coffee | 16 ++++---- 4 files changed, 62 insertions(+), 9 deletions(-) rename lib/{transforms => mixins}/reduce.coffee (77%) rename test/{todo => }/reduce.coffee (70%) diff --git a/README.md b/README.md index 150fd64..96e9103 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It provides three classes of functionality: * [`each`](#each) * [`map`](#map) + * [`reduce`](#map) + 3. Functions that allow you to create chains of transformations: @@ -139,6 +141,52 @@ mapped.on("data", console.log); // 6 ``` +--- +#### reduce `_s.reduce(readable, options)` + +Boils a stream down to a single value. `options` takes in: +* `base`: value or function that represents/returns the initial state of the reduction. +* `fn`: function that takes in two arguments: the current state of the reduction, and a new piece of incoming data, and returns the updated state of the reduction. +* `key`: optional function to apply to incoming data in order to partition the incoming data into separate reductions. + +```javascript +var readable = _s.fromArray([1, 2, 3]); +var reduced = _s.reduce(readable, { + base: 0, + fn: function(a, b) { return a + b; } +}); +reduced.on('data', console.log); +// 6 +``` + +```javascript +var readable = _s.fromArray([ + {a: 1, b: 2}, + {a: 1, b: 3}, + {a: 1, b: 4}, + {a: 2, b: 1}, + {a: 3, b: 2} +]); +var reduced = _s.reduce(readable, { + base: function() { return {}; }, + key: function(new_obj) { return new_obj.a; }, + fn: function(obj, new_obj) { + if (obj.b == null) { + obj = { + a: new_obj.a, + b: [] + }; + } + obj.b.push(new_obj.b); + return obj; + } +}); +reduced.on('data', console.log); +// { a: 1, b: [ 2, 3, 4 ] } +// { a: 2, b: [ 1 ] } +// { a: 3, b: [ 2 ] } +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/transforms/reduce.coffee b/lib/mixins/reduce.coffee similarity index 77% rename from lib/transforms/reduce.coffee rename to lib/mixins/reduce.coffee index d02363d..f411ede 100644 --- a/lib/transforms/reduce.coffee +++ b/lib/mixins/reduce.coffee @@ -3,7 +3,7 @@ _ = require 'underscore' debug = require('debug') 'us:reduce' module.exports = class Reduce extends Transform - constructor: (@stream_opts, @options) -> + constructor: (@options, @stream_opts) -> super @stream_opts # TODO @options._async = _(@options).isFunction and @options.fn.length is 2 if @options.key? @@ -24,3 +24,7 @@ module.exports = class Reduce extends Transform else @_val = @options.fn @_val, chunk cb() + +module.exports = + reduce: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Reduce options, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index 5951370..453c36c 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -48,6 +48,7 @@ module.exports = -> "toArray" "each" "map" + "reduce" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/reduce.coffee b/test/reduce.coffee similarity index 70% rename from test/todo/reduce.coffee rename to test/reduce.coffee index e88dd59..aefcb5f 100644 --- a/test/todo/reduce.coffee +++ b/test/reduce.coffee @@ -1,47 +1,47 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.reduce', -> # fails for node < v0.10.20 due to https://github.com/joyent/node/issues/6183 it 'works with an empty stream with base 0', (done) -> - _([]).stream().reduce + _s(_s.fromArray []).chain().reduce base: 0 fn: (count, item) -> count += 1 - .run (err, data) -> + .toArray (err, data) -> assert.deepEqual data, [0] assert.ifError err done() it 'works on numbers', (done) -> - _([1, 2, 3]).stream().reduce({fn: ((a,b) -> a + b), base: 0}).run (err, data) -> + _s(_s.fromArray [1, 2, 3]).chain().reduce({fn: ((a,b) -> a + b), base: 0}).toArray (err, data) -> assert.ifError err assert.deepEqual data, [6] done() it 'works on objects', (done) -> - _([{a: 1, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}]).stream().reduce( + _s(_s.fromArray [{a: 1, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}]).chain().reduce( base: {} fn: (obj, new_obj) -> obj = { a: new_obj.a, b: [] } unless obj.b? obj.b.push new_obj.b obj - ).run (err, data) -> + ).toArray (err, data) -> assert.ifError err assert.deepEqual data, [{ a: 1, b: [2,3,4] }] done() it 'works with multiple bases', (done) -> - _([{a: 1, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}, {a: 2, b: 1}, {a: 3, b: 2}]).stream().reduce( + _s(_s.fromArray [{a: 1, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}, {a: 2, b: 1}, {a: 3, b: 2}]).chain().reduce( base: () -> {} key: (new_obj) -> new_obj.a fn: (obj, new_obj) -> obj = { a: new_obj.a, b: [] } unless obj.b? obj.b.push new_obj.b obj - ).run (err, data) -> + ).toArray (err, data) -> assert.ifError err assert.deepEqual data, [{a: 1, b: [2,3,4]}, {a: 2, b:[1]}, {a: 3, b: [2]}] done() From c309e6e00887b0a5c3bfdf655f29e832679f1704 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 22:21:39 -0800 Subject: [PATCH 29/45] readme: fix numbering --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 96e9103..472e0b8 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,12 @@ It provides three classes of functionality: * [`toArray`](#toArray) 2. Functions that take a Readable stream and transform its data, returning a new readable stream: - * [`each`](#each) * [`map`](#map) * [`reduce`](#map) 3. Functions that allow you to create chains of transformations: - * [`chain`](#chain) * [`value`](#value) From d256495ffbde888de140cd01c1485c027fff87c9 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 22:23:16 -0800 Subject: [PATCH 30/45] readme: fix anchor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 472e0b8..0951f07 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ It provides three classes of functionality: 2. Functions that take a Readable stream and transform its data, returning a new readable stream: * [`each`](#each) * [`map`](#map) - * [`reduce`](#map) + * [`reduce`](#reduce) 3. Functions that allow you to create chains of transformations: From 3fa1b7572ba59b25bb28284f27c7611ee4d40472 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 15 Nov 2013 22:40:36 -0800 Subject: [PATCH 31/45] filter --- README.md | 20 +++++++++++++++++++- lib/mixins/each.coffee | 4 +--- lib/{transforms => mixins}/filter.coffee | 6 +++++- lib/mixins/map.coffee | 2 +- lib/mixins/reduce.coffee | 2 +- lib/understream.coffee | 1 + test/{todo => }/filter.coffee | 4 ++-- 7 files changed, 30 insertions(+), 9 deletions(-) rename lib/{transforms => mixins}/filter.coffee (71%) rename test/{todo => }/filter.coffee (86%) diff --git a/README.md b/README.md index 0951f07..526f460 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It provides three classes of functionality: * [`each`](#each) * [`map`](#map) * [`reduce`](#reduce) - + * [`filter`](#filter) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -185,6 +185,24 @@ reduced.on('data', console.log); // { a: 3, b: [ 2 ] } ``` +--- +#### filter `_s.filter(readable, iterator)` + +Returns a readable stream that emits all data from `readable` that passes `iterator`. +If it has only one argument, `iterator` is assumed to be synchronous. +If it has two arguments, it is assumed to return its result asynchronously. + +```javascript +var readable = _s.fromArray([1, 2, 3, 4]); +var filtered = _s.filter(readable, function(num) { return num % 2 === 0 }); +// var filtered = _s.filter(readable, function(num, cb) { +// setTimeout(function() { cb(null, num % 2 === 0); }, 1000); +// }); +filtered.on('data', console.log); +// 2 +// 4 +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/mixins/each.coffee b/lib/mixins/each.coffee index e3d59bb..53deffd 100644 --- a/lib/mixins/each.coffee +++ b/lib/mixins/each.coffee @@ -20,6 +20,4 @@ class Each extends Transform module.exports = each: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - each = new Each options, stream_opts - readable.pipe each - each + readable.pipe(new Each options, stream_opts) diff --git a/lib/transforms/filter.coffee b/lib/mixins/filter.coffee similarity index 71% rename from lib/transforms/filter.coffee rename to lib/mixins/filter.coffee index df1916a..921f5fa 100644 --- a/lib/transforms/filter.coffee +++ b/lib/mixins/filter.coffee @@ -3,7 +3,7 @@ _ = require 'underscore' debug = require('debug') 'us:filter' module.exports = class Filter extends Transform - constructor: (@stream_opts, @options) -> + constructor: (@options, @stream_opts) -> super @stream_opts @options = { fn: @options } if _(@options).isFunction() @options._async = true if @options.fn.length is 2 @@ -16,3 +16,7 @@ module.exports = class Filter extends Transform else @push chunk if @options.fn chunk cb() + +module.exports = + filter: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Filter options, stream_opts) diff --git a/lib/mixins/map.coffee b/lib/mixins/map.coffee index 7cfe4bf..79702f0 100644 --- a/lib/mixins/map.coffee +++ b/lib/mixins/map.coffee @@ -2,7 +2,7 @@ _ = require 'underscore' debug = require('debug') 'us:map' -module.exports = class Map extends Transform +class Map extends Transform constructor: (@options, @stream_opts) -> super @stream_opts @options = { fn: @options } if _(@options).isFunction() diff --git a/lib/mixins/reduce.coffee b/lib/mixins/reduce.coffee index f411ede..e6961b0 100644 --- a/lib/mixins/reduce.coffee +++ b/lib/mixins/reduce.coffee @@ -2,7 +2,7 @@ _ = require 'underscore' debug = require('debug') 'us:reduce' -module.exports = class Reduce extends Transform +class Reduce extends Transform constructor: (@options, @stream_opts) -> super @stream_opts # TODO @options._async = _(@options).isFunction and @options.fn.length is 2 diff --git a/lib/understream.coffee b/lib/understream.coffee index 453c36c..f661a8b 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -49,6 +49,7 @@ module.exports = -> "each" "map" "reduce" + "filter" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/filter.coffee b/test/filter.coffee similarity index 86% rename from test/todo/filter.coffee rename to test/filter.coffee index 5432415..e817b54 100644 --- a/test/todo/filter.coffee +++ b/test/filter.coffee @@ -1,7 +1,7 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" sinon = require 'sinon' describe '_.filter', -> @@ -12,7 +12,7 @@ describe '_.filter', -> expected = input.slice 0, 1 async.forEach [synch, asynch], (fn, cb_fe) -> spy = sinon.spy fn - _(input).stream().filter(spy).run (err, result) -> + _s(_s.fromArray input).chain().filter(spy).toArray (err, result) -> assert.ifError err assert.deepEqual expected, result assert.equal spy.callCount, 2 From 6d25b68775c26e57f861e00c3d590227ff308cac Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 07:25:54 -0800 Subject: [PATCH 32/45] where --- README.md | 20 ++++++++++++++++++++ lib/{transforms => mixins}/where.coffee | 8 ++++++-- lib/understream.coffee | 1 + test/{todo => }/where.coffee | 4 ++-- 4 files changed, 29 insertions(+), 4 deletions(-) rename lib/{transforms => mixins}/where.coffee (60%) rename test/{todo => }/where.coffee (71%) diff --git a/README.md b/README.md index 526f460..b1ad800 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ It provides three classes of functionality: * [`map`](#map) * [`reduce`](#reduce) * [`filter`](#filter) + * [`where`](#where) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -203,6 +204,25 @@ filtered.on('data', console.log); // 4 ``` +--- +#### where `_s.where(readable, attrs)` + +Filters `readable` to emit only objects that contain the attributes in the `attrs` object. + +```javascript +var readable = _s.fromArray([ + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 1, b: 3}, + {a: 1, b: 4} +]) +var whered = _s.where(readable, {a:1}); +whered.on('data', console.log); +// { a: 1, b: 2 } +// { a: 1, b: 3 } +// { a: 1, b: 4 } +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/transforms/where.coffee b/lib/mixins/where.coffee similarity index 60% rename from lib/transforms/where.coffee rename to lib/mixins/where.coffee index f865884..fbc8afd 100644 --- a/lib/transforms/where.coffee +++ b/lib/mixins/where.coffee @@ -3,10 +3,14 @@ _ = require 'underscore' debug = require('debug') 'us:invoke' # TODO: support args (might be different than underscore API due to arg length logic in lib/understream) -module.exports = class Where extends Transform - constructor: (@stream_opts, @attrs) -> +class Where extends Transform + constructor: (@attrs, @stream_opts) -> super @stream_opts _transform: (chunk, encoding, cb) => for key, val of @attrs return cb() if val isnt chunk[key] cb null, chunk + +module.exports = + where: (readable, attrs, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Where attrs, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index f661a8b..e366dc0 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -50,6 +50,7 @@ module.exports = -> "map" "reduce" "filter" + "where" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/where.coffee b/test/where.coffee similarity index 71% rename from test/todo/where.coffee rename to test/where.coffee index 4b63abb..3d87737 100644 --- a/test/todo/where.coffee +++ b/test/where.coffee @@ -1,12 +1,12 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.where', -> it 'works', (done) -> input = [{a: 1, b: 2}, {a: 2, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}] - _(input).stream().where({a: 1}).run (err, result) -> + _s(_s.fromArray input).chain().where({a: 1}).toArray (err, result) -> assert.ifError err assert.deepEqual result, _(input).where({a: 1}) done() From c32e13bede309792ca5bb3897bbe5d99fb3738d7 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 07:34:13 -0800 Subject: [PATCH 33/45] invoke --- README.md | 17 +++++++++++++++++ lib/{transforms => mixins}/invoke.coffee | 8 ++++++-- lib/understream.coffee | 1 + test/{todo => }/invoke.coffee | 4 ++-- 4 files changed, 26 insertions(+), 4 deletions(-) rename lib/{transforms => mixins}/invoke.coffee (56%) rename test/{todo => }/invoke.coffee (70%) diff --git a/README.md b/README.md index b1ad800..a30e5ba 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ It provides three classes of functionality: * [`reduce`](#reduce) * [`filter`](#filter) * [`where`](#where) + * [`invoke`](#invoke) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -223,6 +224,22 @@ whered.on('data', console.log); // { a: 1, b: 4 } ``` +--- +#### invoke `_s.invoke(readable, method)` + +Returns a stream that emits the results of invoking `method` on every object in `readable`. + +```javascript +var readable = _s.fromArray([ + {m: function() { return 1; }}, + {m: function() { return 2; }} +]) +var invoked = _s.invoke(readable, 'm'); +invoked.on('data', console.log); +// 1 +// 2 +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/transforms/invoke.coffee b/lib/mixins/invoke.coffee similarity index 56% rename from lib/transforms/invoke.coffee rename to lib/mixins/invoke.coffee index 9f01f7a..0600859 100644 --- a/lib/transforms/invoke.coffee +++ b/lib/mixins/invoke.coffee @@ -3,8 +3,12 @@ _ = require 'underscore' debug = require('debug') 'us:invoke' # TODO: support args (might be different than underscore API due to arg length logic in lib/understream) -module.exports = class Invoke extends Transform - constructor: (@stream_opts, @method) -> +class Invoke extends Transform + constructor: (@method, @stream_opts) -> super @stream_opts _transform: (chunk, encoding, cb) => cb null, chunk[@method].apply(chunk) + +module.exports = + invoke: (readable, method, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Invoke method, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index e366dc0..c604f81 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -51,6 +51,7 @@ module.exports = -> "reduce" "filter" "where" + "invoke" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/invoke.coffee b/test/invoke.coffee similarity index 70% rename from test/todo/invoke.coffee rename to test/invoke.coffee index c3a4a75..3e8a495 100644 --- a/test/todo/invoke.coffee +++ b/test/invoke.coffee @@ -1,12 +1,12 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.invoke', -> it 'works', (done) -> input = [{m: () -> '1'}, {m: () -> '2'}] - _(input).stream().invoke('m').run (err, result) -> + _s(_s.fromArray input).chain().invoke('m').toArray (err, result) -> assert.ifError err assert.deepEqual result, _(input).invoke('m') done() From d442ee9d02e92eb378b0df3775910549ab70e8f3 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 07:49:03 -0800 Subject: [PATCH 34/45] groupBy --- README.md | 25 +++++++++++++++++++++++ lib/{transforms => mixins}/groupBy.coffee | 8 ++++++-- lib/understream.coffee | 1 + test/{todo => }/groupBy.coffee | 12 +++++------ 4 files changed, 38 insertions(+), 8 deletions(-) rename lib/{transforms => mixins}/groupBy.coffee (78%) rename test/{todo => }/groupBy.coffee (57%) diff --git a/README.md b/README.md index a30e5ba..96a030a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ It provides three classes of functionality: * [`filter`](#filter) * [`where`](#where) * [`invoke`](#invoke) + * [`groupBy`](#groupBy) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -240,6 +241,30 @@ invoked.on('data', console.log); // 2 ``` +--- +#### groupBy `_s.groupBy(readable, options)` + +When `options` is a function, creates a stream that will emit an object representing the groupings of the data in `readable` partitioned by the function. + +```javascript +var readable = _s.fromArray([1.3, 2.1, 2.4]); +var grouped = _s.groupBy(readable, Math.floor); +grouped.on('data', console.log); +// { '1': [ 1.3 ], '2': [ 2.1, 2.4 ] } +``` + +Alternatively, `options` can be an object containing the following keys: +* `fn`: the function to apply to data coming through `readable`. +* `unpack`: emit each grouping as a separate object. + +```javascript +var readable = _s.fromArray([1.3, 2.1, 2.4]); +var grouped = _s.groupBy(readable, {fn: Math.floor, unpack: true}); +grouped.on('data', console.log); +// { '1': [ 1.3 ] } +// { '2': [ 2.1, 2.4 ] } +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/transforms/groupBy.coffee b/lib/mixins/groupBy.coffee similarity index 78% rename from lib/transforms/groupBy.coffee rename to lib/mixins/groupBy.coffee index 31edb22..5da3265 100644 --- a/lib/transforms/groupBy.coffee +++ b/lib/mixins/groupBy.coffee @@ -2,8 +2,8 @@ _ = require 'underscore' debug = require('debug') 'us:groupBy' -module.exports = class GroupBy extends Transform - constructor: (@stream_opts, @options) -> +class GroupBy extends Transform + constructor: (@options, @stream_opts) -> super @stream_opts @options = switch when _(@options).isFunction() @@ -33,3 +33,7 @@ module.exports = class GroupBy extends Transform @options.fn chunk, (err, hash) => return cb err if err add hash + +module.exports = + groupBy: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new GroupBy options, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index c604f81..ba54b3f 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -52,6 +52,7 @@ module.exports = -> "filter" "where" "invoke" + "groupBy" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/groupBy.coffee b/test/groupBy.coffee similarity index 57% rename from test/todo/groupBy.coffee rename to test/groupBy.coffee index 84bccc3..c9e3201 100644 --- a/test/todo/groupBy.coffee +++ b/test/groupBy.coffee @@ -1,35 +1,35 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.groupBy', -> it 'fn', (done) -> - _([1.3, 2.1, 2.4]).stream().groupBy(Math.floor).run (err, data) -> + _s(_s.fromArray [1.3, 2.1, 2.4]).chain().groupBy(Math.floor).toArray (err, data) -> assert.ifError err assert.deepEqual data, [{1: [1.3], 2: [2.1, 2.4]}] done() it 'string', (done) -> - _(['one', 'two', 'three']).stream().groupBy('length').run (err, data) -> + _s(_s.fromArray ['one', 'two', 'three']).chain().groupBy('length').toArray (err, data) -> assert.ifError err assert.deepEqual [{3: ["one", "two"], 5: ["three"]}], data done() it 'can unpack into > 1 object', (done) -> - _([1.3, 2.1, 2.4]).stream().groupBy({ fn: Math.floor, unpack: true }).run (err, data) -> + _s(_s.fromArray [1.3, 2.1, 2.4]).chain().groupBy({ fn: Math.floor, unpack: true }).toArray (err, data) -> assert.ifError err assert.deepEqual data, [ {'1':[1.3]}, {'2':[2.1,2.4]} ] done() it 'supports an async function, not unpacked', (done) -> - _([1.3, 2.1, 2.4]).stream().groupBy({ fn: ((num, cb) -> cb null, Math.floor num) }).run (err, data) -> + _s(_s.fromArray [1.3, 2.1, 2.4]).chain().groupBy({ fn: ((num, cb) -> cb null, Math.floor num) }).toArray (err, data) -> assert.ifError err assert.deepEqual data, [ {1: [1.3], 2: [2.1,2.4]} ] done() it 'supports an async function, unpacked', (done) -> - _([1.3, 2.1, 2.4]).stream().groupBy({ fn: ((num, cb) -> cb null, Math.floor num), unpack: true }).run (err, data) -> + _s(_s.fromArray [1.3, 2.1, 2.4]).chain().groupBy({ fn: ((num, cb) -> cb null, Math.floor num), unpack: true }).toArray (err, data) -> assert.ifError err assert.deepEqual data, [ {'1':[1.3]}, {'2':[2.1,2.4]} ] done() From 661089cf5666559dc505e6bbd8e01ed528b8e9a2 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 08:00:21 -0800 Subject: [PATCH 35/45] first --- README.md | 16 ++++++++++++++++ lib/{transforms => mixins}/first.coffee | 6 +++++- lib/understream.coffee | 1 + test/{todo => }/first.coffee | 11 +++++++---- 4 files changed, 29 insertions(+), 5 deletions(-) rename lib/{transforms => mixins}/first.coffee (53%) rename test/{todo => }/first.coffee (68%) diff --git a/README.md b/README.md index 96a030a..a545772 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It provides three classes of functionality: * [`where`](#where) * [`invoke`](#invoke) * [`groupBy`](#groupBy) + * [`first`](#first) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -265,6 +266,21 @@ grouped.on('data', console.log); // { '2': [ 2.1, 2.4 ] } ``` +--- +#### first `_s.first(readable[, n])` + +Returns a stream that only emits the first `n` objects in `readable`. +`n` equals 1 by default. + +```javascript +var readable = _s.fromArray([1, 2, 3, 4, 5]); +var first = _s.first(readable, 3); +first.on('data', console.log); +// 1 +// 2 +// 3 +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/transforms/first.coffee b/lib/mixins/first.coffee similarity index 53% rename from lib/transforms/first.coffee rename to lib/mixins/first.coffee index 052e2ec..15de905 100644 --- a/lib/transforms/first.coffee +++ b/lib/mixins/first.coffee @@ -1,7 +1,7 @@ {Transform} = require 'stream' module.exports = class First extends Transform - constructor: (stream_opts, @first=1) -> + constructor: (@first=1, stream_opts) -> super stream_opts @seen = 0 _transform: (chunk, encoding, cb) => @@ -10,3 +10,7 @@ module.exports = class First extends Transform @push null return cb null, chunk + +module.exports = + first: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new First options, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index ba54b3f..c7ee572 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -53,6 +53,7 @@ module.exports = -> "where" "invoke" "groupBy" + "first" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/first.coffee b/test/first.coffee similarity index 68% rename from test/todo/first.coffee rename to test/first.coffee index 34e008f..6e8294e 100644 --- a/test/todo/first.coffee +++ b/test/first.coffee @@ -1,13 +1,13 @@ _ = require 'underscore' assert = require 'assert' async = require 'async' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.first', -> # fails for node < v0.10.20 due to https://github.com/joyent/node/issues/6183 it 'sends through all objects if limit > size of stream', (done) -> input = [0..10] - _(input).stream().first(100).run (err, result) -> + _s(_s.fromArray input).chain().first(100).toArray (err, result) -> assert.ifError err assert.deepEqual result, input done() @@ -15,7 +15,7 @@ describe '_.first', -> it 'sends through limit objects if limit < size of stream', (done) -> LIMIT = 5 input = [0..10] - _(input).stream().first(LIMIT).run (err, result) -> + _s(_s.fromArray input).chain().first(LIMIT).toArray (err, result) -> assert.ifError err assert.deepEqual result, _(input).first(LIMIT) done() @@ -25,7 +25,10 @@ describe '_.first', -> HIGHWATERMARK = 1 input = [0..100] seen = 0 - _(input).stream().defaults(objectMode: true, highWaterMark: HIGHWATERMARK).each(-> seen++).first(LIMIT).run (err, result) -> + _s(_s.fromArray input).chain() + .each((-> seen++), {objectMode: true, highWaterMark: HIGHWATERMARK}) + .first(LIMIT, {objectMode: true, highWaterMark: HIGHWATERMARK}) + .toArray (err, result) -> assert.ifError err assert.equal seen, LIMIT+HIGHWATERMARK*2 # 1 highWaterMark for buffering in first, 1 highWaterMark for buffering in each done() From c8c4f270e588233ba33e3f9b8072eba21e2d76af Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 08:05:47 -0800 Subject: [PATCH 36/45] rest --- README.md | 15 +++++++++++++++ lib/mixins/rest.coffee | 14 ++++++++++++++ lib/transforms/rest.coffee | 10 ---------- lib/understream.coffee | 1 + test/{todo => }/rest.coffee | 6 +++--- 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 lib/mixins/rest.coffee delete mode 100644 lib/transforms/rest.coffee rename test/{todo => }/rest.coffee (71%) diff --git a/README.md b/README.md index a545772..7d287bc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ It provides three classes of functionality: * [`invoke`](#invoke) * [`groupBy`](#groupBy) * [`first`](#first) + * [`rest`](#rest) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -281,6 +282,20 @@ first.on('data', console.log); // 3 ``` +--- +#### rest `_s.rest(readable[, n])` + +Returns a stream that skips over the first `n` objects in `readable`. +`n` equals 1 by default. + +```javascript +var readable = _s.fromArray([1, 2, 3, 4, 5]); +var rest = _s.rest(readable, 3); +rest.on('data', console.log); +// 4 +// 5 +``` + --- #### chain `_s.chain(obj)` diff --git a/lib/mixins/rest.coffee b/lib/mixins/rest.coffee new file mode 100644 index 0000000..65381f6 --- /dev/null +++ b/lib/mixins/rest.coffee @@ -0,0 +1,14 @@ +{Transform} = require 'stream' + +class Rest extends Transform + constructor: (@rest=1, stream_opts) -> + super stream_opts + @seen = -1 + _transform: (chunk, encoding, cb) => + @seen++ + return cb() if @seen < @rest + cb null, chunk + +module.exports = + rest: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Rest options, stream_opts) diff --git a/lib/transforms/rest.coffee b/lib/transforms/rest.coffee deleted file mode 100644 index c061ac9..0000000 --- a/lib/transforms/rest.coffee +++ /dev/null @@ -1,10 +0,0 @@ -{Transform} = require 'stream' - -module.exports = class Rest extends Transform - constructor: (stream_opts, @rest=1) -> - super stream_opts - @seen = -1 - _transform: (chunk, encoding, cb) => - @seen++ - return cb() if @seen < @rest - cb null, chunk diff --git a/lib/understream.coffee b/lib/understream.coffee index c7ee572..a147d42 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -54,6 +54,7 @@ module.exports = -> "invoke" "groupBy" "first" + "rest" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/rest.coffee b/test/rest.coffee similarity index 71% rename from test/todo/rest.coffee rename to test/rest.coffee index fcfa736..1fb1afd 100644 --- a/test/todo/rest.coffee +++ b/test/rest.coffee @@ -1,13 +1,13 @@ _ = require 'underscore' assert = require 'assert' async = require 'async' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.rest', -> it 'skips some objects if skip < size of stream', (done) -> SKIP = 5 input = [0..10] - _(input).stream().rest(SKIP).run (err, result) -> + _s(_s.fromArray input).chain().rest(SKIP).toArray (err, result) -> assert.ifError err assert.deepEqual result, _(input).rest(SKIP) done() @@ -15,7 +15,7 @@ describe '_.rest', -> it 'skips all objects if skip size > size of stream', (done) -> SKIP = 100 input = [0..10] - _(input).stream().rest(SKIP).run (err, result) -> + _s(_s.fromArray input).chain().rest(SKIP).toArray (err, result) -> assert.ifError err assert.deepEqual result, [] done() From f07c5c540850b8ca77f274d158679cdcbd9b721a Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 08:16:14 -0800 Subject: [PATCH 37/45] flatten --- README.md | 55 ++++++++++++---------------------- lib/mixins/flatten.coffee | 14 +++++++++ lib/transforms/flatten.coffee | 10 ------- lib/understream.coffee | 1 + test/{todo => }/flatten.coffee | 4 +-- 5 files changed, 36 insertions(+), 48 deletions(-) create mode 100644 lib/mixins/flatten.coffee delete mode 100644 lib/transforms/flatten.coffee rename test/{todo => }/flatten.coffee (86%) diff --git a/README.md b/README.md index 7d287bc..fac60ff 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ It provides three classes of functionality: * [`groupBy`](#groupBy) * [`first`](#first) * [`rest`](#rest) + * [`flatten`](#flatten) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -296,6 +297,24 @@ rest.on('data', console.log); // 5 ``` +--- +#### flatten `_s.flatten(readable[, shallow])` + +Returns a stream that unpacks any arrays into their individual elements. +By default `shallow` is false, and all nested arrays are also unpacked. + +```javascript +var readable = _s.fromArray([1, 2, [3], [4], [5, [6]]]); +var flatten = _s.flatten(readable); +flatten.on('data', console.log); +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 +``` + --- #### chain `_s.chain(obj)` @@ -325,11 +344,6 @@ var readable = _s.chain(_s.fromArray([3, 4, 5, 6])).value(); \ No newline at end of file diff --git a/lib/mixins/flatten.coffee b/lib/mixins/flatten.coffee new file mode 100644 index 0000000..93c0341 --- /dev/null +++ b/lib/mixins/flatten.coffee @@ -0,0 +1,14 @@ +_ = require 'underscore' +{Transform} = require 'stream' + +class Flatten extends Transform + constructor: (@shallow=false, stream_opts) -> super stream_opts + _transform: (chunk, enc, cb) => + return cb null, chunk unless _(chunk).isArray() + els = if @shallow then chunk else _(chunk).flatten() + @push el for el in els + cb() + +module.exports = + flatten: (readable, shallow, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Flatten shallow, stream_opts) diff --git a/lib/transforms/flatten.coffee b/lib/transforms/flatten.coffee deleted file mode 100644 index 1a56c44..0000000 --- a/lib/transforms/flatten.coffee +++ /dev/null @@ -1,10 +0,0 @@ -_ = require 'underscore' -{Transform} = require 'stream' - -module.exports = class Flatten extends Transform - constructor: (stream_opts, @shallow=false) -> super stream_opts - _transform: (chunk, enc, cb) => - return cb null, chunk unless _(chunk).isArray() - els = if @shallow then chunk else _(chunk).flatten() - @push el for el in els - cb() diff --git a/lib/understream.coffee b/lib/understream.coffee index a147d42..2b6e172 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -55,6 +55,7 @@ module.exports = -> "groupBy" "first" "rest" + "flatten" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/flatten.coffee b/test/flatten.coffee similarity index 86% rename from test/todo/flatten.coffee rename to test/flatten.coffee index 0e8ee34..be83a9b 100644 --- a/test/todo/flatten.coffee +++ b/test/flatten.coffee @@ -1,11 +1,11 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" match_underscore = (fn, input, args, cb) -> [fn, input, args, cb] = [fn, input, [], args] if arguments.length is 3 - _(input).stream()[fn](args...).run (err, result) -> + _s(_s.fromArray input).chain()[fn](args...).toArray (err, result) -> assert.ifError err assert.deepEqual result, _(input)[fn](args...) cb() From 49a044d12940d7e134eb7eea71e484672a7675b8 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 08:34:22 -0800 Subject: [PATCH 38/45] uniq --- README.md | 21 +++++++++++++++++++-- lib/{transforms => mixins}/uniq.coffee | 18 +++++++++++------- lib/understream.coffee | 1 + test/{todo => }/uniq.coffee | 20 ++++++++++---------- 4 files changed, 41 insertions(+), 19 deletions(-) rename lib/{transforms => mixins}/uniq.coffee (61%) rename test/{todo => }/uniq.coffee (68%) diff --git a/README.md b/README.md index fac60ff..9e5fe9d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ It provides three classes of functionality: * [`first`](#first) * [`rest`](#rest) * [`flatten`](#flatten) + * [`uniq`](#uniq) 3. Functions that allow you to create chains of transformations: * [`chain`](#chain) @@ -315,6 +316,24 @@ flatten.on('data', console.log); // 6 ``` +--- +#### uniq `_s.uniq(readable[, sorted, hash_fn])` + +Returns a stream that emits the unique elements of `readable`. +Assumes the input is unsorted unless `sorted` is set to true. +Uses builtin comparison unless `hash_fn` is specified. +Alternatively you can specify one argument containing both parameters: `{sorted: ..., hash_fn: ...}`. + +```javascript +var readable = _s.fromArray([4, 4, 3, 2, 1]) +var uniq = _s.uniq(readable); +uniq.on('data', console.log); +// 4 +// 3 +// 2 +// 1 +``` + --- #### chain `_s.chain(obj)` @@ -381,6 +400,4 @@ _.stream().file(path_to_file).split('\n').each(console.log).run() ### Split -### Uniq - --!> \ No newline at end of file diff --git a/lib/transforms/uniq.coffee b/lib/mixins/uniq.coffee similarity index 61% rename from lib/transforms/uniq.coffee rename to lib/mixins/uniq.coffee index 869111a..ef50777 100644 --- a/lib/transforms/uniq.coffee +++ b/lib/mixins/uniq.coffee @@ -2,8 +2,8 @@ _ = require 'underscore' {Transform} = require 'stream' class SortedUniq extends Transform - constructor: (@stream_opts, @hash_fn) -> - super @stream_opts + constructor: (@hash_fn, stream_opts) -> + super stream_opts @last = null _transform: (obj, encoding, cb) => hash = @hash_fn obj @@ -12,8 +12,8 @@ class SortedUniq extends Transform cb null, obj class UnsortedUniq extends Transform - constructor: (@stream_opts, @hash_fn) -> - super @stream_opts + constructor: (@hash_fn, stream_opts) -> + super stream_opts @seen = {} _transform: (obj, encoding, cb) => hash = @hash_fn obj @@ -21,8 +21,8 @@ class UnsortedUniq extends Transform @seen[hash] = true cb null, obj -module.exports = class Uniq - constructor: (stream_opts, sorted, hash_fn) -> +class Uniq + constructor: (sorted, hash_fn, stream_opts) -> if _(sorted).isFunction() # For underscore-style arguments hash_fn = sorted sorted = false @@ -30,4 +30,8 @@ module.exports = class Uniq hash_fn = sorted.hash_fn sorted = sorted.sorted hash_fn ?= String - return new (if sorted then SortedUniq else UnsortedUniq) stream_opts, hash_fn + return new (if sorted then SortedUniq else UnsortedUniq) hash_fn, stream_opts + +module.exports = + uniq: (readable, sorted, hash_fn, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Uniq sorted, hash_fn, stream_opts) diff --git a/lib/understream.coffee b/lib/understream.coffee index 2b6e172..a346c7f 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -56,6 +56,7 @@ module.exports = -> "first" "rest" "flatten" + "uniq" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/todo/uniq.coffee b/test/uniq.coffee similarity index 68% rename from test/todo/uniq.coffee rename to test/uniq.coffee index 1e7193f..1c0671a 100644 --- a/test/todo/uniq.coffee +++ b/test/uniq.coffee @@ -1,6 +1,6 @@ _ = require 'underscore' assert = require 'assert' -_.mixin require("#{__dirname}/../index").exports() +_s = require "#{__dirname}/../index" describe '_.uniq', -> expected = [1...4] @@ -11,24 +11,24 @@ describe '_.uniq', -> describe 'underscore-style arguments', -> it 'works without a hash function', (done) -> - _(sorted_input).stream().uniq(true).run (err, result) -> + _s(_s.fromArray sorted_input).chain().uniq(true).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() it 'works with a hash function', (done) -> - _(sorted_input).stream().uniq(true, String).run (err, result) -> + _s(_s.fromArray sorted_input).chain().uniq(true, String).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() describe 'options object', -> it 'works without a hash function', (done) -> - _(sorted_input).stream().uniq(sorted: true).run (err, result) -> + _s(_s.fromArray sorted_input).chain().uniq(sorted: true).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() it 'works with a hash function', (done) -> - _(sorted_input).stream().uniq({sorted: true, hash_fn: String}).run (err, result) -> + _s(_s.fromArray sorted_input).chain().uniq({sorted: true, hash_fn: String}).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() @@ -39,31 +39,31 @@ describe '_.uniq', -> describe 'no arguments', -> it 'works', (done) -> - _(unsorted_input).stream().uniq().run (err, result) -> + _s(_s.fromArray unsorted_input).chain().uniq().toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() describe 'underscore-style arguments', -> it 'works with a hash function', (done) -> - _(unsorted_input).stream().uniq(String).run (err, result) -> + _s(_s.fromArray unsorted_input).chain().uniq(String).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() it 'gives invalid results with sorted true', (done) -> - _(unsorted_input).stream().uniq(true).run (err, result) -> + _s(_s.fromArray unsorted_input).chain().uniq(true).toArray (err, result) -> assert.ifError err assert.deepEqual result, unsorted_input done() describe 'options object', -> it 'works with a hash function', (done) -> - _(unsorted_input).stream().uniq(hash_fn: String).run (err, result) -> + _s(_s.fromArray unsorted_input).chain().uniq(hash_fn: String).toArray (err, result) -> assert.ifError err assert.deepEqual result, expected done() it 'gives invalid result with sorted true', (done) -> - _(unsorted_input).stream().uniq(sorted: true).run (err, result) -> + _s(_s.fromArray unsorted_input).chain().uniq(sorted: true).toArray (err, result) -> assert.ifError err assert.deepEqual result, unsorted_input done() From 245b5abdbea49fd4f3275c5fb27a5fadf6f4eedd Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 08:52:43 -0800 Subject: [PATCH 39/45] add aliases cc @jonahkagan not tested...not sure the best way to test it. Could duplicate all existing tests and run them under aliases, but that seems like overkill. --- README.md | 14 ++++++++++++++ lib/mixins/each.coffee | 6 ++++-- lib/mixins/filter.coffee | 7 +++++-- lib/mixins/first.coffee | 8 ++++++-- lib/mixins/map.coffee | 7 +++++-- lib/mixins/reduce.coffee | 8 ++++++-- lib/mixins/rest.coffee | 8 ++++++-- lib/mixins/uniq.coffee | 7 +++++-- 8 files changed, 51 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9e5fe9d..8a575b2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ _s.each(readable, console.log); // 6 ``` +*aliases*: `forEach` + --- #### map `_s.map(readable, iterator)` @@ -146,6 +148,8 @@ mapped.on("data", console.log); // 6 ``` +*aliases*: `collect` + --- #### reduce `_s.reduce(readable, options)` @@ -192,6 +196,8 @@ reduced.on('data', console.log); // { a: 3, b: [ 2 ] } ``` +*aliases*: `inject`, `foldl` + --- #### filter `_s.filter(readable, iterator)` @@ -210,6 +216,8 @@ filtered.on('data', console.log); // 4 ``` +*aliases*: `select` + --- #### where `_s.where(readable, attrs)` @@ -284,6 +292,8 @@ first.on('data', console.log); // 3 ``` +*aliases*: `head`, `take` + --- #### rest `_s.rest(readable[, n])` @@ -298,6 +308,8 @@ rest.on('data', console.log); // 5 ``` +*aliases*: `tail`, `drop` + --- #### flatten `_s.flatten(readable[, shallow])` @@ -334,6 +346,8 @@ uniq.on('data', console.log); // 1 ``` +*aliases*: `unique` + --- #### chain `_s.chain(obj)` diff --git a/lib/mixins/each.coffee b/lib/mixins/each.coffee index 53deffd..31ed33b 100644 --- a/lib/mixins/each.coffee +++ b/lib/mixins/each.coffee @@ -18,6 +18,8 @@ class Each extends Transform @push chunk cb() +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Each options, stream_opts) module.exports = - each: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Each options, stream_opts) + each: fn + forEach: fn diff --git a/lib/mixins/filter.coffee b/lib/mixins/filter.coffee index 921f5fa..8c361bc 100644 --- a/lib/mixins/filter.coffee +++ b/lib/mixins/filter.coffee @@ -17,6 +17,9 @@ module.exports = class Filter extends Transform @push chunk if @options.fn chunk cb() +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Filter options, stream_opts) + module.exports = - filter: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Filter options, stream_opts) + filter: fn + select: fn diff --git a/lib/mixins/first.coffee b/lib/mixins/first.coffee index 15de905..217e859 100644 --- a/lib/mixins/first.coffee +++ b/lib/mixins/first.coffee @@ -11,6 +11,10 @@ module.exports = class First extends Transform return cb null, chunk +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new First options, stream_opts) + module.exports = - first: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new First options, stream_opts) + first: fn + head: fn + take: fn diff --git a/lib/mixins/map.coffee b/lib/mixins/map.coffee index 79702f0..45b3fe3 100644 --- a/lib/mixins/map.coffee +++ b/lib/mixins/map.coffee @@ -18,6 +18,9 @@ class Map extends Transform @push @options.fn(chunk) cb() +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Map options, stream_opts) + module.exports = - map: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Map options, stream_opts) + map: fn + collect: fn diff --git a/lib/mixins/reduce.coffee b/lib/mixins/reduce.coffee index e6961b0..5b91a91 100644 --- a/lib/mixins/reduce.coffee +++ b/lib/mixins/reduce.coffee @@ -25,6 +25,10 @@ class Reduce extends Transform @_val = @options.fn @_val, chunk cb() +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Reduce options, stream_opts) + module.exports = - reduce: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Reduce options, stream_opts) + reduce: fn + inject: fn + foldl: fn diff --git a/lib/mixins/rest.coffee b/lib/mixins/rest.coffee index 65381f6..392df28 100644 --- a/lib/mixins/rest.coffee +++ b/lib/mixins/rest.coffee @@ -9,6 +9,10 @@ class Rest extends Transform return cb() if @seen < @rest cb null, chunk +fn = (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Rest options, stream_opts) + module.exports = - rest: (readable, options, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Rest options, stream_opts) + rest: fn + tail: fn + drop: fn diff --git a/lib/mixins/uniq.coffee b/lib/mixins/uniq.coffee index ef50777..de47025 100644 --- a/lib/mixins/uniq.coffee +++ b/lib/mixins/uniq.coffee @@ -32,6 +32,9 @@ class Uniq hash_fn ?= String return new (if sorted then SortedUniq else UnsortedUniq) hash_fn, stream_opts +fn = (readable, sorted, hash_fn, stream_opts={objectMode:readable._readableState.objectMode}) -> + readable.pipe(new Uniq sorted, hash_fn, stream_opts) + module.exports = - uniq: (readable, sorted, hash_fn, stream_opts={objectMode:readable._readableState.objectMode}) -> - readable.pipe(new Uniq sorted, hash_fn, stream_opts) + uniq: fn + unique: fn From 21de36e67e7a30492a80843ef865b281c8732921 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 09:27:53 -0800 Subject: [PATCH 40/45] readme: document how to pass in builtin stream options cc @jonahkagan @azylman I've been sneaking this feature in as I convert things for 1.0, but all of the mixins in the 1.0 branch default to adopting the objectMode of their upstream, e.g. the stream returned by `_s.each(readable, fn)` will have the same objectMode as `readable`. I've documented this in this commit, let me know what you think. I'd also like to add some sugar to the objectMode option so that you can specify both the readable and writable side's objectMode: ``` _s.(readable).map(fn, { objectMode: {readable: false, writable: true} }) ``` This would let you safely have streams that convert streams of binary/string data into individual objects and vice versa. --- README.md | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8a575b2..9b0a348 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,9 @@ _s.toArray(readable, function(err, arr) { ``` --- -#### each `_s.each(readable, iterator)` +#### each `_s.each(readable, iterator[,` [`stream_opts`](#stream_opts)`])` -Calls the iterator function on each object in your stream, and emits the same object when your interator function is done. +Calls the iterator function on each object in your stream, and emits the same object when your iterator function is done. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. If it has two arguments, it is assumed to be asynchronous (`(element, cb)`). @@ -132,7 +132,7 @@ _s.each(readable, console.log); *aliases*: `forEach` --- -#### map `_s.map(readable, iterator)` +#### map `_s.map(readable, iterator[,` [`stream_opts`](#stream_opts)`])` Makes a new stream that is the result of calling `iterator` on each piece of data in `readable`. If the iterator function has one argument (`(element)`), it is assumed to be synchronous. @@ -151,7 +151,7 @@ mapped.on("data", console.log); *aliases*: `collect` --- -#### reduce `_s.reduce(readable, options)` +#### reduce `_s.reduce(readable, options[,` [`stream_opts`](#stream_opts)`])` Boils a stream down to a single value. `options` takes in: * `base`: value or function that represents/returns the initial state of the reduction. @@ -199,7 +199,7 @@ reduced.on('data', console.log); *aliases*: `inject`, `foldl` --- -#### filter `_s.filter(readable, iterator)` +#### filter `_s.filter(readable, iterator[,` [`stream_opts`](#stream_opts)`])` Returns a readable stream that emits all data from `readable` that passes `iterator`. If it has only one argument, `iterator` is assumed to be synchronous. @@ -219,7 +219,7 @@ filtered.on('data', console.log); *aliases*: `select` --- -#### where `_s.where(readable, attrs)` +#### where `_s.where(readable, attrs[,` [`stream_opts`](#stream_opts)`])` Filters `readable` to emit only objects that contain the attributes in the `attrs` object. @@ -238,7 +238,7 @@ whered.on('data', console.log); ``` --- -#### invoke `_s.invoke(readable, method)` +#### invoke `_s.invoke(readable, method[,` [`stream_opts`](#stream_opts)`])` Returns a stream that emits the results of invoking `method` on every object in `readable`. @@ -254,7 +254,7 @@ invoked.on('data', console.log); ``` --- -#### groupBy `_s.groupBy(readable, options)` +#### groupBy `_s.groupBy(readable, options[,` [`stream_opts`](#stream_opts)`])` When `options` is a function, creates a stream that will emit an object representing the groupings of the data in `readable` partitioned by the function. @@ -278,7 +278,7 @@ grouped.on('data', console.log); ``` --- -#### first `_s.first(readable[, n])` +#### first `_s.first(readable[, n,` [`stream_opts`](#stream_opts)`])` Returns a stream that only emits the first `n` objects in `readable`. `n` equals 1 by default. @@ -295,7 +295,7 @@ first.on('data', console.log); *aliases*: `head`, `take` --- -#### rest `_s.rest(readable[, n])` +#### rest `_s.rest(readable[, n,` [`stream_opts`](#stream_opts)`])` Returns a stream that skips over the first `n` objects in `readable`. `n` equals 1 by default. @@ -311,7 +311,7 @@ rest.on('data', console.log); *aliases*: `tail`, `drop` --- -#### flatten `_s.flatten(readable[, shallow])` +#### flatten `_s.flatten(readable[, shallow,` [`stream_opts`](#stream_opts)`])` Returns a stream that unpacks any arrays into their individual elements. By default `shallow` is false, and all nested arrays are also unpacked. @@ -329,7 +329,7 @@ flatten.on('data', console.log); ``` --- -#### uniq `_s.uniq(readable[, sorted, hash_fn])` +#### uniq `_s.uniq(readable[, sorted, hash_fn,` [`stream_opts`](#stream_opts)`])` Returns a stream that emits the unique elements of `readable`. Assumes the input is unsorted unless `sorted` is set to true. @@ -362,7 +362,7 @@ _s.chain(_s.fromArray([3, 4, 5, 6])).each(console.log) ``` --- -#### value `_s.chain(obj)` +#### value `_s.chain(obj)...value()` Analagous to underscore's `value`: exits a chain and returns the return value of the last method called. @@ -374,7 +374,18 @@ var readable = _s.chain(_s.fromArray([3, 4, 5, 6])).value(); // 6 ``` +### `stream_opts` +By default, node streams take in some parameters that describe the data in the stream and the behavior of the stream's backpressure: + +* `objectMode`: Boolean specifying whether the stream will be processing javascript objects (vs. strings/buffer data). +* `highWaterMark`: Number specifying the maximum size of a stream's internal buffer, i.e. the point at which it starts to exert backpressure on upstreams. +If `objectMode` is true, this represents the maximum number of objects to buffer. +If `objectMode` is false, this represents the number of bytes to buffer. + +In general it is a [bad idea](#TODO) to pipe two streams together that have mismatched `objectMode`s. +Thus, all of understream's builtin mixins set `objectMode` equal to the `objectMode` of the readable stream passed in. +This assures that backpressure works properly, and it is recommended you do the same in your own mixins. \ No newline at end of file +--!> From 1a3977aa392aac0ad496080379d6a1215d511e9a Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 16 Nov 2013 09:47:32 -0800 Subject: [PATCH 41/45] test: add gates on broken tests in < v0.10.20 cc @azylman I think this should fix travis. To be super user-friendly I'm thinking we should also gate the features themselves, e.g. throw if a user uses first/reduce/rest and their node version is bad. --- test/first.coffee | 3 +++ test/helpers.coffee | 3 +++ test/reduce.coffee | 4 +++- test/rest.coffee | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/helpers.coffee diff --git a/test/first.coffee b/test/first.coffee index 6e8294e..cd98815 100644 --- a/test/first.coffee +++ b/test/first.coffee @@ -2,9 +2,12 @@ _ = require 'underscore' assert = require 'assert' async = require 'async' _s = require "#{__dirname}/../index" +test_helpers = require "#{__dirname}/helpers" describe '_.first', -> # fails for node < v0.10.20 due to https://github.com/joyent/node/issues/6183 + return if test_helpers.node_major() is 10 and test_helpers.node_minor() < 20 + it 'sends through all objects if limit > size of stream', (done) -> input = [0..10] _s(_s.fromArray input).chain().first(100).toArray (err, result) -> diff --git a/test/helpers.coffee b/test/helpers.coffee new file mode 100644 index 0000000..07a619d --- /dev/null +++ b/test/helpers.coffee @@ -0,0 +1,3 @@ +module.exports = + node_major: -> Number process.version.match(/^v(\d+)\.(\d+)\.(\d+)$/)[2] + node_minor: -> Number process.version.match(/^v(\d+)\.(\d+)\.(\d+)$/)[3] diff --git a/test/reduce.coffee b/test/reduce.coffee index aefcb5f..d52eea6 100644 --- a/test/reduce.coffee +++ b/test/reduce.coffee @@ -2,10 +2,12 @@ assert = require 'assert' async = require 'async' _ = require 'underscore' _s = require "#{__dirname}/../index" +test_helpers = require "#{__dirname}/helpers" describe '_.reduce', -> - # fails for node < v0.10.20 due to https://github.com/joyent/node/issues/6183 + return if test_helpers.node_major() is 10 and test_helpers.node_minor() < 20 + it 'works with an empty stream with base 0', (done) -> _s(_s.fromArray []).chain().reduce base: 0 diff --git a/test/rest.coffee b/test/rest.coffee index 1fb1afd..27b8884 100644 --- a/test/rest.coffee +++ b/test/rest.coffee @@ -2,8 +2,12 @@ _ = require 'underscore' assert = require 'assert' async = require 'async' _s = require "#{__dirname}/../index" +test_helpers = require "#{__dirname}/helpers" describe '_.rest', -> + # fails for node < v0.10.20 due to https://github.com/joyent/node/issues/6183 + return if test_helpers.node_major() is 10 and test_helpers.node_minor() < 20 + it 'skips some objects if skip < size of stream', (done) -> SKIP = 5 input = [0..10] From 0eda4d03da82d42ae633da997fc0e0c2df49c4fb Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 18 Nov 2013 06:03:54 -0800 Subject: [PATCH 42/45] range --- README.md | 16 ++++++++++++++++ lib/mixins/range.coffee | 23 +++++++++++++++++++++++ lib/understream.coffee | 1 + test/range.coffee | 26 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 lib/mixins/range.coffee create mode 100644 test/range.coffee diff --git a/README.md b/README.md index 9b0a348..0c527a3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It provides three classes of functionality: * [`fromArray`](#fromArray) * [`fromString`](#fromArray) * [`toArray`](#toArray) + * [`range`](#range) 2. Functions that take a Readable stream and transform its data, returning a new readable stream: * [`each`](#each) @@ -113,6 +114,21 @@ _s.toArray(readable, function(err, arr) { // [ 3, 4, 5, 6 ] ``` +--- +#### range `_s.range(size, stream_opts)` `_s.range(start, stop[, step, stream_opts])` + +Generates the integers from 0 to `size-1`, inclusive. +Alternatively generates integers from `start` to `stop` in increments of `step`, with a default `step` of 1. + +```javascript +_s.range(5).on('data', console.log); +// 0 +// 1 +// 2 +// 3 +// 4 +``` + --- #### each `_s.each(readable, iterator[,` [`stream_opts`](#stream_opts)`])` diff --git a/lib/mixins/range.coffee b/lib/mixins/range.coffee new file mode 100644 index 0000000..d53fb0a --- /dev/null +++ b/lib/mixins/range.coffee @@ -0,0 +1,23 @@ +{Readable} = require 'stream' +_ = require 'underscore' +debug = require('debug') 'us:range' + +class Range extends Readable + constructor: (@start, @stop, @step, @stream_opts) -> + # must be in objectMode since not producing strings or buffers + super _(@stream_opts or {}).extend objectMode: true + @size = Math.max Math.ceil((@stop - @start) / @step), 0 + _read: (size) => + return @push() unless @size + @push @start + @start += @step + @size -= 1 + +module.exports = + range: (start, stop, step, stream_opts) -> + if arguments.length <= 1 + # did not specify stop and step, maybe not even start + stop = start or 0 + start = 0 + step = arguments[2] or 1 + new Range start, stop, step, stream_opts diff --git a/lib/understream.coffee b/lib/understream.coffee index a346c7f..0f97a0e 100644 --- a/lib/understream.coffee +++ b/lib/understream.coffee @@ -57,6 +57,7 @@ module.exports = -> "rest" "flatten" "uniq" + "range" ]).each (fn) -> _s.mixin require("#{__dirname}/mixins/#{fn}") _s diff --git a/test/range.coffee b/test/range.coffee new file mode 100644 index 0000000..4576173 --- /dev/null +++ b/test/range.coffee @@ -0,0 +1,26 @@ +assert = require 'assert' +_s = require "#{__dirname}/../index" +_ = require 'underscore' + +readable_equals_array = (readable, array, cb) -> + _s.toArray readable, (err, arr) -> + assert.ifError err + assert.deepEqual arr, array + cb() + +tests = + 'size': [5] + 'size 0': [0] + 'start, stop': [0, 10] + 'start, stop zero': [0, 0] + 'start, stop, step': [0, 10, 2] + 'start, stop zero, step': [0, 0, 2] + 'start, negative stop, negative step': [0, -10, -2] + 'start, stop zero, negative step': [0, 0, -2] + 'negative size': [-10] + 'start > stop': [10, 0] + +describe '_s.range', -> + _(tests).each (args, test) -> + it test, (done) -> + readable_equals_array _s.range.apply(_s, args), _.range.apply(_, args), done From 16002016bdb5e7af11dab43b10d044b3f5cb79fb Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 18 Nov 2013 14:32:50 -0800 Subject: [PATCH 43/45] test: aliases --- test/aliases.coffee | 18 ++++++++++++++++++ test/chain-wrap.coffee | 8 ++------ test/helpers.coffee | 7 +++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 test/aliases.coffee diff --git a/test/aliases.coffee b/test/aliases.coffee new file mode 100644 index 0000000..f3c50f7 --- /dev/null +++ b/test/aliases.coffee @@ -0,0 +1,18 @@ +assert = require 'assert' +_ = require 'underscore' +_s = require "#{__dirname}/../index" +test_helpers = require './helpers' + +describe 'aliases', -> + _([ + ['each', 'forEach'] + ['filter', 'select'] + ['first', 'head', 'take'] + ['map', 'collect'] + ['reduce', 'inject', 'foldl'] + ['rest', 'tail', 'drop'] + ['uniq', 'unique'] + ]).each (alias_set) -> + _(test_helpers.adjacent alias_set).each ([fn1, fn2]) -> + it "#{fn1} === #{fn2}", -> + assert _s[fn1] is _s[fn2] diff --git a/test/chain-wrap.coffee b/test/chain-wrap.coffee index 41a87e7..8ecfe7a 100644 --- a/test/chain-wrap.coffee +++ b/test/chain-wrap.coffee @@ -3,11 +3,7 @@ async = require 'async' _sMaker = require "../lib/understream" sinon = require 'sinon' _ = require 'underscore' - -# Takes an array and returns an array of adjacent pairs of elements in the -# array, wrapping around at the end. -adjacent = (arr) -> - _.zip arr, _.rest(arr).concat [_.first arr] +test_helpers = require './helpers' methods = (obj) -> _.chain().functions(obj).without('value', 'mixin').value() @@ -54,5 +50,5 @@ _.each # Since equivalence is transitive, to assert that a group of expressions # are equivalent, we can assert that each one is equivalent to one other # one. - _.each adjacent(_.pairs(exps)), ([[name1, exp1], [name2, exp2]]) -> + _.each test_helpers.adjacent(_.pairs(exps)), ([[name1, exp1], [name2, exp2]]) -> describe "#{name1}/#{name2}", -> testEquivalent exp1, exp2 diff --git a/test/helpers.coffee b/test/helpers.coffee index 07a619d..83908e6 100644 --- a/test/helpers.coffee +++ b/test/helpers.coffee @@ -1,3 +1,10 @@ +_ = require 'underscore' + module.exports = node_major: -> Number process.version.match(/^v(\d+)\.(\d+)\.(\d+)$/)[2] node_minor: -> Number process.version.match(/^v(\d+)\.(\d+)\.(\d+)$/)[3] + + # Takes an array and returns an array of adjacent pairs of elements in the + # array, wrapping around at the end. + adjacent: (arr) -> + _.zip arr, _.rest(arr).concat [_.first arr] From bd6da66b05eb4b77a3ceb20fca4d0c969260bc43 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 18 Nov 2013 14:34:44 -0800 Subject: [PATCH 44/45] test: stop testing 0.11 in travis too unpredictable --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f31fdde..a82dd95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - 0.10 - - 0.11 services: - mongodb notifications: From 7f6b246e025169862a96d74a52554d6cccd45025 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Tue, 19 Nov 2013 09:59:30 -0800 Subject: [PATCH 45/45] test: range + stream opts --- lib/mixins/range.coffee | 7 +++++-- test/range.coffee | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mixins/range.coffee b/lib/mixins/range.coffee index d53fb0a..9ba2be2 100644 --- a/lib/mixins/range.coffee +++ b/lib/mixins/range.coffee @@ -15,9 +15,12 @@ class Range extends Readable module.exports = range: (start, stop, step, stream_opts) -> - if arguments.length <= 1 + args = _(arguments).toArray() + if _(args).chain().last().isObject().value() + stream_opts = args.pop() + if args.length <= 1 # did not specify stop and step, maybe not even start stop = start or 0 start = 0 - step = arguments[2] or 1 + step = args[2] or 1 new Range start, stop, step, stream_opts diff --git a/test/range.coffee b/test/range.coffee index 4576173..3e9382a 100644 --- a/test/range.coffee +++ b/test/range.coffee @@ -9,6 +9,7 @@ readable_equals_array = (readable, array, cb) -> cb() tests = + 'no args': [] 'size': [5] 'size 0': [0] 'start, stop': [0, 10] @@ -24,3 +25,5 @@ describe '_s.range', -> _(tests).each (args, test) -> it test, (done) -> readable_equals_array _s.range.apply(_s, args), _.range.apply(_, args), done + it "#{test} with stream_opts", (done) -> + readable_equals_array _s.range.apply(_s, args.concat({highWaterMark:10})), _.range.apply(_, args), done