@atomist/rugs
Version:
Helper functions for Rugs
306 lines (267 loc) • 8.97 kB
text/typescript
/*
* 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.
*/
import { GraphNode, PathExpression } from "@atomist/rug/tree/PathExpression";
import { clone, isArray, isFunction, isPrimitive } from "../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)
*/
export function byExample<R extends GraphNode, L extends GraphNode>(g: any): PathExpression<R, L> {
const pathExpression = `/${queryByExampleString(g).path}`;
return new PathExpression<R, L>(pathExpression);
}
/**
* 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
*/
export function forRoot<R extends GraphNode>(g: any): PathExpression<R, R> {
return byExample<R, R>(g);
}
/**
* The path into a subgraph, along with whether it's to be treated as a match
* or as a predicate.
*/
class Branch {
// tslint:disable-next-line:no-shadowed-variable
constructor(public path: string, public match: boolean) { }
}
/**
* Internal state of query string generation
*/
class PathBuilderState {
private isMatch: boolean;
private simplePredicates = "";
private complexPredicates = "";
private rootExpression: string;
constructor(private root: any) {
this.isMatch = isMatch(root);
this.rootExpression = typeToAddress(root);
}
public addSimplePredicate(pred: string) {
this.simplePredicates += pred;
}
public addComplexPredicate(pred: string) {
this.complexPredicates += pred;
}
/**
* Mark this branch as a match branch, not a predicate?
*/
public markAsMatch() {
this.isMatch = true;
}
/**
* The branch built from the state we've built up.
* This is the ultimate objective.
*/
public branch() {
return new Branch(
this.rootExpression +
this.simplePredicates +
this.complexPredicates +
customPredicate(this.root),
this.isMatch);
}
}
/**
* If we're going down a branch that we need a match in,
* return the branch NOT as a predicate.
*/
function queryByExampleString(g: any): Branch {
const state = new PathBuilderState(g);
// tslint:disable-next-line:forin
for (const id in g) {
let value: any = 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: any, state: PathBuilderState, id: string, value) {
if (value == null) {
throw new Error("What to do with explicit null?");
} else if (value === root) {
const e = `Cycle detected processing property [${id}] returning ${JSON.stringify(value)} with state ${state}`;
throw new Error(e);
} else if (isArray(value)) {
handleArray(state, id, value);
} else if (isGraphNode(value)) {
handleGraphNode(state, id, value);
} else if (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: PathBuilderState, id: string, value) {
state.addSimplePredicate(`[@${id}='${value}']`);
}
function handleArray(state: PathBuilderState, id: string, values: any[]) {
values.forEach((v) => {
handleAny(values, state, id, v);
});
}
function handleGraphNode(state: PathBuilderState, id: string, value: GraphNode) {
const branch = queryByExampleString(value);
if (branch.match) {
state.markAsMatch();
}
const step = `/${id}::${branch.path}`;
state.addComplexPredicate(branch.match ? step : `[${step}]`);
}
function typeToAddress(g: any): string {
// TODO fragile. Or is this a convention we can rely on?
return 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: string): boolean {
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
*/
export function match(a) {
a.$match = true;
return a;
}
export function isMatch(a) {
return a.$match === true;
}
/**
* Interface mixed into enhanced objects.
*/
export interface Enhanced<T> {
/**
* Add a custom predicate string to this node
*/
withCustomPredicate(predicate: string): EnhancedReturn<T>;
/**
* Match either of these cases
* @param a function to add examples to an object of this type
* @param b function to add examples tp am pbkect of this type
*/
optional(what: (T) => void): EnhancedReturn<T>;
/**
* Specify that we should NOT match whatever state the specified function creates
* @param what what we should not do: Invoke "with" or "add" methods
*/
not(what: (T) => void): EnhancedReturn<T>;
or(a: (T) => void, b: (T) => void): EnhancedReturn<T>;
}
export type EnhancedReturn<T> = T & Enhanced<T>;
/*
Mixin functions to add to nodes to
allow building more powerful queries.
*/
function withCustomPredicate(predicate: string) {
if (!this.$predicate) {
this.$predicate = "";
}
this.$predicate += predicate;
return this;
}
export function customPredicate(a): string {
return a.$predicate ? a.$predicate : "";
}
/*
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<T>(what: (T) => void) {
const shallowCopy = clone(this);
what(shallowCopy);
const rawPredicate = dropLeadingType(byExample(shallowCopy).expression);
const optionalPredicate = rawPredicate + "?";
this.withCustomPredicate(optionalPredicate);
return this;
}
function not<T>(what: (T) => void) {
const shallowCopy = clone(this);
what(shallowCopy);
const rawPredicate = dropLeadingType(byExample(shallowCopy).expression);
const nottedPredicate = rawPredicate.replace("[", "[not ");
this.withCustomPredicate(nottedPredicate);
return this;
}
function or<T>(a: (T) => void, b: (T) => void) {
const aCopy = clone(this);
const bCopy = clone(this);
a(aCopy);
b(bCopy);
const aPredicate =
dropLeadingType(byExample(aCopy).expression);
const bPredicate =
dropLeadingType(byExample(bCopy).expression);
const 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: string): string {
return s.substring(s.indexOf("["));
}
/**
* Decorate a node with appropriate mixin functions
* to add power to query by example.
* @param a node to decorate
*/
export function enhance<T>(node): EnhancedReturn<T> {
// Manually mix in the methods from the Enhanced interface
const optKey = "optional";
node[optKey] = optional;
const withKey = "withCustomPredicate";
node[withKey] = withCustomPredicate;
const notKey = "not";
node[notKey] = not;
const orKey = "or";
node[orKey] = or;
return node;
}