spahql
Version:
A query language and data model for deep Javascript object structures.
280 lines (248 loc) • 10.9 kB
JavaScript
/**
* class SpahQL.Token.PathComponent < SpahQL.Token.Base
*
* A token describing any path component within a selection query, comprised of one or two path delimiters
* followed by a key or property name and an optional set of filter query tokens.
**/
SpahQL_classExtend("SpahQL.Token.PathComponent", SpahQL.Token.Base, {
// Singleton
// ---------------------------
// Atom configuration: paths
ATOM_PATH_DELIMITER: "/",
ATOM_PROPERTY_IDENTIFIER: ".",
ATOM_PATH_WILDCARD: "*",
/**
* SpahQL.Token.PathComponent.parseAt(index, queryString) -> Array result or null
*
* Reads the given queryString starting at the given index and attempts to identify and parse
* a path component token. If found, the token will be returned in a tuple \[resumeIndex, foundToken\].
* Returns null if nothing is found.
**/
"parseAt": function(i, query) {
if(query.charAt(i) == this.ATOM_PATH_DELIMITER) {
var j = i+1;
var pc = new this();
var usingProperty = false;
// Grab second (recursive) slash
if(query.charAt(j) == this.ATOM_PATH_DELIMITER) {
pc.recursive = true;
j++;
}
// Check for wildcard, which halts the key reader and moves on to filters
if(query.charAt(j) == this.ATOM_PATH_WILDCARD) {
// Expect filter or end
pc.key = this.ATOM_PATH_WILDCARD;
j++
}
else {
// Get keyname / property name (until run out of alphanum/-/_)
if(query.charAt(j) == this.ATOM_PROPERTY_IDENTIFIER) {
usingProperty = true;
j++
}
else if(query.charAt(j) == this.ATOM_PATH_DELIMITER) {
this.throwParseErrorAt(j, query, "3 path delimiters found in a row. Maximum legal count is 2.");
}
// Read ahead for keyname, if not found then move on.
var kReadResult = SpahQL.Token.KeyName.parseAt(j, query);
if(!kReadResult && usingProperty) {
this.throwParseError(j, query, "Found unexpected character '"+query.charAt(j)+"' when expecting TOKEN_PROPERTY")
}
else if(kReadResult) {
if(usingProperty) pc.property = kReadResult[1];
else pc.key = kReadResult[1];
j = kReadResult[0];
}
}
// End keyname/propertyname segment
// Start filters
var fReadResult;
while(fReadResult = SpahQL.Token.FilterQuery.parseAt(j, query)) {
pc.filterQueries.push(fReadResult[1]);
j = fReadResult[0];
}
// When out of filters, exit
return [j, pc]
}
return null;
}
},{
// Instance
// ----------------------------------------
// Constants for known symbols
PROPERTY_TYPE: "type",
PROPERTY_SIZE: "size",
PROPERTY_EXPLODE: "explode",
PROPERTY_KEY: "key",
PROPERTY_PATH: "path",
/**
* SpahQL.Token.PathComponent#key -> String
*
* The key specified in this path component, if a keyname was used.
**/
/**
* SpahQL.Token.PathComponent#property -> String
*
* The property specified in this path component, if a property name was used.
**/
/**
* SpahQL.Token.PathComponent#recursive -> Boolean
*
* A flag indicating whether or not this path component should recurse through its
* scope data during evaluation.
**/
/**
* SpahQL.Token.PathComponent#filterQueries -> Array Token.FilterQuery
*
* Lists all filter queries associated with this path component, in the order in which they were
* encountered during parsing.
**/
/**
* new SpahQL.Token.PathComponent(key, property, recursive, filterQueries)
*
* Instantiate a path component token with blank-slate values
**/
"init": function(key, property, recursive, filterQueries) {
this.key = key || null;
this.property = property || null;
this.recursive = recursive || false;
this.filterQueries = filterQueries || [];
},
/**
* SpahQL.Token.PathComponent#evaluate(rootData, scopeData, path) -> Array
* - pathComponent (Object): A path component object as generated by the query parser
* - rootData (Object): The entire root-level data structure being queried
* - scopeData (Object): The data for the scope at which this query is being executed.
* - path (String): The string path for the root of the scopeData argument.
*
* Evaluates this path pomponent and returns a set of query results.
* Used primarily in Token.SelectionQuery#evaluate to map each path component to a set of results, allowing the query process to be
* effectively forked or halted.
**/
"evaluate": function(rootData, scopeData, path) {
var results;
var scopePath = (!path || path == "/")? "" : path; // Root path is blanked for easy appending
if(this.key == null && this.property == null) {
// Root query,
results = [SpahQL.result(path, scopeData, rootData)]; // Uses original path arg
}
else if(this.key != null) {
// Key query - key might be wildcard.
var keyName = this.key.value; // pull from token
results = this.fetchResultsFromObjectByKey(keyName, rootData, scopeData, scopePath, this.recursive);
}
else if(this.property != null) {
// Property query
var propertyName = this.property.value;
results = this.fetchResultsFromObjectByProperty(propertyName, rootData, scopeData, scopePath, this.recursive);
}
// Now filter results if there are filter queries
if(results.length > 0 && this.filterQueries.length > 0) {
var fI, rI;
// Loop filter queries
for(fI=0; fI<this.filterQueries.length; fI++) {
var filterQueryToken = this.filterQueries[fI];
var filteredResults = [];
// Loop results and assert filters against the result's data
for(rI = 0; rI < results.length; rI++) {
var r = results[rI];
var filterResult = filterQueryToken.evaluate(rootData, r.value);
if(filterResult && filteredResults.indexOf(r) < 0) {
filteredResults.push(r);
}
} // result loop
// Set results to those allowed by this filter query
results = filteredResults;
} // filter query loop
} // condition
// Return remainder
return results;
},
/**
* SpahQL.Token.PathComponent#fetchResultsFromObjectByKey(key, object, path, recursive) -> Array
* - key (String): The key to be retrieved from the object. Numeric keys in string formare acceptable when accessing arrays.]
* - rootData (Object): The root data structure being queried.
* - scopeData (Object): The data structure from which the key's associated value will be retrieved
* - path (String): The path at which the item used as the 'object' argument is located
* - recursive (Boolean): A flag indicating whether the key should also be pulled from any child objects of the given object. I N C E P T I O N.
*
* Retrieves the value(s) associated with a given key from the given object, if such a key exists.
**/
fetchResultsFromObjectByKey: function(key, rootData, scopeData, path, recursive) {
var oType = SpahQL.DataHelper.objectType(scopeData);
var results = [];
if(oType == "array" || oType == "object") {
// Loop and append
for(var oKey in scopeData) {
var oVal = scopeData[oKey];
var oValType = SpahQL.DataHelper.objectType(oVal);
var oPath = path+"/"+oKey;
// Match at this level
if(key == SpahQL.QueryParser.ATOM_PATH_WILDCARD || key.toString() == oKey.toString()) {
results.push(SpahQL.result(oPath, oVal, rootData));
}
// Recurse! That is, if we should. Or not. It's cool.
if(recursive && (oValType == "array" || oValType == "object")) {
results = results.concat(this.fetchResultsFromObjectByKey(key, rootData, oVal, oPath, recursive));
}
}
}
return results;
},
/**
* SpahQL.Token.PathComponent#fetchResultsFromObjectByProperty(key, object, path, recursive) -> Array
* - key (String): The key to be retrieved from the object. Numeric keys in string formare acceptable when accessing arrays.
* - rootData (Object): The root data structure being queried.
* - scopeData (Object): The data structure from which the key's associated value will be retrieved
* - path (String): The path at which the item used as the 'object' argument is located
* - recursive (Boolean): A flag indicating whether the key should also be pulled from any child objects of the given object. I N C E P T I O N.
*
* Retrieves the specified Spah object property from the given object, if the object supports the specified property.
**/
fetchResultsFromObjectByProperty: function(property, rootData, scopeData, path, recursive) {
var oType = SpahQL.DataHelper.objectType(scopeData);
var pPath = path+"/."+property;
var results = [];
switch(property) {
case this.PROPERTY_SIZE:
switch(oType) {
case "array": case "string":
results.push(SpahQL.result(pPath, scopeData.length, rootData));
break;
case "object":
results.push(SpahQL.result(pPath, SpahQL.DataHelper.hashKeys(scopeData).length, rootData));
break;
}
break;
case this.PROPERTY_TYPE:
results.push(SpahQL.result(pPath, oType, rootData));
break;
case this.PROPERTY_EXPLODE:
if(oType =="string") {
for(var c=0; c<scopeData.length; c++) {
results.push(SpahQL.result(path+"/"+c, scopeData.charAt(c), rootData));
}
}
break;
case this.PROPERTY_PATH:
results.push(SpahQL.result(pPath, (path||"/"), rootData));
break;
case this.PROPERTY_KEY:
var pKey = (!path||path==""||path=="/")? path : path.substring(path.lastIndexOf("/")+1);
results.push(SpahQL.result(pPath, pKey, rootData));
break;
default:
throw new SpahQL.Errors.SpahQLRunTimeError("Unrecognised property token '"+property+"'.");
break;
}
// recurse if needed
if(recursive && (oType == "array" || oType == "object")) {
for(var k in scopeData) {
var kPath = path+"/"+k;
var kVal = scopeData[k];
results = results.concat(this.fetchResultsFromObjectByProperty(property, rootData, kVal, kPath, recursive));
}
}
return results;
}
});