jspath icon indicating copy to clipboard operation
jspath copied to clipboard

Actual “find” method

Open kizu opened this issue 11 years ago • 11 comments

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.

kizu avatar Mar 05 '13 23:03 kizu

I will think about it )

dfilatov avatar Mar 06 '13 18:03 dfilatov

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.

ds82 avatar May 22 '13 08:05 ds82

jspath returns reference to original object, but this object has no context with it parent/siblings.

dfilatov avatar May 25 '13 19:05 dfilatov

+1 for 'find'

Djemo avatar Jun 13 '13 12:06 Djemo

+1

mb21 avatar Jul 21 '14 08:07 mb21

+1, that would be nice to have

AlexFess avatar Aug 12 '14 09:08 AlexFess

+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.

xogeny avatar Oct 24 '14 01:10 xogeny

@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

formula1 avatar Nov 19 '14 11:11 formula1

+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.

bitcloud avatar Feb 16 '15 15:02 bitcloud

+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.

zmorris avatar Apr 26 '18 17:04 zmorris

@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.

zmorris avatar Apr 26 '18 22:04 zmorris