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 +11 -0
- package/README.md +5 -2
- package/package.json +10 -9
- package/src/match.js +61 -9
- package/test/integration.js +10 -8
- package/test/performance.js +30 -7
- package/test/unit/match.js +59 -0
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
|
|
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.
|
|
33
|
-
"check-types": "^11.
|
|
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
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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()
|
package/test/integration.js
CHANGED
|
@@ -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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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', () => {
|
package/test/performance.js
CHANGED
|
@@ -7,18 +7,41 @@ const path = require('path')
|
|
|
7
7
|
const check = require('check-types')
|
|
8
8
|
const bfj = require('../src')
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const inPath = getDataPath('.json');
|
|
11
11
|
|
|
12
12
|
let time = process.hrtime()
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
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('
|
|
18
|
-
|
|
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
|
-
.
|
|
21
|
-
|
|
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)
|
package/test/unit/match.js
CHANGED
|
@@ -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
|
|