├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store* 3 | *.log 4 | *.gz 5 | 6 | node_modules 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "0.10" 3 | - "0.12" 4 | - "1" 5 | - "2" 6 | sudo: false 7 | language: node_js 8 | script: "npm run-script test-travis" 9 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming JSON.stringify() 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![Dependency Status][david-image]][david-url] 7 | [![License][license-image]][license-url] 8 | [![Downloads][downloads-image]][downloads-url] 9 | 10 | Similar to [JSONStream.stringify()](https://github.com/dominictarr/JSONStream#jsonstreamstringifyopen-sep-close) except it is, by default, a binary stream, and it is a streams2 implementation. 11 | 12 | ## Example 13 | 14 | The main use case for this is to stream a database query to a web client. 15 | This is meant to be used only with arrays, not objects. 16 | 17 | ```js 18 | var Stringify = require('streaming-json-stringify') 19 | 20 | app.get('/things', function (req, res, next) { 21 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 22 | 23 | db.things.find() 24 | .stream() 25 | .pipe(Stringify()) 26 | .pipe(res) 27 | }) 28 | ``` 29 | 30 | will yield something like 31 | 32 | ```json 33 | [ 34 | {"_id":"123412341234123412341234"} 35 | , 36 | {"_id":"123412341234123412341234"} 37 | ] 38 | 39 | ``` 40 | 41 | ## Separators 42 | 43 | * The stream always starts with `'[\n'`. 44 | * Documents are separated by `'\n,\n'`. 45 | * The stream is terminated with `'\n]\n'`. 46 | 47 | ## Stringifier 48 | 49 | By default, [json-stringify-safe](https://www.npmjs.com/package/json-stringify-safe) is used to convert objects into strings. This can be configured with `options.stringifier`. 50 | 51 | ## API 52 | 53 | ### Stringify([options]) 54 | 55 | Returns a `Transform` stream. 56 | The options are passed to the `Transform` constructor. 57 | 58 | ### JSON.stringify options 59 | 60 | You can override these: 61 | 62 | ```js 63 | var stringify = Stringify() 64 | stringify.replacer = function () {} 65 | stringify.space = 2 66 | stringify.opener = '[' 67 | stringify.seperator = ',' 68 | stringify.closer = ']' 69 | stringify.stringifier = JSON.stringify 70 | ``` 71 | 72 | [gitter-image]: https://badges.gitter.im/stream-utils/streaming-json-stringify.png 73 | [gitter-url]: https://gitter.im/stream-utils/streaming-json-stringify 74 | [npm-image]: https://img.shields.io/npm/v/streaming-json-stringify.svg?style=flat-square 75 | [npm-url]: https://npmjs.org/package/streaming-json-stringify 76 | [github-tag]: http://img.shields.io/github/tag/stream-utils/streaming-json-stringify.svg?style=flat-square 77 | [github-url]: https://github.com/stream-utils/streaming-json-stringify/tags 78 | [travis-image]: https://img.shields.io/travis/stream-utils/streaming-json-stringify.svg?style=flat-square 79 | [travis-url]: https://travis-ci.org/stream-utils/streaming-json-stringify 80 | [coveralls-image]: https://img.shields.io/coveralls/stream-utils/streaming-json-stringify.svg?style=flat-square 81 | [coveralls-url]: https://coveralls.io/r/stream-utils/streaming-json-stringify 82 | [david-image]: http://img.shields.io/david/stream-utils/streaming-json-stringify.svg?style=flat-square 83 | [david-url]: https://david-dm.org/stream-utils/streaming-json-stringify 84 | [license-image]: http://img.shields.io/npm/l/streaming-json-stringify.svg?style=flat-square 85 | [license-url]: LICENSE 86 | [downloads-image]: http://img.shields.io/npm/dm/streaming-json-stringify.svg?style=flat-square 87 | [downloads-url]: https://npmjs.org/package/streaming-json-stringify 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | 4 | db.collection.find().stream().pipe(Stringify()).pipe(res) 5 | 6 | */ 7 | 8 | var Transform = require('readable-stream/transform') 9 | var stringify = require('json-stringify-safe') 10 | var util = require('util') 11 | 12 | util.inherits(Stringify, Transform) 13 | 14 | module.exports = Stringify 15 | 16 | function Stringify(options) { 17 | if (!(this instanceof Stringify)) 18 | return new Stringify(options || {}) 19 | if (options && options.replacer) { 20 | this.replacer = options.replacer; 21 | } 22 | if (options && options.space !== undefined) { 23 | this.space = options.space; 24 | } 25 | Transform.call(this, options || {}) 26 | this._writableState.objectMode = true 27 | 28 | // Array Deliminator and Stringifier defaults 29 | var opener = options && options.opener ? options.opener : '[\n' 30 | var seperator = options && options.seperator ? options.seperator : '\n,\n' 31 | var closer = options && options.closer ? options.closer : '\n]\n' 32 | var stringifier = options && options.stringifier ? options.stringifier : stringify 33 | 34 | // Array Deliminators and Stringifier 35 | this.opener = new Buffer(opener, 'utf8') 36 | this.seperator = new Buffer(seperator, 'utf8') 37 | this.closer = new Buffer(closer, 'utf8') 38 | this.stringifier = stringifier 39 | } 40 | 41 | // Flags 42 | Stringify.prototype.started = false 43 | 44 | // JSON.stringify options 45 | Stringify.prototype.replacer = null 46 | Stringify.prototype.space = 0 47 | 48 | Stringify.prototype._transform = function (doc, enc, cb) { 49 | if (this.started) { 50 | this.push(this.seperator) 51 | } else { 52 | this.push(this.opener) 53 | this.started = true 54 | } 55 | 56 | doc = this.stringifier(doc, this.replacer, this.space) 57 | 58 | this.push(new Buffer(doc, 'utf8')) 59 | cb() 60 | } 61 | 62 | Stringify.prototype._flush = function (cb) { 63 | if (!this.started) this.push(this.opener) 64 | this.push(this.closer) 65 | this.push(null) 66 | cb() 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming-json-stringify", 3 | "description": "Streaming JSON.stringify()", 4 | "version": "3.1.0", 5 | "author": "Jonathan Ong (http://jongleberry.com)", 6 | "repository": "stream-utils/streaming-json-stringify", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "cat-stream": "*", 10 | "istanbul": "0", 11 | "mocha": "3.2.0", 12 | "sinon": "^1.17.2" 13 | }, 14 | "scripts": { 15 | "test": "mocha --reporter spec", 16 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot", 17 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot" 18 | }, 19 | "keywords": [ 20 | "stream", 21 | "json", 22 | "stringify" 23 | ], 24 | "files": [ 25 | "index.js" 26 | ], 27 | "dependencies": { 28 | "json-stringify-safe": "5", 29 | "readable-stream": "2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var PassThrough = require('readable-stream/passthrough') 3 | var assert = require('assert') 4 | var cat = require('cat-stream') 5 | var sinon = require('sinon'); 6 | 7 | var Stringify = require('..') 8 | 9 | describe('Streamify()', function () { 10 | it('should work with an empty stream', function (done) { 11 | var stream = new PassThrough({ 12 | objectMode: true 13 | }) 14 | 15 | stream.pipe(Stringify()).pipe(cat(function (err, buf) { 16 | assert.ifError(err) 17 | assert.equal(buf.toString('utf8'), '[\n\n]\n') 18 | done() 19 | })) 20 | 21 | stream.end() 22 | }) 23 | 24 | it('should work with a stream of length 1', function (done) { 25 | var stream = new PassThrough({ 26 | objectMode: true 27 | }) 28 | 29 | stream.pipe(Stringify()).pipe(cat(function (err, buf) { 30 | assert.ifError(err) 31 | assert.equal(buf.toString('utf8'), '[\n{}\n]\n') 32 | done() 33 | })) 34 | 35 | stream.write({}) 36 | stream.end() 37 | }) 38 | 39 | it('should work with a stream of length 2', function (done) { 40 | var stream = new PassThrough({ 41 | objectMode: true 42 | }) 43 | 44 | stream.pipe(Stringify()).pipe(cat(function (err, buf) { 45 | assert.ifError(err) 46 | assert.equal(buf.toString('utf8'), '[\n{}\n,\n{}\n]\n') 47 | done() 48 | })) 49 | 50 | stream.write({}) 51 | stream.write({}) 52 | stream.end() 53 | }) 54 | 55 | it('should work with non-objects', function (done) { 56 | var stream = new PassThrough({ 57 | objectMode: true 58 | }) 59 | 60 | stream.pipe(Stringify()).pipe(cat(function (err, buf) { 61 | assert.ifError(err) 62 | assert.equal(buf.toString('utf8'), '[\n"hello"\n]\n') 63 | done() 64 | })) 65 | 66 | stream.write('hello') 67 | stream.end() 68 | }) 69 | 70 | it('should return a string if encoding: "utf8"', function (done) { 71 | var stream = new PassThrough({ 72 | objectMode: true 73 | }) 74 | 75 | stream.pipe(Stringify({ 76 | encoding: 'utf8' 77 | })).once('data', function (chunk) { 78 | assert.equal(typeof chunk, 'string') 79 | done() 80 | }) 81 | 82 | stream.write({}) 83 | stream.end() 84 | }) 85 | 86 | it('should allow a space argument to JSON.stringify()', function (done) { 87 | var stream = new PassThrough({ 88 | objectMode: true 89 | }) 90 | 91 | var stringify = Stringify() 92 | stringify.space = 2 93 | 94 | var obj = { 95 | a: 1 96 | } 97 | 98 | stream 99 | .pipe(stringify) 100 | .pipe(cat(function (err, buf) { 101 | assert.ifError(err) 102 | assert.equal(buf.toString('utf8'), '[\n' + JSON.stringify(obj, null, 2) + '\n]\n') 103 | 104 | done() 105 | })) 106 | 107 | stream.end(obj) 108 | }) 109 | it('should allow a space argument to JSON.stringify()', function (done) { 110 | var stream = new PassThrough({ 111 | objectMode: true 112 | }) 113 | 114 | var stringify = Stringify({space:2}) 115 | 116 | var obj = { 117 | a: 1 118 | } 119 | 120 | stream 121 | .pipe(stringify) 122 | .pipe(cat(function (err, buf) { 123 | assert.ifError(err) 124 | assert.equal(buf.toString('utf8'), '[\n' + JSON.stringify(obj, null, 2) + '\n]\n') 125 | 126 | done() 127 | })) 128 | 129 | stream.end(obj) 130 | }) 131 | 132 | it('should allow a space argument to JSON.stringify()', function (done) { 133 | var stream = new PassThrough({ 134 | objectMode: true 135 | }) 136 | 137 | var replacer = function(key, value){ 138 | if(key === 'a') return undefined 139 | return value 140 | } 141 | 142 | var stringify = Stringify({replacer:replacer}) 143 | 144 | var obj = { 145 | a: 1 146 | } 147 | 148 | stream 149 | .pipe(stringify) 150 | .pipe(cat(function (err, buf) { 151 | assert.ifError(err) 152 | assert.equal(buf.toString('utf8'), '[\n' + JSON.stringify({}, null, 2) + '\n]\n') 153 | 154 | done() 155 | })) 156 | 157 | stream.end(obj) 158 | }) 159 | 160 | it('should allow custom openings and closings', function (done) { 161 | var stream = new PassThrough({ 162 | objectMode: true 163 | }) 164 | 165 | var stringify = Stringify({opener: "{\"test\": [\n", closer: "\n]}\n"}) 166 | 167 | var obj = [{a: 1}] 168 | 169 | stream 170 | .pipe(stringify) 171 | .pipe(cat(function (err, buf) { 172 | assert.ifError(err) 173 | assert.equal(buf.toString('utf8'), "{\"test\": [\n" + JSON.stringify(obj, null) + "\n]}\n") 174 | 175 | done() 176 | })) 177 | 178 | stream.end(obj) 179 | }) 180 | 181 | it('should allow custom seperators', function (done) { 182 | var stream = new PassThrough({ 183 | objectMode: true 184 | }) 185 | 186 | var stringify = Stringify({seperator: ' , '}) 187 | 188 | stream 189 | .pipe(stringify) 190 | .pipe(cat(function (err, buf) { 191 | assert.ifError(err) 192 | assert.equal(buf.toString('utf8'), "[\n1 , 2 , 3\n]\n") 193 | 194 | done() 195 | })) 196 | 197 | stream.write(1) 198 | stream.write(2) 199 | stream.write(3) 200 | stream.end() 201 | }) 202 | 203 | it('should allow custom stringifiers', function(done) { 204 | var stream = new PassThrough({ 205 | objectMode: true 206 | }) 207 | 208 | var stringifier = sinon.spy(JSON.stringify); 209 | 210 | var stringify = Stringify({stringifier: stringifier}) 211 | 212 | stream 213 | .pipe(stringify) 214 | .pipe(cat(function (err, buf) { 215 | assert.ifError(err) 216 | assert.ok(stringifier.called) 217 | assert.equal(buf.toString('utf8'), "[\n1\n,\n2\n,\n3\n]\n") 218 | 219 | done() 220 | })) 221 | 222 | stream.write(1) 223 | stream.write(2) 224 | stream.write(3) 225 | stream.end() 226 | }) 227 | }) 228 | --------------------------------------------------------------------------------