jspath
jspath copied to clipboard
Actual “find” method
Right now I can see only apply
method that returns the values themselves. However if I'd like to manipulate the data in some way, I'd need to know those items' locations.
What I propose is to add something like JSPath.find
method, which would get all the things you could throw to the apply
method:
JSPath.find(
'.automobiles{.maker === "Honda" && .year > 2009}.model',
{
"automobiles" : [
{ "maker" : "Nissan", "model" : "Teana", "year" : 2011 },
{ "maker" : "Honda", "model" : "Jazz", "year" : 2010 },
{ "maker" : "Honda", "model" : "Civic", "year" : 2007 },
{ "maker" : "Toyota", "model" : "Yaris", "year" : 2008 },
{ "maker" : "Honda", "model" : "Accord", "year" : 2011 }
],
"motorcycles" : [{ "maker" : "Honda", "model" : "ST1300", "year" : 2012 }]
});
But this would return an array of the “steps” which lead to every found element, so in the case above the result would be:
[
[ "automobiles", 1, "model" ],
[ "automobiles", 4, "model" ]
]
This way you could then easily find any specific item or it's parent/siblings using any helper functions, so you then could change this item (like in #13) or change anything around it — its parents or siblings.
I will think about it )
Why not use find to return a reference instead of values? Since JS is all about "call-by-reference", one could easily modify those returned references to manipulate the original object.
jspath returns reference to original object, but this object has no context with it parent/siblings.
+1 for 'find'
+1
+1, that would be nice to have
+1
For me, I'd rather have a way to apply a function to the matching data and replace their values, i.e.,
var data = {
"automobiles" : [
{ "maker" : "Nissan", "model" : "Teana", "year" : 2011 },
{ "maker" : "Honda", "model" : "Jazz", "year" : 2010 },
{ "maker" : "Honda", "model" : "Civic", "year" : 2007 },
{ "maker" : "Toyota", "model" : "Yaris", "year" : 2008 },
{ "maker" : "Honda", "model" : "Accord", "year" : 2011 }
],
"motorcycles" : [{ "maker" : "Honda", "model" : "ST1300", "year" : 2012 }]
};
JSPath.apply('.automobiles{.maker === "Honda" && .year > 2009}.model',
data, function(name) { return name+" (found)" });
Which would have the effect of changing data
to:
{
"automobiles" : [
{ "maker" : "Nissan", "model" : "Teana", "year" : 2011 },
{ "maker" : "Honda", "model" : "Jazz (found)", "year" : 2010 },
{ "maker" : "Honda", "model" : "Civic", "year" : 2007 },
{ "maker" : "Toyota", "model" : "Yaris", "year" : 2008 },
{ "maker" : "Honda", "model" : "Accord (found)", "year" : 2011 }
],
"motorcycles" : [{ "maker" : "Honda", "model" : "ST1300", "year" : 2012 }]
}
The existing name apply
almost gives the sense that you are applying a transformation. But you aren't. However, I suspect you could actually add the functionality I'm describing above in a backward compatible way (since it just adds an additional, optional argument).
Just a thought.
@xonegy Well in this case you would want to get the .automobiles object. Javascript returns Objects by reference, so if you set or delete a property of the object, it will update the original.
Consider this example: https://gist.github.com/formula1/07d23b0e3842c7658f8d
Here we only make modifications to res yet it changes j.
As for getting a direct path from point A to point B, it could be nice. mpath is far less powerful than jspath, however it is an example of a direct path from point A to point B.
Sample data "borrowed" from here: http://json.org/example
If you want a cleaner jquery-esque interface, heres a wrapper Object https://gist.github.com/formula1/1c12acb2b21beb3943c7
it needs the JSPath to be in a variable names jspath and you'll want to create a new
one when you want to use it. Additionally, its important to get the parent object and not the actual value itself as its almost impossible to backtrace without parsing your string and running jspath a second time with the values enumerated (which is more work than 5 minutes. However you have accessing to chaining like so
new PathWrapper(".a..path{to your things}").apply(myOb).set("a-path", "a-value").delete("uglypath").get() == original values
+1 for getting the path instead of the object or even both like in https://www.npmjs.com/package/jsonpath with jp.nodes
would love to use jspath for that, because I really like the syntax.
+1 I also need paths to found nodes, which JSONPath provides. Unfortunately JSONPath's query syntax can't filter by path and value simultaneously, so JSPath does better there. But without paths, JSPath is relatively useless for traversing the large JSON trees returned by parsers like Babylon.
@dfilatov I went ahead and wrote what I thought was a way to index the json by value -> path so that paths could be looked up for JSPath results. It kind of works but only if every value in the json is a unique reference, which is unlikely in practice:
const JSPath = require('jspath');
const traverse = require('traverse');
// index object as map of each value's reference -> path
function toValuePaths(object) {
return traverse.reduce(
object,
function(accumulator) {
if (accumulator.get(this.node) !== undefined) {
console.log("Object's reference is not unique: ", this.path);
// throw new Error("Object's reference is not unique: " + JSON.stringify(this.path));
}
accumulator.set(this.node, this.path);
return accumulator;
},
new Map()
);
}
// convert JSPath results to {path: ..., value: ...}
function jspathNodes(jspathResults, valuePaths) {
const isArray = Array.isArray(jspathResults);
if (!isArray) {
jspathResults = [jspathResults];
}
const nodes = jspathResults.map(function(reference) {
return { path: valuePaths.get(reference), value: reference };
});
return isArray ? nodes : nodes[0];
}
{
const json = {
automobiles: [
{ maker: 'Nissan', model: 'Teana', year: 2011 },
{ maker: 'Honda', model: 'Jazz', year: 2010 },
{ maker: 'Honda', model: 'Civic', year: 2007 },
{ maker: 'Toyota', model: 'Yaris', year: 2008 },
{ maker: 'Honda', model: 'Accord', year: 2011 }
],
motorcycles: [{ maker: 'Honda', model: 'ST1300', year: 2012 }]
};
const jsonMap = toValuePaths(json);
console.log('works (for unique node references):');
console.log(
jspathNodes(
JSPath.apply(
'.automobiles{.maker === "Honda" && .year > 2009}.model',
json
),
jsonMap
)
);
}
console.log();
{
const json = {
a: 'hi',
b: 'hi',
c: 'hi'
};
const jsonMap = toValuePaths(json);
console.log("fails (node references aren't unique):");
console.log(jspathNodes(JSPath.apply('..*', json), jsonMap));
}
Output:
Object's reference is not unique: [ 'automobiles', '2', 'maker' ]
Object's reference is not unique: [ 'automobiles', '4', 'maker' ]
Object's reference is not unique: [ 'automobiles', '4', 'year' ]
Object's reference is not unique: [ 'motorcycles', '0', 'maker' ]
works (for unique node references):
[ { path: [ 'automobiles', '1', 'model' ], value: 'Jazz' },
{ path: [ 'automobiles', '4', 'model' ], value: 'Accord' } ]
Object's reference is not unique: [ 'b' ]
Object's reference is not unique: [ 'c' ]
fails (node references aren't unique):
[ { path: [ 'c' ], value: 'hi' },
{ path: [ 'c' ], value: 'hi' },
{ path: [ 'c' ], value: 'hi' } ]
Live demo: https://repl.it/repls/SweetThickProcessors
As you can see, if people really need the paths for results then there is really no way for them to get them themselves. It would be awesome if you could implement a call to return the results as something like [{node: ..., value ...}, ...]
similar to the way that JSONPath.nodes()
works. Short of that, I think that xogeny's callback implementation would be least intrusive. The callback function should be of the form:
function (value, path, object) {
}
Where the path and original object arguments are optional, which is similar to the Array.map() prototype. Note that there is some consensus on JavaScript object paths as an array of keys, but converting that to a string is somewhat of an open question due to the existence of keys like .
. I hope someone solves this because string paths are easier to deal with IMHO.
Other than that, I think JSPath is pretty fantastic and so far is working better for me than JSONPath so thank you for your efforts.