UNPKG

@atomist/rugs

Version:

Helper functions for Rugs

259 lines (258 loc) 8.15 kB
/* * 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. */ "use strict"; 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;