diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7cfab05 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# editorconfig.org +root = true + +[*] +charset=utf-8 +end_of_line=lf +indent_size=2 +indent_style=space +insert_final_newline=true +tab_width=2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a7df05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Deployed apps should consider commenting this line out: +# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git +node_modules + +.coveralls.yml +stream-from-factory-*.*.*.tgz diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..815a0fd --- /dev/null +++ b/.jscsrc @@ -0,0 +1,12 @@ +{ + "preset": "google", + + "requireParenthesesAroundIIFE": true, + "maximumLineLength": 80, + "validateLineBreaks": "LF", + "validateIndentation": 2, + + "disallowKeywords": ["with"], + "disallowSpacesInsideObjectBrackets": null, + "disallowImplicitTypeConversion": ["string"] +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..325ddbc --- /dev/null +++ b/.jshintrc @@ -0,0 +1,90 @@ +{ + "globals": { + /* mocha */ + "after": true, + "afterEach": true, + "before": true, + "beforeEach": true, + "describe": true, + "it": true + }, + + /* Enforcing options */ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "es3": true, + "forin": true, + "freeze": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "nonbsp": true, + "nonew": true, + "plusplus": false, + "quotmark": "single", + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "maxparams": 8, + "maxdepth": 3, + "maxstatements": 20, + "maxcomplexity": 10, + "maxlen": 80, + + /* Relaxing options */ + "asi": false, + "boss": false, + "debug": false, + "eqnull": false, + "esnext": true, + "evil": false, + "expr": false, + "funcscope": false, + "gcl": false, + "globalstrict": false, + "iterator": false, + "lastsemic": false, + "laxbreak": false, + "laxcomma": false, + "loopfunc": false, + "maxerr": 100, + "moz": false, + "multistr": false, + "notypeof": false, + "proto": false, + "scripturl": false, + "smarttabs": false, + "shadow": false, + "sub": false, + "supernew": false, + "validthis": false, + "noyield": false, + + /* Environments */ + "browser": false, + "couch": false, + "devel": false, + "dojo": false, + "jquery": false, + "mootools": false, + "node": true, + "nonstandard": false, + "phantom": false, + "prototypejs": false, + "rhino": false, + "worker": false, + "wsh": false, + "yui": false, + + /* Legacy */ + "nomen": false, + "onevar": true, + "passfail": false, + "white": false +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..18ae2d8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "0.11" + - "0.10" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75346e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Michael Mayer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9356243 --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# stream-from-factory [![Dependencies Status Image](https://gemnasium.com/schnittstabil/stream-from-factory.svg)](https://gemnasium.com/schnittstabil/stream-from-factory) [![Build Status Image](https://travis-ci.org/schnittstabil/stream-from-factory.svg)](https://travis-ci.org/schnittstabil/stream-from-factory) [![Coverage Status](https://coveralls.io/repos/schnittstabil/stream-from-factory/badge.png)](https://coveralls.io/r/schnittstabil/stream-from-factory) + +Create streams from sync and async factories. + +```bash +npm install stream-from-factory --save +``` + +## About Factories + +A factory is simply a function creating a product (i.e. a JavaScript value, like numbers, objects, etc.) and sends it back to its caller. + +(The term _factory_ refers to the idea of Joshua Blochs _static factory methods_, not to _abstract factories_, nor to _factory methods_.) + +### Synchronous Factories + +_Synchronous Factories_ are functions `return`ing a single product - other than `undefined` - or throwing an arbitrary JavaScript value: + +```JavaScript +function syncFactory() { + if (...) { + throw new Error('sth. went wrong...'); // typically Errors are thrown + } + return null; // that's ok (typeof null !== 'undefined'), but see below (API) +} +``` +### Asynchronous Factories + +_Asynchronous Factories_ working with callbacks (in node style), but don't return a value; + +```JavaScript +function asyncFactory(done) { + if (...) { + // return an error: + done(new Error('sth. went wrong...')); + return; // that's ok (!) + } + // return a result: + done(null, ['a', 'string', 'array']); +} +``` + +## Usage + +### Factories creating `String | Buffer`s + +```JavaScript +var StreamFromFactory = require('stream-from-factory'); + +function syncFactory() { + return new Buffer('buff!'); +} + +function asyncFactory(done) { + setTimeout(function() { + done(null, 'strrrring!'); + }, 1000); +} + + +StreamFromFactory(syncFactory) + .pipe(process.stdout); // output: buff! + +StreamFromFactory(asyncFactory) + .pipe(process.stdout); // output: strrrring! +``` + +### Factories creating arbitrary JavaScript values + +```JavaScript +var StreamFromFactory = require('stream-from-factory'); + +function logFunc(){ + console.log('func!?!'); +}; + +function asyncFactory(done) { + setTimeout(function() { + done(null, logFunc); + }, 1000); +} + +StreamFromFactory.obj(asyncFactory) + .on('data', function(fn){ + fn(); // output: func!?! + }); +``` + +### Errors + +```JavaScript +var StreamFromFactory = require('stream-from-factory'); + +function syncFactory() { + throw new Error('sth. went wrong ;-)'); +} + +function asyncFactory(done) { + setTimeout(function() { + done(new Error('sth. went wrong ;-)')); + }, 1000); +} + +StreamFromFactory(syncFactory) + .on('error', function(err){ + console.log(err); // output: [Error: sth. went wrong ;-)] + }) + .on('data', function(data){ + // do something awsome + }); +``` + +### [Gulp](http://gulpjs.com/) File Factories + +Gulp files are [vinyl](https://github.com/wearefractal/vinyl) files: + +```bash +npm install vinyl +``` + +Test some awsome Gulp plugin: + +```JavaScript +var StreamFromFactory = require('stream-from-factory'), + File = require('vinyl'); + +function creatTestFile(){ + return new File({ + cwd: '/', + base: '/hello/', + path: '/hello/hello.js', + contents: new Buffer('console.log("Hello");') + }); +} + +StreamFromFactory.obj(creatTestFile) + .pipe(someAwsomeGulpPlugin()) + .on('data', function(file){ + console.log(file.contents.toString()); // dunno what someAwsomeGulpPlugin does :) + }); +``` + +See also [stream-recorder](https://github.com/schnittstabil/stream-recorder) for testing gulp plugins. + +## API + +### Class: StreamFromFactory + +_StreamFromFactorys_ are [Readable](http://nodejs.org/api/stream.html#stream_class_stream_readable_1) streams. + +#### new StreamFromFactory(factory, [options]) + +* _factory_ `Function` Async or sync factory. +* _options_ `Object` passed through [new Readable([options])](http://nodejs.org/api/stream.html#stream_new_stream_readable_options) + +Notes: + +* The `new` operator can be omitted. +* `null` is special for streams (signals end-of-stream). Using a factory returning `null` will result in an empty stream. + +#### StreamFromFactory#obj(factory, [options]) + +A convenience wrapper for `new StreamFromFactory(factory, {objectMode: true, ...})`. + +## License + +Copyright (c) 2014 Michael Mayer + +Licensed under the MIT license. diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..274145b --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,63 @@ +'use strict'; +var gulp = require('gulp'), + istanbul = require('gulp-istanbul'), + jscs = require('gulp-jscs'), + jshint = require('gulp-jshint'), + mocha = require('gulp-mocha'), + scripts = ['**/*.js', '!node_modules/**', '!coverage/**'], + tests = 'test.js'; + +function lint(fail) { + return function() { + var l = gulp.src(scripts) + /* hint */ + .pipe(jshint()) + .pipe(jshint.reporter()) + /* jscs */ + .pipe(jscs()); + + if (fail) { + return l.pipe(jshint.reporter('fail')); + } else { + return l; + } + }; +} + +gulp.task('lint', lint(true)); +gulp.task('lint-watch', lint(false)); + +gulp.task('watch:lint', function() { + gulp.watch(scripts, ['lint-watch']); +}); + +gulp.task('watch:test', function() { + gulp.watch(scripts, ['test']); +}); + +gulp.task('watch', function() { + gulp.watch(scripts, ['lint-watch', 'test']); +}); + +gulp.task('test', function() { + return gulp.src(tests) + .pipe(mocha({reporter: 'spec'})); +}); + +gulp.task('coverage', function (done) { + gulp.src(scripts.concat(['!' + tests])) + .pipe(istanbul()) + .on('finish', function () { + /* tests */ + gulp.src(tests) + .pipe(mocha({ + reporter: 'dot' + })) + .pipe(istanbul.writeReports({ + reporters: ['lcovonly', 'text-summary', 'html'] + })) + .on('end', done); + }); +}); + +gulp.task('default', ['lint', 'coverage']); diff --git a/index.js b/index.js new file mode 100644 index 0000000..ba36bbd --- /dev/null +++ b/index.js @@ -0,0 +1,61 @@ +'use strict'; +var Readable = require('stream').Readable, + inherits = require('util').inherits; + +function StreamFromFactory(factory, options) { + if (!(this instanceof StreamFromFactory)) { + return new StreamFromFactory(factory, options); + } + + Readable.call(this, options); + + var self = this, + doneCalled = false, + factoryCalled = false; + + function done(err, result) { + if (doneCalled) { + self.emit('error', new Error('done called to many times: ' + + JSON.stringify([err, result]) )); + return; + } else { + doneCalled = true; + } + if (err) { + self.emit('error', err); + } + self.push(result); + // don't push null || undefined twice + if (typeof result !== 'undefined' && result !== null) { + self.push(null); + } + } + + this._read = function() { + var product; + if (!factoryCalled) { + factoryCalled = true; + if (typeof factory !== 'function') { + // throw TypeError: + factory(done); + } + try { + product = factory(done); + } catch (err) { + done(err, product); + } + if (typeof product !== 'undefined') { + done(null, product); + } + } + }; +} +inherits(StreamFromFactory, Readable); + +StreamFromFactory.obj = function(factory, options) { + options = options || {}; + options.objectMode = true; + return new StreamFromFactory(factory, options); +}; + +module.exports = StreamFromFactory; diff --git a/package.json b/package.json new file mode 100644 index 0000000..aebb8f8 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "stream-from-factory", + "version": "0.1.0", + "description": "Create streams from sync and async factories", + "repository": { + "type": "git", + "url": "https://github.com/schnittstabil/stream-from-factory.git" + }, + "keywords": [ + "readable", + "object", + "stream", + "gulpfriendly", + "adapter", + "to stream", + "sync", + "async", + "factory" + ], + "author": "Michael Mayer (https://github.com/schnittstabil)", + "license": "MIT", + "bugs": { + "url": "https://github.com/schnittstabil/stream-from-factory/issues" + }, + "homepage": "https://github.com/schnittstabil/stream-from-factory", + "engines": { + "node": "^0.11 || ^0.10" + }, + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "./node_modules/gulp/bin/gulp.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" + }, + "devDependencies": { + "coveralls": "^2.11.1", + "gulp": "^3.8.6", + "gulp-istanbul": "^0.2.0", + "gulp-jscs": "^0.6.0", + "gulp-jshint": "^1.7.1", + "gulp-mocha": "^0.5.0", + "merge-stream": "^0.1.5", + "stream-recorder": "^0.3.0", + "vinyl": "^0.2.3" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..b83d7d7 --- /dev/null +++ b/test.js @@ -0,0 +1,190 @@ +'use strict'; +var fromFactory = require('./'), + merge = require('merge-stream'), + assert = require('assert'), + recorder = require('stream-recorder'), + gulp = require('gulp'), + File = require('vinyl'), + testfilePath = '/test/file.coffee', + testfile = new File({ + cwd: '/', + base: '/test/', + path: testfilePath, + contents: new Buffer('answer: 42') + }); + +function AsyncFactory(err, result) { + return function(done) { + setTimeout(function() { + done(err, result); + }, 500); + }; +} + +function SyncFactory(err, result) { + return function() { + if (err) { + throw err; + } + return result; + }; +} + +/* + * call done twice + */ +function BuggyFactory(err, result) { + return function(done) { + setTimeout(function() { + done(err, result); + setTimeout(function() { + done(err, result); + }, 500); + }, 500); + }; +} + +describe('fromFactory', function() { + + describe('with string as value', function() { + var input = '\uD834\uDF06'; + + it('should emit value', function(done) { + fromFactory(new AsyncFactory(null, input)) + .on('error', done) + .pipe(recorder(function(result) { + assert.deepEqual(result.toString(), input); + done(); + })) + .resume(); + }); + + describe('and decodeStrings:false option', function() { + it('should emit value', function(done) { + fromFactory(new SyncFactory(null, input), + {decodeStrings: false}) + .on('error', done) + .pipe(recorder(function(result) { + assert.deepEqual(result.toString(), input); + done(); + })) + .resume(); + }); + }); + }); + + it('should emit async errors', function(done) { + fromFactory(new AsyncFactory(new Error('async test'))) + .on('error', function(err) { + if (err) { + assert.ok((err instanceof Error) && /async test/.test(err), err); + done(); + } + }) + .resume(); + }); + + it('should emit sync errors', function(done) { + fromFactory(new SyncFactory(new Error('sync test'))) + .on('error', function(err) { + if (err) { + assert.ok((err instanceof Error) && /sync test/.test(err), err); + done(); + } + }) + .resume(); + }); + + it('should throw errors on non factory', function() { + var sut = fromFactory(null); + assert.throws(function() { + sut.read(); + }, /object/); + }); + + it('should emit errors on multiple done calls', function(done) { + fromFactory(new BuggyFactory(null, 'buggy test')) + .on('error', function(err) { + if (err) { + assert.ok((err instanceof Error) && /done/.test(err), err); + done(); + } + }) + .resume(); + }); + + it('constructor should return new instance w/o new', function() { + var sut = fromFactory, + instance = sut(new AsyncFactory()); + assert.strictEqual(instance instanceof fromFactory, true); + }); +}); + +describe('fromFactory.obj', function() { + + describe('with string array as value', function() { + var input = ['foo', 'bar']; + it('should emit value in object mode', function(done) { + var opts = {objectMode: true}; + fromFactory(new AsyncFactory(null, input), opts) + .on('error', done) + .pipe(recorder(opts, function(result) { + assert.deepEqual(result, [input]); + done(); + })) + .resume(); + }); + }); + + [null, undefined].forEach(function(eof) { + describe('with value == ' + eof, function() { + it('should end stream', function(done) { + var opts = {objectMode: true}; + fromFactory(new AsyncFactory(null, eof), opts) + .on('error', done) + .pipe(recorder(opts, function(result) { + assert.deepEqual(result, []); + done(); + })) + .resume(); + }); + }); + }); + + describe('with mixed object as value', function() { + var input = ['foo', 1, { foobar: 'foobar', answer: 42 }, {}, 'bar', + undefined, null]; + + it('should emit value', function(done) { + var opts = {objectMode: true}; + fromFactory.obj(new AsyncFactory(null, input)) + .on('error', done) + .pipe(recorder(opts, function(result) { + assert.deepEqual(result, [input]); + done(); + })) + .resume(); + }); + }); + + describe('in duplex mode', function() { + it('should insert vinyl file in gulp stream', function(done) { + var opts = {objectMode: true}; + var sut = new fromFactory.obj(new AsyncFactory(null, testfile)); + merge(gulp.src(__filename), sut) + .on('error', done) + .pipe(recorder(opts, function(result) { + var paths = result.map(function(file) { return file.path; }); + assert.deepEqual(paths.sort(), [testfilePath, __filename].sort()); + done(); + })) + .resume(); + }); + }); + + it('constructor should return new instance w/o new', function() { + var sut = fromFactory.obj, + instance = sut(); + assert.strictEqual(instance instanceof fromFactory, true); + }); +});