var assert = require('assert'); var dict = require('./dict'); var Parser = require('./parser'); var Handlers = require('./handlers'); var JSONPath = function() { this.initialize.apply(this, arguments); }; JSONPath.prototype.initialize = function() { this.parser = new Parser(); this.handlers = new Handlers(); }; JSONPath.prototype.parse = function(string) { assert.ok(_is_string(string), "we need a path"); return this.parser.parse(string); }; JSONPath.prototype.parent = function(obj, string) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(string, "we need a path"); var node = this.nodes(obj, string)[0]; var key = node.path.pop(); /* jshint unused:false */ return this.value(obj, node.path); } JSONPath.prototype.apply = function(obj, string, fn) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(string, "we need a path"); assert.equal(typeof fn, "function", "fn needs to be function") var nodes = this.nodes(obj, string).sort(function(a, b) { // sort nodes so we apply from the bottom up return b.path.length - a.path.length; }); nodes.forEach(function(node) { var key = node.path.pop(); var parent = this.value(obj, this.stringify(node.path)); var val = node.value = fn.call(obj, parent[key]); parent[key] = val; }, this); return nodes; } JSONPath.prototype.value = function(obj, path, value) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(path, "we need a path"); if (arguments.length >= 3) { var node = this.nodes(obj, path).shift(); if (!node) return this._vivify(obj, path, value); var key = node.path.slice(-1).shift(); var parent = this.parent(obj, this.stringify(node.path)); parent[key] = value; } return this.query(obj, this.stringify(path), 1).shift(); } JSONPath.prototype._vivify = function(obj, string, value) { var self = this; assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(string, "we need a path"); var path = this.parser.parse(string) .map(function(component) { return component.expression.value }); var setValue = function(path, value) { var key = path.pop(); var node = self.value(obj, path); if (!node) { setValue(path.concat(), typeof key === 'string' ? {} : []); node = self.value(obj, path); } node[key] = value; } setValue(path, value); return this.query(obj, string)[0]; } JSONPath.prototype.query = function(obj, string, count) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(_is_string(string), "we need a path"); var results = this.nodes(obj, string, count) .map(function(r) { return r.value }); return results; }; JSONPath.prototype.paths = function(obj, string, count) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(string, "we need a path"); var results = this.nodes(obj, string, count) .map(function(r) { return r.path }); return results; }; JSONPath.prototype.nodes = function(obj, string, count) { assert.ok(obj instanceof Object, "obj needs to be an object"); assert.ok(string, "we need a path"); if (count === 0) return []; var path = this.parser.parse(string); var handlers = this.handlers; var partials = [ { path: ['$'], value: obj } ]; var matches = []; if (path.length && path[0].expression.type == 'root') path.shift(); if (!path.length) return partials; path.forEach(function(component, index) { if (matches.length >= count) return; var handler = handlers.resolve(component); var _partials = []; partials.forEach(function(p) { if (matches.length >= count) return; var results = handler(component, p, count); if (index == path.length - 1) { // if we're through the components we're done matches = matches.concat(results || []); } else { // otherwise accumulate and carry on through _partials = _partials.concat(results || []); } }); partials = _partials; }); return count ? matches.slice(0, count) : matches; }; JSONPath.prototype.stringify = function(path) { assert.ok(path, "we need a path"); var string = '$'; var templates = { 'descendant-member': '..{{value}}', 'child-member': '.{{value}}', 'descendant-subscript': '..[{{value}}]', 'child-subscript': '[{{value}}]' }; path = this._normalize(path); path.forEach(function(component) { if (component.expression.type == 'root') return; var key = [component.scope, component.operation].join('-'); var template = templates[key]; var value; if (component.expression.type == 'string_literal') { value = JSON.stringify(component.expression.value) } else { value = component.expression.value; } if (!template) throw new Error("couldn't find template " + key); string += template.replace(/{{value}}/, value); }); return string; } JSONPath.prototype._normalize = function(path) { assert.ok(path, "we need a path"); if (typeof path == "string") { return this.parser.parse(path); } else if (Array.isArray(path) && typeof path[0] == "string") { var _path = [ { expression: { type: "root", value: "$" } } ]; path.forEach(function(component, index) { if (component == '$' && index === 0) return; if (typeof component == "string" && component.match("^" + dict.identifier + "$")) { _path.push({ operation: 'member', scope: 'child', expression: { value: component, type: 'identifier' } }); } else { var type = typeof component == "number" ? 'numeric_literal' : 'string_literal'; _path.push({ operation: 'subscript', scope: 'child', expression: { value: component, type: type } }); } }); return _path; } else if (Array.isArray(path) && typeof path[0] == "object") { return path } throw new Error("couldn't understand path " + path); } function _is_string(obj) { return Object.prototype.toString.call(obj) == '[object String]'; } JSONPath.Handlers = Handlers; JSONPath.Parser = Parser; var instance = new JSONPath; instance.JSONPath = JSONPath; module.exports = instance;