bfj 5.3.0 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/match.js ADDED
@@ -0,0 +1,218 @@
1
+ 'use strict'
2
+
3
+ const check = require('check-types')
4
+ const DataStream = require('./datastream')
5
+ const events = require('./events')
6
+ const Hoopy = require('hoopy')
7
+ const walk = require('./walk')
8
+
9
+ const DEFAULT_BUFFER_LENGTH = 1024
10
+
11
+ module.exports = match
12
+
13
+ /**
14
+ * Public function `match`.
15
+ *
16
+ * Asynchronously parses a stream of JSON data, returning a stream of items
17
+ * that match the argument. Note that if a value is `null`, it won't be matched
18
+ * because `null` is used to signify end-of-stream in node.
19
+ *
20
+ * @param stream: Readable instance representing the incoming JSON.
21
+ *
22
+ * @param selector: Regular expression, string or predicate function used to
23
+ * identify matches. If a regular expression or string is
24
+ * passed, only property keys are tested. If a predicate is
25
+ * passed, both the key and the value are passed to it as
26
+ * arguments.
27
+ *
28
+ * @option numbers: Boolean, indicating whether numerical keys (e.g. array
29
+ * indices) should be coerced to strings before testing the
30
+ * match. Only applies if the `selector` argument is a string
31
+ * or regular expression.
32
+ *
33
+ * @option ndjson: Set this to true to parse newline-delimited JSON,
34
+ * default is `false`.
35
+ *
36
+ * @option yieldRate: The number of data items to process per timeslice,
37
+ * default is 16384.
38
+ *
39
+ * @option bufferLength: The length of the match buffer, default is 1024.
40
+ *
41
+ * @option highWaterMark: If set, will be passed to the readable stream constructor
42
+ * as the value for the highWaterMark option.
43
+ *
44
+ * @option Promise: The promise constructor to use, defaults to bluebird.
45
+ **/
46
+ function match (stream, selector, options = {}) {
47
+ const scopes = []
48
+ const properties = []
49
+ const emitter = walk(stream, options)
50
+ const matches = new Hoopy(options.bufferLength || DEFAULT_BUFFER_LENGTH)
51
+ let streamOptions
52
+ const { highWaterMark } = options
53
+ if (highWaterMark) {
54
+ streamOptions = { highWaterMark }
55
+ }
56
+ const results = new DataStream(read, streamOptions)
57
+
58
+ let selectorFunction, selectorString, resume
59
+ let coerceNumbers = false
60
+ let awaitPush = true
61
+ let isEnded = false
62
+ let length = 0
63
+ let index = 0
64
+
65
+ if (check.function(selector)) {
66
+ selectorFunction = selector
67
+ selector = null
68
+ } else {
69
+ coerceNumbers = !! options.numbers
70
+
71
+ if (check.string(selector)) {
72
+ check.assert.nonEmptyString(selector)
73
+ selectorString = selector
74
+ selector = null
75
+ } else {
76
+ check.assert.instanceStrict(selector, RegExp)
77
+ }
78
+ }
79
+
80
+ emitter.on(events.array, array)
81
+ emitter.on(events.object, object)
82
+ emitter.on(events.property, property)
83
+ emitter.on(events.endArray, endScope)
84
+ emitter.on(events.endObject, endScope)
85
+ emitter.on(events.string, value)
86
+ emitter.on(events.number, value)
87
+ emitter.on(events.literal, value)
88
+ emitter.on(events.end, end)
89
+ emitter.on(events.error, error)
90
+ emitter.on(events.dataError, dataError)
91
+
92
+ return results
93
+
94
+ function read () {
95
+ if (awaitPush) {
96
+ awaitPush = false
97
+
98
+ if (isEnded) {
99
+ if (length > 0) {
100
+ after()
101
+ }
102
+
103
+ return endResults()
104
+ }
105
+ }
106
+
107
+ if (resume) {
108
+ const resumeCopy = resume
109
+ resume = null
110
+ resumeCopy()
111
+ after()
112
+ }
113
+ }
114
+
115
+ function after () {
116
+ if (awaitPush || resume) {
117
+ return
118
+ }
119
+
120
+ let i
121
+
122
+ for (i = 0; i < length && ! resume; ++i) {
123
+ if (! results.push(matches[i + index])) {
124
+ pause()
125
+ }
126
+ }
127
+
128
+ if (i === length) {
129
+ index = length = 0
130
+ } else {
131
+ length -= i
132
+ index += i
133
+ }
134
+ }
135
+
136
+ function pause () {
137
+ resume = emitter.pause()
138
+ }
139
+
140
+ function endResults () {
141
+ if (! awaitPush) {
142
+ results.push(null)
143
+ }
144
+ }
145
+
146
+ function array () {
147
+ scopes.push([])
148
+ }
149
+
150
+ function object () {
151
+ scopes.push({})
152
+ }
153
+
154
+ function property (name) {
155
+ properties.push(name)
156
+ }
157
+
158
+ function endScope () {
159
+ value(scopes.pop())
160
+ }
161
+
162
+ function value (v) {
163
+ let key
164
+
165
+ if (scopes.length > 0) {
166
+ const scope = scopes[scopes.length - 1]
167
+
168
+ if (Array.isArray(scope)) {
169
+ key = scope.length
170
+ } else {
171
+ key = properties.pop()
172
+ }
173
+
174
+ scope[key] = v
175
+ }
176
+
177
+ if (v === null) {
178
+ return
179
+ }
180
+
181
+ if (selectorFunction) {
182
+ if (selectorFunction(key, v, scopes.length)) {
183
+ push(v)
184
+ }
185
+ } else {
186
+ if (coerceNumbers && typeof key === 'number') {
187
+ key = key.toString()
188
+ }
189
+
190
+ if ((selectorString && selectorString === key) || (selector && selector.test(key))) {
191
+ push(v)
192
+ }
193
+ }
194
+ }
195
+
196
+ function push (v) {
197
+ if (length + 1 === matches.length) {
198
+ pause()
199
+ }
200
+
201
+ matches[index + length++] = v
202
+
203
+ after()
204
+ }
205
+
206
+ function end () {
207
+ isEnded = true
208
+ endResults()
209
+ }
210
+
211
+ function error (e) {
212
+ results.emit('error', e)
213
+ }
214
+
215
+ function dataError (e) {
216
+ results.emit('dataError', e)
217
+ }
218
+ }
package/src/parse.js CHANGED
@@ -65,6 +65,7 @@ function parse (stream, options = {}) {
65
65
  emitter.on(events.endObject, endScope)
66
66
  emitter.on(events.end, end)
67
67
  emitter.on(events.error, error)
68
+ emitter.on(events.dataError, error)
68
69
 
69
70
  if (shouldHandleNdjson) {
70
71
  emitter.on(events.endLine, endLine)
package/src/stream.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const util = require('util')
4
+ const Readable = require('stream').Readable
5
+ const check = require('check-types')
6
+
7
+ util.inherits(BfjStream, Readable)
8
+
9
+ module.exports = BfjStream
10
+
11
+ function BfjStream (read, options) {
12
+ if (check.not.instanceStrict(this, BfjStream)) {
13
+ return new BfjStream(read)
14
+ }
15
+
16
+ check.assert.function(read, 'Invalid read implementation')
17
+
18
+ this._read = function () { // eslint-disable-line no-underscore-dangle
19
+ read()
20
+ }
21
+
22
+ return Readable.call(this, options)
23
+ }
package/src/streamify.js CHANGED
@@ -18,34 +18,42 @@ module.exports = streamify
18
18
  * Asynchronously serialises a data structure to a stream of JSON
19
19
  * data. Sanely handles promises, buffers, maps and other iterables.
20
20
  *
21
- * @param data: The data to transform.
21
+ * @param data: The data to transform.
22
22
  *
23
- * @option space: Indentation string, or the number of spaces
24
- * to indent each nested level by.
23
+ * @option space: Indentation string, or the number of spaces
24
+ * to indent each nested level by.
25
25
  *
26
- * @option promises: 'resolve' or 'ignore', default is 'resolve'.
26
+ * @option promises: 'resolve' or 'ignore', default is 'resolve'.
27
27
  *
28
- * @option buffers: 'toString' or 'ignore', default is 'toString'.
28
+ * @option buffers: 'toString' or 'ignore', default is 'toString'.
29
29
  *
30
- * @option maps: 'object' or 'ignore', default is 'object'.
30
+ * @option maps: 'object' or 'ignore', default is 'object'.
31
31
  *
32
- * @option iterables: 'array' or 'ignore', default is 'array'.
32
+ * @option iterables: 'array' or 'ignore', default is 'array'.
33
33
  *
34
- * @option circular: 'error' or 'ignore', default is 'error'.
34
+ * @option circular: 'error' or 'ignore', default is 'error'.
35
35
  *
36
- * @option yieldRate: The number of data items to process per timeslice,
37
- * default is 16384.
36
+ * @option yieldRate: The number of data items to process per timeslice,
37
+ * default is 16384.
38
38
  *
39
- * @option bufferLength: The length of the buffer, default is 1024.
39
+ * @option bufferLength: The length of the buffer, default is 1024.
40
40
  *
41
- * @option Promise: The promise constructor to use, defaults to bluebird.
41
+ * @option highWaterMark: If set, will be passed to the readable stream constructor
42
+ * as the value for the highWaterMark option.
43
+ *
44
+ * @option Promise: The promise constructor to use, defaults to bluebird.
42
45
  **/
43
46
  function streamify (data, options = {}) {
44
47
  const emitter = eventify(data, options)
45
48
  const json = new Hoopy(options.bufferLength || DEFAULT_BUFFER_LENGTH)
46
49
  const Promise = promise(options)
47
50
  const space = normaliseSpace(options)
48
- const stream = new JsonStream(read)
51
+ let streamOptions
52
+ const { highWaterMark } = options
53
+ if (highWaterMark) {
54
+ streamOptions = { highWaterMark }
55
+ }
56
+ const stream = new JsonStream(read, streamOptions)
49
57
 
50
58
  let awaitPush = true
51
59
  let index = 0
@@ -67,6 +75,7 @@ function streamify (data, options = {}) {
67
75
  emitter.on(events.endObject, noRacing(endObject))
68
76
  emitter.on(events.end, noRacing(end))
69
77
  emitter.on(events.error, noRacing(error))
78
+ emitter.on(events.dataError, noRacing(dataError))
70
79
 
71
80
  return stream
72
81
 
@@ -255,6 +264,10 @@ function streamify (data, options = {}) {
255
264
  }
256
265
 
257
266
  function error (err) {
267
+ stream.emit('error', err)
268
+ }
269
+
270
+ function dataError (err) {
258
271
  stream.emit('dataError', err)
259
272
  }
260
273
  }
package/src/stringify.js CHANGED
@@ -11,27 +11,30 @@ module.exports = stringify
11
11
  * Returns a promise and asynchronously serialises a data structure to a
12
12
  * JSON string. Sanely handles promises, buffers, maps and other iterables.
13
13
  *
14
- * @param data: The data to transform
14
+ * @param data: The data to transform
15
15
  *
16
- * @option space: Indentation string, or the number of spaces
17
- * to indent each nested level by.
16
+ * @option space: Indentation string, or the number of spaces
17
+ * to indent each nested level by.
18
18
  *
19
- * @option promises: 'resolve' or 'ignore', default is 'resolve'.
19
+ * @option promises: 'resolve' or 'ignore', default is 'resolve'.
20
20
  *
21
- * @option buffers: 'toString' or 'ignore', default is 'toString'.
21
+ * @option buffers: 'toString' or 'ignore', default is 'toString'.
22
22
  *
23
- * @option maps: 'object' or 'ignore', default is 'object'.
23
+ * @option maps: 'object' or 'ignore', default is 'object'.
24
24
  *
25
- * @option iterables: 'array' or 'ignore', default is 'array'.
25
+ * @option iterables: 'array' or 'ignore', default is 'array'.
26
26
  *
27
- * @option circular: 'error' or 'ignore', default is 'error'.
27
+ * @option circular: 'error' or 'ignore', default is 'error'.
28
28
  *
29
- * @option yieldRate: The number of data items to process per timeslice,
30
- * default is 16384.
29
+ * @option yieldRate: The number of data items to process per timeslice,
30
+ * default is 16384.
31
31
  *
32
- * @option bufferLength: The length of the buffer, default is 1024.
32
+ * @option bufferLength: The length of the buffer, default is 1024.
33
33
  *
34
- * @option Promise: The promise constructor to use, defaults to bluebird.
34
+ * @option highWaterMark: If set, will be passed to the readable stream constructor
35
+ * as the value for the highWaterMark option.
36
+ *
37
+ * @option Promise: The promise constructor to use, defaults to bluebird.
35
38
  **/
36
39
  function stringify (data, options) {
37
40
  const json = []
@@ -42,6 +45,7 @@ function stringify (data, options) {
42
45
 
43
46
  stream.on('data', read)
44
47
  stream.on('end', end)
48
+ stream.on('error', error)
45
49
  stream.on('dataError', error)
46
50
 
47
51
  return new Promise((res, rej) => {
package/src/unpipe.js CHANGED
@@ -29,7 +29,7 @@ function unpipe (callback, options) {
29
29
 
30
30
  const jsonstream = new stream.PassThrough()
31
31
 
32
- parse(jsonstream, options)
32
+ parse(jsonstream, Object.assign({}, options, { ndjson: false }))
33
33
  .then(data => callback(null, data))
34
34
  .catch(error => callback(error))
35
35
 
package/src/walk.js CHANGED
@@ -374,7 +374,7 @@ function initialise (stream, options = {}) {
374
374
 
375
375
  return endScope(scp)
376
376
  .then(() => {
377
- if (! shouldHandleNdjson || scopes.length > 0) {
377
+ if (scopes.length > 0) {
378
378
  return checkCharacter(character(), ',', currentPosition)
379
379
  }
380
380
  })
@@ -389,7 +389,7 @@ function initialise (stream, options = {}) {
389
389
 
390
390
  function fail (actual, expected, position) {
391
391
  return emit(
392
- events.error,
392
+ events.dataError,
393
393
  error.create(
394
394
  actual,
395
395
  expected,
package/src/write.js CHANGED
@@ -13,29 +13,32 @@ module.exports = write
13
13
  * JSON file on disk. Sanely handles promises, buffers, maps and other
14
14
  * iterables.
15
15
  *
16
- * @param path: Path to the JSON file.
16
+ * @param path: Path to the JSON file.
17
17
  *
18
- * @param data: The data to transform.
18
+ * @param data: The data to transform.
19
19
  *
20
- * @option space: Indentation string, or the number of spaces
21
- * to indent each nested level by.
20
+ * @option space: Indentation string, or the number of spaces
21
+ * to indent each nested level by.
22
22
  *
23
- * @option promises: 'resolve' or 'ignore', default is 'resolve'.
23
+ * @option promises: 'resolve' or 'ignore', default is 'resolve'.
24
24
  *
25
- * @option buffers: 'toString' or 'ignore', default is 'toString'.
25
+ * @option buffers: 'toString' or 'ignore', default is 'toString'.
26
26
  *
27
- * @option maps: 'object' or 'ignore', default is 'object'.
27
+ * @option maps: 'object' or 'ignore', default is 'object'.
28
28
  *
29
- * @option iterables: 'array' or 'ignore', default is 'array'.
29
+ * @option iterables: 'array' or 'ignore', default is 'array'.
30
30
  *
31
- * @option circular: 'error' or 'ignore', default is 'error'.
31
+ * @option circular: 'error' or 'ignore', default is 'error'.
32
32
  *
33
- * @option yieldRate: The number of data items to process per timeslice,
34
- * default is 16384.
33
+ * @option yieldRate: The number of data items to process per timeslice,
34
+ * default is 16384.
35
35
  *
36
- * @option bufferLength: The length of the buffer, default is 1024.
36
+ * @option bufferLength: The length of the buffer, default is 1024.
37
37
  *
38
- * @option Promise: The promise constructor to use, defaults to bluebird.
38
+ * @option highWaterMark: If set, will be passed to the readable stream constructor
39
+ * as the value for the highWaterMark option.
40
+ *
41
+ * @option Promise: The promise constructor to use, defaults to bluebird.
39
42
  **/
40
43
  function write (path, data, options) {
41
44
  const Promise = promise(options)
@@ -41,6 +41,14 @@ suite('integration:', () => {
41
41
  assert.lengthOf(bfj.walk, 1)
42
42
  })
43
43
 
44
+ test('match function is exported', () => {
45
+ assert.isFunction(bfj.match)
46
+ })
47
+
48
+ test('match expects two arguments', () => {
49
+ assert.lengthOf(bfj.match, 2)
50
+ })
51
+
44
52
  test('parse function is exported', () => {
45
53
  assert.isFunction(bfj.parse)
46
54
  })
@@ -161,61 +169,6 @@ suite('integration:', () => {
161
169
  })
162
170
  })
163
171
 
164
- suite('parse NDJSON:', () => {
165
- let failed, file, results
166
-
167
- setup(() => {
168
- failed = false
169
- file = path.join(__dirname, 'data.ndjson')
170
- results = []
171
- fs.writeFileSync(file, [
172
- JSON.stringify([ 'b', 'a', 'r' ]),
173
- JSON.stringify(null),
174
- '',
175
- '',
176
- JSON.stringify('wibble')
177
- ].join('\n'))
178
- const stream = fs.createReadStream(file)
179
- return bfj.parse(stream, { ndjson: true })
180
- .then(result => {
181
- results.push(result)
182
- return bfj.parse(stream, { ndjson: true })
183
- })
184
- .then(result => {
185
- results.push(result)
186
- return bfj.parse(stream, { ndjson: true })
187
- })
188
- .then(result => {
189
- results.push(result)
190
- return bfj.parse(stream, { ndjson: true })
191
- })
192
- .then(result => {
193
- results.push(result)
194
- return bfj.parse(stream, { ndjson: true })
195
- })
196
- .then(result => results.push(result))
197
- .catch(e => {
198
- failed = true
199
- })
200
- })
201
-
202
- teardown(() => {
203
- fs.unlinkSync(file)
204
- })
205
-
206
- test('results were correct', () => {
207
- assert.isFalse(failed)
208
- assert.lengthOf(results, 5)
209
- assert.deepEqual(results, [
210
- [ 'b', 'a', 'r' ],
211
- null,
212
- 'wibble',
213
- undefined,
214
- undefined
215
- ])
216
- })
217
- })
218
-
219
172
  suite('read error:', () => {
220
173
  let failed, file, result, error
221
174
 
@@ -264,12 +217,95 @@ suite('integration:', () => {
264
217
  })
265
218
  })
266
219
 
220
+ suite('match predicate:', () => {
221
+ let file, results, errors
222
+
223
+ setup(done => {
224
+ file = path.join(__dirname, 'data.json')
225
+ fs.writeFileSync(file, JSON.stringify({
226
+ foo: 'bar',
227
+ baz: 'qux',
228
+ wibble: 'blee'
229
+ }))
230
+ results = []
231
+ errors = []
232
+ const datastream = bfj.match(fs.createReadStream(file), (k, v) => k === 'baz' || v === 'blee')
233
+ datastream.on('data', item => results.push(item))
234
+ datastream.on('error', error => errors.push(error))
235
+ datastream.on('end', done)
236
+ })
237
+
238
+ test('the correct properties were matched', () => {
239
+ assert.deepEqual([ 'qux', 'blee' ], results)
240
+ })
241
+
242
+ test('no errors occurred', () => {
243
+ assert.deepEqual(errors, [])
244
+ })
245
+ })
246
+
247
+ suite('match nested:', () => {
248
+ let file, results, errors
249
+
250
+ setup(done => {
251
+ file = path.join(__dirname, 'data.json')
252
+ fs.writeFileSync(file, JSON.stringify({
253
+ foo: {
254
+ bar: 'baz'
255
+ }
256
+ }))
257
+ results = []
258
+ errors = []
259
+ const datastream = bfj.match(fs.createReadStream(file), () => true)
260
+ datastream.on('data', item => results.push(item))
261
+ datastream.on('error', error => errors.push(error))
262
+ datastream.on('end', done)
263
+ })
264
+
265
+ test('the correct properties were matched', () => {
266
+ assert.deepEqual([ 'baz', { bar: 'baz' }, { foo: { bar: 'baz' } } ], results)
267
+ })
268
+
269
+ test('no errors occurred', () => {
270
+ assert.deepEqual(errors, [])
271
+ })
272
+ })
273
+
274
+ suite('match ndjson:', () => {
275
+ let file, results, errors
276
+
277
+ setup(done => {
278
+ file = path.join(__dirname, 'data.ndjson')
279
+ fs.writeFileSync(file, [
280
+ JSON.stringify([ 'a', 'b' ]),
281
+ JSON.stringify(null),
282
+ '',
283
+ '',
284
+ JSON.stringify('wibble')
285
+ ].join('\n'))
286
+ results = []
287
+ errors = []
288
+ const datastream = bfj.match(fs.createReadStream(file), () => true, { ndjson: true })
289
+ datastream.on('data', item => results.push(item))
290
+ datastream.on('error', error => errors.push(error))
291
+ datastream.on('end', done)
292
+ })
293
+
294
+ test('the correct properties were matched', () => {
295
+ assert.deepEqual([ 'a', 'b', [ 'a', 'b' ], 'wibble' ], results)
296
+ })
297
+
298
+ test('no errors occurred', () => {
299
+ assert.deepEqual(errors, [])
300
+ })
301
+ })
302
+
267
303
  suite('parse request:', () => {
268
304
  let error, result
269
305
 
270
306
  setup(done => {
271
307
  const jsonstream = new stream.PassThrough()
272
- request({ url: 'https://raw.githubusercontent.com/philbooth/bfj/master/package.json' })
308
+ request({ url: 'https://gitlab.com/philbooth/bfj/raw/master/package.json' })
273
309
  .pipe(bfj.unpipe((err, res) => {
274
310
  error = err
275
311
  result = res
@@ -283,6 +319,61 @@ suite('integration:', () => {
283
319
  })
284
320
  })
285
321
 
322
+ suite('parse NDJSON:', () => {
323
+ let failed, file, results
324
+
325
+ setup(() => {
326
+ failed = false
327
+ file = path.join(__dirname, 'data.ndjson')
328
+ results = []
329
+ fs.writeFileSync(file, [
330
+ JSON.stringify([ 'b', 'a', 'r' ]),
331
+ JSON.stringify(null),
332
+ '',
333
+ '',
334
+ JSON.stringify('wibble')
335
+ ].join('\n'))
336
+ const stream = fs.createReadStream(file)
337
+ return bfj.parse(stream, { ndjson: true })
338
+ .then(result => {
339
+ results.push(result)
340
+ return bfj.parse(stream, { ndjson: true })
341
+ })
342
+ .then(result => {
343
+ results.push(result)
344
+ return bfj.parse(stream, { ndjson: true })
345
+ })
346
+ .then(result => {
347
+ results.push(result)
348
+ return bfj.parse(stream, { ndjson: true })
349
+ })
350
+ .then(result => {
351
+ results.push(result)
352
+ return bfj.parse(stream, { ndjson: true })
353
+ })
354
+ .then(result => results.push(result))
355
+ .catch(e => {
356
+ failed = true
357
+ })
358
+ })
359
+
360
+ teardown(() => {
361
+ fs.unlinkSync(file)
362
+ })
363
+
364
+ test('results were correct', () => {
365
+ assert.isFalse(failed)
366
+ assert.lengthOf(results, 5)
367
+ assert.deepEqual(results, [
368
+ [ 'b', 'a', 'r' ],
369
+ null,
370
+ 'wibble',
371
+ undefined,
372
+ undefined
373
+ ])
374
+ })
375
+ })
376
+
286
377
  suite('stringify value:', () => {
287
378
  let result
288
379