bfj 7.0.2 → 7.1.0

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/HISTORY.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # History
2
2
 
3
+ ## 7.1.0
4
+
5
+ ### New features
6
+
7
+ * match: support jsonpath expressions (96e3a20eea18efd982867dd894c2f1c2a649321e)
8
+
9
+ ### Other changes
10
+
11
+ * deps: upgrade dependencies (f6e8664281931e0eeea26d62059bfbc9e9a8c37d)
12
+ * tests: add perf test for bfj.match (f345bc83e5fbd9204cab02169a2ae9f4c02d1450)
13
+
3
14
  ## 7.0.2
4
15
 
5
16
  ### Other changes
package/README.md CHANGED
@@ -243,7 +243,7 @@ request({ url }).pipe(bfj.unpipe((error, data) => {
243
243
  ```js
244
244
  const bfj = require('bfj');
245
245
 
246
- // Call match with your stream and a selector predicate/regex/string
246
+ // Call match with your stream and a selector predicate/regex/JSONPath/string
247
247
  const dataStream = bfj.match(jsonStream, selector, options);
248
248
 
249
249
  // Get data out of the returned stream with event handlers
@@ -264,7 +264,7 @@ It takes three arguments:
264
264
  a [readable stream][readable]
265
265
  from which the JSON will be parsed;
266
266
  a selector argument for determining matches,
267
- which may be a string, a regular expression or a predicate function;
267
+ which may be a string, a regular expression, a JSONPath expression, or a predicate function;
268
268
  and an [options](#options-for-parsing-functions) object.
269
269
 
270
270
  If the selector is a string,
@@ -275,6 +275,9 @@ If it is a regular expression,
275
275
  the comparison will be made
276
276
  by calling the [RegExp `test` method][regexp-test]
277
277
  with the property key.
278
+ If it is a JSONPath expression,
279
+ it must start with `$.` to identify the root node
280
+ and only use `child` scope expressions for subsequent nodes.
278
281
  Predicate functions will be called with three arguments:
279
282
  `key`, `value` and `depth`.
280
283
  If the result of the predicate is a truthy value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfj",
3
- "version": "7.0.2",
3
+ "version": "7.1.0",
4
4
  "description": "Big-friendly JSON. Asynchronous streaming functions for large JSON data sets.",
5
5
  "homepage": "https://gitlab.com/philbooth/bfj",
6
6
  "bugs": "https://gitlab.com/philbooth/bfj/issues",
@@ -29,18 +29,19 @@
29
29
  "node": ">= 8.0.0"
30
30
  },
31
31
  "dependencies": {
32
- "bluebird": "^3.5.5",
33
- "check-types": "^11.1.1",
32
+ "bluebird": "^3.7.2",
33
+ "check-types": "^11.2.3",
34
34
  "hoopy": "^0.1.4",
35
+ "jsonpath": "^1.1.1",
35
36
  "tryer": "^1.0.1"
36
37
  },
37
38
  "devDependencies": {
38
- "chai": "^4.2.0",
39
- "eslint": "^6.0.1",
40
- "mocha": "^6.1.4",
41
- "please-release-me": "^2.1.2",
42
- "proxyquire": "^2.1.0",
43
- "request": "^2.88.0",
39
+ "axios": "^1.5.0",
40
+ "chai": "^4.3.8",
41
+ "eslint": "^8.48.0",
42
+ "mocha": "^10.2.0",
43
+ "please-release-me": "^2.1.4",
44
+ "proxyquire": "^2.1.3",
44
45
  "spooks": "^2.0.0"
45
46
  },
46
47
  "scripts": {
package/src/match.js CHANGED
@@ -4,6 +4,7 @@ const check = require('check-types')
4
4
  const DataStream = require('./datastream')
5
5
  const events = require('./events')
6
6
  const Hoopy = require('hoopy')
7
+ const jsonpath = require('jsonpath')
7
8
  const walk = require('./walk')
8
9
 
9
10
  const DEFAULT_BUFFER_LENGTH = 1024
@@ -49,6 +50,7 @@ module.exports = match
49
50
  * @option Promise: The promise constructor to use, defaults to bluebird.
50
51
  **/
51
52
  function match (stream, selector, options = {}) {
53
+ const keys = []
52
54
  const scopes = []
53
55
  const properties = []
54
56
  const emitter = walk(stream, options)
@@ -60,7 +62,7 @@ function match (stream, selector, options = {}) {
60
62
  }
61
63
  const results = new DataStream(read, streamOptions)
62
64
 
63
- let selectorFunction, selectorString, resume
65
+ let selectorFunction, selectorPath, selectorString, resume
64
66
  let coerceNumbers = false
65
67
  let awaitPush = true
66
68
  let isEnded = false
@@ -73,16 +75,29 @@ function match (stream, selector, options = {}) {
73
75
  if (check.function(selector)) {
74
76
  selectorFunction = selector
75
77
  selector = null
76
- } else {
77
- coerceNumbers = !! options.numbers
78
-
79
- if (check.string(selector)) {
80
- check.assert.nonEmptyString(selector)
81
- selectorString = selector
82
- selector = null
78
+ } else if (check.string(selector)) {
79
+ check.assert.nonEmptyString(selector)
80
+
81
+ if (selector.startsWith('$.')) {
82
+ selectorPath = jsonpath.parse(selector)
83
+ check.assert.identical(selectorPath.shift(), {
84
+ expression: {
85
+ type: 'root',
86
+ value: '$',
87
+ },
88
+ })
89
+ selectorPath.forEach((part) => {
90
+ check.assert.equal(part.scope, 'child')
91
+ })
83
92
  } else {
84
- check.assert.instanceStrict(selector, RegExp)
93
+ selectorString = selector
94
+ coerceNumbers = !! options.numbers
85
95
  }
96
+
97
+ selector = null
98
+ } else {
99
+ check.assert.instanceStrict(selector, RegExp)
100
+ coerceNumbers = !! options.numbers
86
101
  }
87
102
 
88
103
  emitter.on(events.array, array)
@@ -160,6 +175,8 @@ function match (stream, selector, options = {}) {
160
175
  }
161
176
 
162
177
  function property (name) {
178
+ keys.push(name)
179
+
163
180
  if (scopes.length < minDepth) {
164
181
  return
165
182
  }
@@ -168,6 +185,9 @@ function match (stream, selector, options = {}) {
168
185
  }
169
186
 
170
187
  function endScope () {
188
+ if (selectorPath) {
189
+ keys.pop()
190
+ }
171
191
  value(scopes.pop())
172
192
  }
173
193
 
@@ -198,6 +218,10 @@ function match (stream, selector, options = {}) {
198
218
  if (selectorFunction(key, v, scopes.length)) {
199
219
  push(v)
200
220
  }
221
+ } else if (selectorPath) {
222
+ if (isSelectorPathSatisfied([ ...keys, key ])) {
223
+ push(v)
224
+ }
201
225
  } else {
202
226
  if (coerceNumbers && typeof key === 'number') {
203
227
  key = key.toString()
@@ -209,6 +233,34 @@ function match (stream, selector, options = {}) {
209
233
  }
210
234
  }
211
235
 
236
+ function isSelectorPathSatisfied (path) {
237
+ if (selectorPath.length !== path.length) {
238
+ return false
239
+ }
240
+
241
+ return selectorPath.every(({ expression, operation }, i) => {
242
+ if (
243
+ (operation === 'member' && expression.type === 'identifier') ||
244
+ (operation === 'subscript' && (
245
+ expression.type === 'string_literal' ||
246
+ expression.type === 'numeric_literal'
247
+ ))
248
+ ) {
249
+ return path[i] === expression.value
250
+ }
251
+
252
+ if (
253
+ operation === 'subscript' &&
254
+ expression.type === 'wildcard' &&
255
+ expression.value === '*'
256
+ ) {
257
+ return true
258
+ }
259
+
260
+ return false
261
+ })
262
+ }
263
+
212
264
  function push (v) {
213
265
  if (length + 1 === matches.length) {
214
266
  pause()
@@ -1,10 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('chai').assert
4
+ const axios = require('axios')
4
5
  const fs = require('fs')
5
6
  const path = require('path')
6
7
  const Promise = require('bluebird')
7
- const request = require('request')
8
8
  const stream = require('stream')
9
9
 
10
10
  const modulePath = '../src'
@@ -307,14 +307,16 @@ suite('integration:', () => {
307
307
  suite('parse request:', () => {
308
308
  let error, result
309
309
 
310
- setup(done => {
310
+ setup((done) => {
311
311
  const jsonstream = new stream.PassThrough()
312
- request({ url: 'https://gitlab.com/philbooth/bfj/raw/master/package.json' })
313
- .pipe(bfj.unpipe((err, res) => {
314
- error = err
315
- result = res
316
- done()
317
- }))
312
+ axios({
313
+ responseType: 'stream',
314
+ url: 'https://gitlab.com/philbooth/bfj/raw/master/package.json',
315
+ }).then((response) => response.data.pipe(bfj.unpipe((err, res) => {
316
+ error = err
317
+ result = res
318
+ done()
319
+ })))
318
320
  })
319
321
 
320
322
  test('result was correct', () => {
@@ -7,18 +7,41 @@ const path = require('path')
7
7
  const check = require('check-types')
8
8
  const bfj = require('../src')
9
9
 
10
- console.log('reading json')
10
+ const inPath = getDataPath('.json');
11
11
 
12
12
  let time = process.hrtime()
13
13
 
14
- bfj.read(getDataPath('.json'))
15
- .then(data => {
14
+ if (process.argv.length === 4) {
15
+ const stuff = []
16
+ const stream = bfj.match(fs.createReadStream(inPath), process.argv[3])
17
+ stream.on('data', thing => stuff.push(thing))
18
+ stream.on('end', () => {
16
19
  reportTime()
17
- console.log('writing json')
18
- return bfj.write(getDataPath('-result.json'), data)
20
+ console.log('hooray!', stuff.length)
21
+ fs.writeFileSync(getDataPath('-result.ndjson'), stuff.map(s => JSON.stringify(s)).join('\n'), {
22
+ encoding: 'utf8',
23
+ })
24
+ process.exit(0)
19
25
  })
20
- .then(() => done('succeeded'))
21
- .catch(error => done(error.stack, 1))
26
+ stream.on('error', error => {
27
+ console.error('error!', error.stack)
28
+ process.exit(1)
29
+ })
30
+ stream.on('dataError', error => {
31
+ console.error('dataError!', error.stack)
32
+ process.exit(2)
33
+ })
34
+ } else {
35
+ console.log('reading json')
36
+ bfj.read(inPath)
37
+ .then(data => {
38
+ reportTime()
39
+ console.log('writing json')
40
+ return bfj.write(getDataPath('-result.json'), data)
41
+ })
42
+ .then(() => done('succeeded'))
43
+ .catch(error => done(error.stack, 1))
44
+ }
22
45
 
23
46
  function getDataPath (suffix) {
24
47
  return path.resolve(__dirname, process.argv[2] + suffix)
@@ -915,6 +915,65 @@ suite('match:', () => {
915
915
  })
916
916
  })
917
917
 
918
+ suite('match with jsonpath expression:', () => {
919
+ let stream, options, result
920
+
921
+ setup(() => {
922
+ result = match({}, '$.foo.bar[*]', {})
923
+ })
924
+
925
+ test('DataStream was called once', () => {
926
+ assert.strictEqual(log.counts.DataStream, 1)
927
+ })
928
+
929
+ test('walk was called once', () => {
930
+ assert.strictEqual(log.counts.walk, 1)
931
+ })
932
+
933
+ test('EventEmitter.on was called eleven times', () => {
934
+ assert.strictEqual(log.counts.on, 11)
935
+ })
936
+
937
+ suite('read events:', () => {
938
+ setup(() => {
939
+ log.args.DataStream[0][0]()
940
+ // { "foo": { "bar": [ "baz", "qux" ], "wibble": "blee" } }
941
+ log.args.on[1][1]()
942
+ log.args.on[2][1]('foo')
943
+ log.args.on[1][1]()
944
+ log.args.on[2][1]('bar')
945
+ log.args.on[0][1]()
946
+ log.args.on[5][1]('baz')
947
+ log.args.on[5][1]('qux')
948
+ log.args.on[3][1]()
949
+ log.args.on[2][1]('wibble')
950
+ log.args.on[5][1]('blee')
951
+ log.args.on[4][1]()
952
+ log.args.on[8][1]()
953
+ })
954
+
955
+ test('results.push was called three times', () => {
956
+ assert.strictEqual(log.counts.push, 3)
957
+ })
958
+
959
+ test('results.push was called correctly first time', () => {
960
+ assert.strictEqual(log.args.push[0][0], 'baz')
961
+ })
962
+
963
+ test('results.push was called correctly second time', () => {
964
+ assert.strictEqual(log.args.push[1][0], 'qux')
965
+ })
966
+
967
+ test('results.push was called correctly third time', () => {
968
+ assert.isNull(log.args.push[2][0])
969
+ })
970
+
971
+ test('results.emit was not called', () => {
972
+ assert.strictEqual(log.counts.emit, 0)
973
+ })
974
+ })
975
+ })
976
+
918
977
  suite('match with numbers=true:', () => {
919
978
  let stream, options, result
920
979