@atomist/rugs
Version:
Helper functions for Rugs
259 lines (258 loc) • 8.15 kB
JavaScript
/*
* Copyright © 2017 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
Object.defineProperty(exports, "__esModule", { value: true });
var PathExpression_1 = require("@atomist/rug/tree/PathExpression");
var Utils_1 = require("../misc/Utils");
/**
* Create a query for this node graph, matching either the root or leaf nodes
* marked with the _match property. Works through navigating public functions
* or properties that return other GraphNodes, or simple values (for simple predicates).
* Doesn't insist on a GraphNode parameter as it could be a JSON structure with the required
* properties instead
* @type R type of root
* @type L type of leaf (may be the same)
*/
function byExample(g) {
var pathExpression = "/" + queryByExampleString(g).path;
return new PathExpression_1.PathExpression(pathExpression);
}
exports.byExample = byExample;
/**
* Query for the given root node. All other paths
* will be expressed as predicates.
* Should be passed to scala-style queries.
* @param g root node
*/
function forRoot(g) {
return byExample(g);
}
exports.forRoot = forRoot;
/**
* The path into a subgraph, along with whether it's to be treated as a match
* or as a predicate.
*/
var Branch = (function () {
// tslint:disable-next-line:no-shadowed-variable
function Branch(path, match) {
this.path = path;
this.match = match;
}
return Branch;
}());
/**
* Internal state of query string generation
*/
var PathBuilderState = (function () {
function PathBuilderState(root) {
this.root = root;
this.simplePredicates = "";
this.complexPredicates = "";
this.isMatch = isMatch(root);
this.rootExpression = typeToAddress(root);
}
PathBuilderState.prototype.addSimplePredicate = function (pred) {
this.simplePredicates += pred;
};
PathBuilderState.prototype.addComplexPredicate = function (pred) {
this.complexPredicates += pred;
};
/**
* Mark this branch as a match branch, not a predicate?
*/
PathBuilderState.prototype.markAsMatch = function () {
this.isMatch = true;
};
/**
* The branch built from the state we've built up.
* This is the ultimate objective.
*/
PathBuilderState.prototype.branch = function () {
return new Branch(this.rootExpression +
this.simplePredicates +
this.complexPredicates +
customPredicate(this.root), this.isMatch);
};
return PathBuilderState;
}());
/**
* If we're going down a branch that we need a match in,
* return the branch NOT as a predicate.
*/
function queryByExampleString(g) {
var state = new PathBuilderState(g);
// tslint:disable-next-line:forin
for (var id in g) {
var value = null;
if (isRelevantPropertyName(id)) {
try {
value = g[id];
}
catch (e) {
// Let value stay undefined
}
}
// Ignore undefined values
if (value) {
handleAny(g, state, id, value);
}
}
return state.branch();
}
function handleAny(root, state, id, value) {
if (value == null) {
throw new Error("What to do with explicit null?");
}
else if (value === root) {
var e = "Cycle detected processing property [" + id + "] returning " + JSON.stringify(value) + " with state " + state;
throw new Error(e);
}
else if (Utils_1.isArray(value)) {
handleArray(state, id, value);
}
else if (isGraphNode(value)) {
handleGraphNode(state, id, value);
}
else if (Utils_1.isPrimitive(value) !== -1) {
handlePrimitive(state, id, value);
}
else {
// console.log(`Don't know what to do with unfamiliar result of invoking [${id}] was [${value}]`);
}
}
function handlePrimitive(state, id, value) {
state.addSimplePredicate("[@" + id + "='" + value + "']");
}
function handleArray(state, id, values) {
values.forEach(function (v) {
handleAny(values, state, id, v);
});
}
function handleGraphNode(state, id, value) {
var branch = queryByExampleString(value);
if (branch.match) {
state.markAsMatch();
}
var step = "/" + id + "::" + branch.path;
state.addComplexPredicate(branch.match ? step : "[" + step + "]");
}
function typeToAddress(g) {
// TODO fragile. Or is this a convention we can rely on?
return Utils_1.isFunction(g.nodeTags) ? g.nodeTags()[0] + "()" : g.nodeTags[0] + "()";
}
function isGraphNode(obj) {
// Simple test for whether an object is a GraphNode
return obj.nodeTags && obj.nodeName;
}
/**
* Is this a property we care about? That is, it's not one of our well-known properties
* and isn't prefixed with _, our convention for holding our internal state
*/
function isRelevantPropertyName(id) {
return ["nodeTags", "nodeName"].indexOf(id) === -1 &&
id.indexOf("_") !== 0 &&
id.indexOf("$") !== 0;
}
/**
* Mark this object as a match that will be
* returned as a leaf (match node)
* @param a object to mark as a match
*/
function match(a) {
a.$match = true;
return a;
}
exports.match = match;
function isMatch(a) {
return a.$match === true;
}
exports.isMatch = isMatch;
/*
Mixin functions to add to nodes to
allow building more powerful queries.
*/
function withCustomPredicate(predicate) {
if (!this.$predicate) {
this.$predicate = "";
}
this.$predicate += predicate;
return this;
}
function customPredicate(a) {
return a.$predicate ? a.$predicate : "";
}
exports.customPredicate = customPredicate;
/*
Our strategy for all these mixed-in methods is the same:
Clone the existing object and run the user's function on it.
The function should create additional predicates.
Then manipulate the returned predicate as necesary.
*/
function optional(what) {
var shallowCopy = Utils_1.clone(this);
what(shallowCopy);
var rawPredicate = dropLeadingType(byExample(shallowCopy).expression);
var optionalPredicate = rawPredicate + "?";
this.withCustomPredicate(optionalPredicate);
return this;
}
function not(what) {
var shallowCopy = Utils_1.clone(this);
what(shallowCopy);
var rawPredicate = dropLeadingType(byExample(shallowCopy).expression);
var nottedPredicate = rawPredicate.replace("[", "[not ");
this.withCustomPredicate(nottedPredicate);
return this;
}
function or(a, b) {
var aCopy = Utils_1.clone(this);
var bCopy = Utils_1.clone(this);
a(aCopy);
b(bCopy);
var aPredicate = dropLeadingType(byExample(aCopy).expression);
var bPredicate = dropLeadingType(byExample(bCopy).expression);
var oredPredicate = aPredicate.replace("]", " or") +
bPredicate.replace("[", " ");
this.withCustomPredicate(oredPredicate);
return this;
}
/**
* Drop the leading type, e.g. Build() from a path expression such as
* Build()[@status='passed']
* Used to extract predicates.
* @param s path expression
*/
function dropLeadingType(s) {
return s.substring(s.indexOf("["));
}
/**
* Decorate a node with appropriate mixin functions
* to add power to query by example.
* @param a node to decorate
*/
function enhance(node) {
// Manually mix in the methods from the Enhanced interface
var optKey = "optional";
node[optKey] = optional;
var withKey = "withCustomPredicate";
node[withKey] = withCustomPredicate;
var notKey = "not";
node[notKey] = not;
var orKey = "or";
node[orKey] = or;
return node;
}
exports.enhance = enhance;