| 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 jsonpath = require('jsonpath')
|
|---|
| 8 | const walk = require('./walk')
|
|---|
| 9 |
|
|---|
| 10 | const DEFAULT_BUFFER_LENGTH = 1024
|
|---|
| 11 |
|
|---|
| 12 | module.exports = match
|
|---|
| 13 |
|
|---|
| 14 | /**
|
|---|
| 15 | * Public function `match`.
|
|---|
| 16 | *
|
|---|
| 17 | * Asynchronously parses a stream of JSON data, returning a stream of items
|
|---|
| 18 | * that match the argument. Note that if a value is `null`, it won't be matched
|
|---|
| 19 | * because `null` is used to signify end-of-stream in node.
|
|---|
| 20 | *
|
|---|
| 21 | * @param stream: Readable instance representing the incoming JSON.
|
|---|
| 22 | *
|
|---|
| 23 | * @param selector: Regular expression, string or predicate function used to
|
|---|
| 24 | * identify matches. If a regular expression or string is
|
|---|
| 25 | * passed, only property keys are tested. If a predicate is
|
|---|
| 26 | * passed, both the key and the value are passed to it as
|
|---|
| 27 | * arguments.
|
|---|
| 28 | *
|
|---|
| 29 | * @option minDepth: Number indicating the minimum depth to apply the selector
|
|---|
| 30 | * to. The default is `0`, but setting it to a higher value
|
|---|
| 31 | * can improve performance and reduce memory usage by
|
|---|
| 32 | * eliminating the need to actualise top-level items.
|
|---|
| 33 | *
|
|---|
| 34 | * @option numbers: Boolean, indicating whether numerical keys (e.g. array
|
|---|
| 35 | * indices) should be coerced to strings before testing the
|
|---|
| 36 | * match. Only applies if the `selector` argument is a string
|
|---|
| 37 | * or regular expression.
|
|---|
| 38 | *
|
|---|
| 39 | * @option ndjson: Set this to true to parse newline-delimited JSON,
|
|---|
| 40 | * default is `false`.
|
|---|
| 41 | *
|
|---|
| 42 | * @option yieldRate: The number of data items to process per timeslice,
|
|---|
| 43 | * default is 16384.
|
|---|
| 44 | *
|
|---|
| 45 | * @option bufferLength: The length of the match buffer, default is 1024.
|
|---|
| 46 | *
|
|---|
| 47 | * @option highWaterMark: If set, will be passed to the readable stream constructor
|
|---|
| 48 | * as the value for the highWaterMark option.
|
|---|
| 49 | *
|
|---|
| 50 | * @option Promise: The promise constructor to use, defaults to bluebird.
|
|---|
| 51 | **/
|
|---|
| 52 | function match (stream, selector, options = {}) {
|
|---|
| 53 | const keys = []
|
|---|
| 54 | const scopes = []
|
|---|
| 55 | const properties = []
|
|---|
| 56 | const emitter = walk(stream, options)
|
|---|
| 57 | const matches = new Hoopy(options.bufferLength || DEFAULT_BUFFER_LENGTH)
|
|---|
| 58 | let streamOptions
|
|---|
| 59 | const { highWaterMark } = options
|
|---|
| 60 | if (highWaterMark) {
|
|---|
| 61 | streamOptions = { highWaterMark }
|
|---|
| 62 | }
|
|---|
| 63 | const results = new DataStream(read, streamOptions)
|
|---|
| 64 |
|
|---|
| 65 | let selectorFunction, selectorPath, selectorString, resume
|
|---|
| 66 | let coerceNumbers = false
|
|---|
| 67 | let awaitPush = true
|
|---|
| 68 | let isEnded = false
|
|---|
| 69 | let length = 0
|
|---|
| 70 | let index = 0
|
|---|
| 71 |
|
|---|
| 72 | const minDepth = options.minDepth || 0
|
|---|
| 73 | check.assert.greaterOrEqual(minDepth, 0)
|
|---|
| 74 |
|
|---|
| 75 | if (check.function(selector)) {
|
|---|
| 76 | selectorFunction = selector
|
|---|
| 77 | 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 | })
|
|---|
| 92 | } else {
|
|---|
| 93 | selectorString = selector
|
|---|
| 94 | coerceNumbers = !! options.numbers
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | selector = null
|
|---|
| 98 | } else {
|
|---|
| 99 | check.assert.instanceStrict(selector, RegExp)
|
|---|
| 100 | coerceNumbers = !! options.numbers
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | emitter.on(events.array, array)
|
|---|
| 104 | emitter.on(events.object, object)
|
|---|
| 105 | emitter.on(events.property, property)
|
|---|
| 106 | emitter.on(events.endArray, endScope)
|
|---|
| 107 | emitter.on(events.endObject, endScope)
|
|---|
| 108 | emitter.on(events.string, value)
|
|---|
| 109 | emitter.on(events.number, value)
|
|---|
| 110 | emitter.on(events.literal, value)
|
|---|
| 111 | emitter.on(events.end, end)
|
|---|
| 112 | emitter.on(events.error, error)
|
|---|
| 113 | emitter.on(events.dataError, dataError)
|
|---|
| 114 |
|
|---|
| 115 | return results
|
|---|
| 116 |
|
|---|
| 117 | function read () {
|
|---|
| 118 | if (awaitPush) {
|
|---|
| 119 | awaitPush = false
|
|---|
| 120 |
|
|---|
| 121 | if (isEnded) {
|
|---|
| 122 | if (length > 0) {
|
|---|
| 123 | after()
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | return endResults()
|
|---|
| 127 | }
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | if (resume) {
|
|---|
| 131 | const resumeCopy = resume
|
|---|
| 132 | resume = null
|
|---|
| 133 | resumeCopy()
|
|---|
| 134 | after()
|
|---|
| 135 | }
|
|---|
| 136 | }
|
|---|
| 137 |
|
|---|
| 138 | function after () {
|
|---|
| 139 | if (awaitPush || resume) {
|
|---|
| 140 | return
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | let i
|
|---|
| 144 |
|
|---|
| 145 | for (i = 0; i < length && ! resume; ++i) {
|
|---|
| 146 | if (! results.push(matches[i + index])) {
|
|---|
| 147 | pause()
|
|---|
| 148 | }
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | if (i === length) {
|
|---|
| 152 | index = length = 0
|
|---|
| 153 | } else {
|
|---|
| 154 | length -= i
|
|---|
| 155 | index += i
|
|---|
| 156 | }
|
|---|
| 157 | }
|
|---|
| 158 |
|
|---|
| 159 | function pause () {
|
|---|
| 160 | resume = emitter.pause()
|
|---|
| 161 | }
|
|---|
| 162 |
|
|---|
| 163 | function endResults () {
|
|---|
| 164 | if (! awaitPush) {
|
|---|
| 165 | results.push(null)
|
|---|
| 166 | }
|
|---|
| 167 | }
|
|---|
| 168 |
|
|---|
| 169 | function array () {
|
|---|
| 170 | scopes.push([])
|
|---|
| 171 | }
|
|---|
| 172 |
|
|---|
| 173 | function object () {
|
|---|
| 174 | scopes.push({})
|
|---|
| 175 | }
|
|---|
| 176 |
|
|---|
| 177 | function property (name) {
|
|---|
| 178 | keys.push(name)
|
|---|
| 179 |
|
|---|
| 180 | if (scopes.length < minDepth) {
|
|---|
| 181 | return
|
|---|
| 182 | }
|
|---|
| 183 |
|
|---|
| 184 | properties.push(name)
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | function endScope () {
|
|---|
| 188 | if (selectorPath) {
|
|---|
| 189 | keys.pop()
|
|---|
| 190 | }
|
|---|
| 191 | value(scopes.pop())
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | function value (v) {
|
|---|
| 195 | let key
|
|---|
| 196 |
|
|---|
| 197 | if (scopes.length < minDepth) {
|
|---|
| 198 | return
|
|---|
| 199 | }
|
|---|
| 200 |
|
|---|
| 201 | if (scopes.length > 0) {
|
|---|
| 202 | const scope = scopes[scopes.length - 1]
|
|---|
| 203 |
|
|---|
| 204 | if (Array.isArray(scope)) {
|
|---|
| 205 | key = scope.length
|
|---|
| 206 | } else {
|
|---|
| 207 | key = properties.pop()
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | scope[key] = v
|
|---|
| 211 | }
|
|---|
| 212 |
|
|---|
| 213 | if (v === null) {
|
|---|
| 214 | return
|
|---|
| 215 | }
|
|---|
| 216 |
|
|---|
| 217 | if (selectorFunction) {
|
|---|
| 218 | if (selectorFunction(key, v, scopes.length)) {
|
|---|
| 219 | push(v)
|
|---|
| 220 | }
|
|---|
| 221 | } else if (selectorPath) {
|
|---|
| 222 | if (isSelectorPathSatisfied([ ...keys, key ])) {
|
|---|
| 223 | push(v)
|
|---|
| 224 | }
|
|---|
| 225 | } else {
|
|---|
| 226 | if (coerceNumbers && typeof key === 'number') {
|
|---|
| 227 | key = key.toString()
|
|---|
| 228 | }
|
|---|
| 229 |
|
|---|
| 230 | if ((selectorString && selectorString === key) || (selector && selector.test(key))) {
|
|---|
| 231 | push(v)
|
|---|
| 232 | }
|
|---|
| 233 | }
|
|---|
| 234 | }
|
|---|
| 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 |
|
|---|
| 264 | function push (v) {
|
|---|
| 265 | if (length + 1 === matches.length) {
|
|---|
| 266 | pause()
|
|---|
| 267 | }
|
|---|
| 268 |
|
|---|
| 269 | matches[index + length++] = v
|
|---|
| 270 |
|
|---|
| 271 | after()
|
|---|
| 272 | }
|
|---|
| 273 |
|
|---|
| 274 | function end () {
|
|---|
| 275 | isEnded = true
|
|---|
| 276 | endResults()
|
|---|
| 277 | }
|
|---|
| 278 |
|
|---|
| 279 | function error (e) {
|
|---|
| 280 | results.emit('error', e)
|
|---|
| 281 | }
|
|---|
| 282 |
|
|---|
| 283 | function dataError (e) {
|
|---|
| 284 | results.emit('dataError', e)
|
|---|
| 285 | }
|
|---|
| 286 | }
|
|---|