'use strict' const check = require('check-types') const events = require('./events') const promise = require('./promise') const walk = require('./walk') module.exports = parse const NDJSON_STATE = new Map() /** * Public function `parse`. * * Returns a promise and asynchronously parses a stream of JSON data. If * there are no errors, the promise is resolved with the parsed data. If * errors occur, the promise is rejected with the first error. * * @param stream: Readable instance representing the incoming JSON. * * @option reviver: Transformation function, invoked depth-first. * * @option yieldRate: The number of data items to process per timeslice, * default is 16384. * * @option Promise: The promise constructor to use, defaults to bluebird. * * @option ndjson: Set this to true to parse newline-delimited JSON. In * this case, each call will be resolved with one value * from the stream. To parse the entire stream, calls * should be made sequentially one-at-a-time until the * returned promise resolves to `undefined`. **/ function parse (stream, options = {}) { const Promise = promise(options) try { check.assert.maybe.function(options.reviver, 'Invalid reviver option') } catch (err) { return Promise.reject(err) } const errors = [] const scopes = [] const reviver = options.reviver const shouldHandleNdjson = !! options.ndjson let emitter, resolve, reject, scopeKey if (shouldHandleNdjson && NDJSON_STATE.has(stream)) { const state = NDJSON_STATE.get(stream) NDJSON_STATE.delete(stream) emitter = state.emitter setImmediate(state.resume) } else { emitter = walk(stream, options) } emitter.on(events.array, array) emitter.on(events.object, object) emitter.on(events.property, property) emitter.on(events.string, value) emitter.on(events.number, value) emitter.on(events.literal, value) emitter.on(events.endArray, endScope) emitter.on(events.endObject, endScope) emitter.on(events.end, end) emitter.on(events.error, error) emitter.on(events.dataError, error) if (shouldHandleNdjson) { emitter.on(events.endLine, endLine) } return new Promise((res, rej) => { resolve = res reject = rej }) function array () { if (errors.length > 0) { return } beginScope([]) } function beginScope (parsed) { if (errors.length > 0) { return } if (scopes.length > 0) { value(parsed) } scopes.push(parsed) } function value (v) { if (errors.length > 0) { return } if (scopes.length === 0) { return scopes.push(v) } const scope = scopes[scopes.length - 1] if (scopeKey) { scope[scopeKey] = v scopeKey = null } else { scope.push(v) } } function object () { if (errors.length > 0) { return } beginScope({}) } function property (name) { if (errors.length > 0) { return } scopeKey = name } function endScope () { if (errors.length > 0) { return } if (scopes.length > 1) { scopes.pop() } } function end () { if (shouldHandleNdjson) { const resume = emitter.pause() emitter.removeAllListeners() NDJSON_STATE.set(stream, { emitter, resume }) } if (errors.length > 0) { return reject(errors[0]) } if (reviver) { scopes[0] = transform(scopes[0], '') } resolve(scopes[0]) } function transform (obj, key) { if (obj && typeof obj === 'object') { Object.keys(obj).forEach(childKey => { obj[childKey] = transform(obj[childKey], childKey) }) } return reviver(key, obj) } function error (e) { errors.push(e) } function endLine () { if (scopes.length > 0) { end() } } }