Kaynağa Gözat

DEVOPS-247 - Added FieldPathExpression, all tests now pass.

Brennan Chesley 12 yıl önce
ebeveyn
işleme
d160a1348d

+ 125 - 11
lib/pipeline/expressions/FieldPathExpression.js

@@ -9,19 +9,88 @@
  * @constructor
  * @constructor
  * @param {String} fieldPath the field path string, without any leading document indicator
  * @param {String} fieldPath the field path string, without any leading document indicator
  **/
  **/
-var FieldPathExpression = module.exports = function FieldPathExpression(path){
-	if (arguments.length !== 1) throw new Error("args expected: path");
-	this.path = new FieldPath(path);
+
+var Expression = require("./Expression"),
+    Variables = require("./Variables"),
+    Value = require("../Value"),
+    FieldPath = require("../FieldPath");
+
+
+var FieldPathExpression = module.exports = function FieldPathExpression(path, variableId){
+    if (arguments.length > 2) throw new Error("args expected: path[, vps]");
+    this.path = new FieldPath(path);
+    if(arguments.length == 2) {
+        this.variable = variableId;
+    } else {
+        this.variable = Variables.ROOT_ID;
+    }
 }, klass = FieldPathExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = FieldPathExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// DEPENDENCIES
-var FieldPath = require("../FieldPath");
+klass.create = function create(path) {
+    return new FieldPathExpression("CURRENT."+path, Variables.ROOT_ID);
+};
+
 
 
 // PROTOTYPE MEMBERS
 // PROTOTYPE MEMBERS
-proto.evaluate = function evaluate(obj){
-	return this._evaluatePath(obj, 0, this.path.fields.length);
+proto.evaluateInternal = function evaluateInternal(vars){
+
+    if(this.path.fields.length === 1) {
+        return vars.getValue(this.variable);
+    }
+
+    if(this.variable === Variables.ROOT_ID) {
+        return this.evaluatePath(1, vars.getRoot());
+    }
+
+    var vari = vars.getValue(this.variable);
+    if(vari instanceof Array) {
+        return this.evaluatePathArray(1,vari);
+    } else if (vari instanceof Object) {
+        return this.evaluatePath(1, vari);
+    } else {
+        return undefined;
+    }
+};
+
+
+/**
+ * Parses a fieldpath using the mongo 2.5 spec with optional variables
+ *
+ * @param raw raw string fieldpath
+ * @param vps variablesParseState
+ * @returns a new FieldPathExpression
+ **/
+proto.parse = function parse(raw, vps) {
+    if(raw[0] === "$") {
+        throw new Error("FieldPath: '" + raw + "' doesn't start with a $");
+    }
+    if(raw.length === 1) {
+        throw new Error("'$' by itself is not a valid FieldPath");
+    }
+
+    if(raw[1] === "$") {
+        var firstPeriod = raw.indexOf('.');
+        var varname = (firstPeriod === -1 ? raw.slice(2) : raw.slice(2,firstPeriod));
+        Variables.uassertValidNameForUserRead(varname);
+        return new FieldPathExpression(raw.slice(2), vps.getVariableName(varname));
+    } else {
+        return new FieldPathExpression("CURRENT." + raw.slice(1), vps.getVariable("CURRENT"));
+    }
+};
+
+
+/**
+ * Parses a fieldpath using the mongo 2.5 spec with optional variables
+ *
+ * @param raw raw string fieldpath
+ * @param vps variablesParseState
+ * @returns a new FieldPathExpression
+ **/
+proto.optimize = function optimize() {
+    return this;
 };
 };
 
 
+
 /**
 /**
  * Internal implementation of evaluate(), used recursively.
  * Internal implementation of evaluate(), used recursively.
  *
  *
@@ -69,13 +138,54 @@ proto._evaluatePath = function _evaluatePath(obj, i, len){
 	return undefined;
 	return undefined;
 };
 };
 
 
+proto.evaluatePathArray = function evaluatePathArray(index, input) {
+
+    if(!(input instanceof Array)) {
+        throw new Error("evaluatePathArray called on non-array");
+    }
+    var result = [];
+
+    for(var ii = 0; ii < input.length; ii++) {
+        if(input[ii] instanceof Object) {
+            var nested = this.evaluatePath(index, input[ii]);
+            if(nested) {
+                result.push(nested);
+            }
+        }
+    }
+    return result;
+};
+
+
+proto.evaluatePath = function(index, input) {
+    if(index === this.path.fields.length -1) {
+        return input[this.path.fields[index]];
+    }
+    var val = input[this.path.fields[index]];
+    if(val instanceof Array) {
+        return this.evaluatePathArray(index+1, val);
+    } else if (val instanceof Object) {
+        return this.evaluatePath(index+1, val);
+    } else {
+        return undefined;
+    }
+
+};
+
+
+
 proto.optimize = function(){
 proto.optimize = function(){
 	return this;
 	return this;
 };
 };
 
 
 proto.addDependencies = function addDependencies(deps){
 proto.addDependencies = function addDependencies(deps){
-	deps[this.path.getPath()] = 1;
-	return deps;
+	if(this.path.fields[0] === "CURRENT" || this.path.fields[0] === "ROOT") {
+            if(this.path.fields.length === 1) {
+                deps[""] = 1;
+            } else {
+                deps[this.path.tail().getPath(false)] = 1;
+            }
+        }
 };
 };
 
 
 // renamed write to get because there are no streams
 // renamed write to get because there are no streams
@@ -83,8 +193,12 @@ proto.getFieldPath = function getFieldPath(usePrefix){
 	return this.path.getPath(usePrefix);
 	return this.path.getPath(usePrefix);
 };
 };
 
 
-proto.toJSON = function toJSON(){
-	return this.path.getPath(true);
+proto.serialize = function toJSON(){
+    if(this.path.fields[0] === "CURRENT" && this.path.fields.length > 1) {
+        return "$" + this.path.tail().getPath(false);
+    } else {
+        return "$$" + this.path.getPath(false);
+    }
 };
 };
 
 
 //TODO: proto.addToBsonObj = ...?
 //TODO: proto.addToBsonObj = ...?

+ 10 - 0
lib/pipeline/expressions/NaryExpression.js

@@ -112,3 +112,13 @@ proto.serialize = function serialize() {
     ret[this.getOpName()] = subret;
     ret[this.getOpName()] = subret;
     return ret;
     return ret;
 };
 };
+
+proto.fixedArity = function(nargs) {
+    this.nargs = nargs;
+};
+
+proto.validateArguments = function(args) {
+    if(this.nargs !== args.length) {
+        throw new Error("Expression " + this.getOpName() + " takes exactly " + this.nargs + " arguments. " + args.length + " were passed in.");
+    }
+};

+ 10 - 8
lib/pipeline/expressions/Variables.js

@@ -1,22 +1,24 @@
 "use strict";
 "use strict";
 
 
-/** 
+/**
  * Class that stores/tracks variables
  * Class that stores/tracks variables
  * @class Variables
  * @class Variables
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
  **/
  **/
-var Variables = module.exports = function Variables(numVars){
+var Variables = module.exports = function Variables(numVars, root){
 	if(numVars) {
 	if(numVars) {
 		if(typeof numVars !== 'number') {
 		if(typeof numVars !== 'number') {
 			throw new Error('numVars must be a number');
 			throw new Error('numVars must be a number');
 		}
 		}
 	}
 	}
-	this._root = {};
+	this._root = root || {};
 	this._rest = numVars ? [] : undefined; //An array of `Value`s
 	this._rest = numVars ? [] : undefined; //An array of `Value`s
 	this._numVars = numVars;
 	this._numVars = numVars;
-}, klass = Variables, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = Variables,
+    base = Object,
+    proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
 
 
 klass.ROOT_ID = -1;
 klass.ROOT_ID = -1;
@@ -65,8 +67,8 @@ proto.setValue = function setValue(id, value) {
 	}
 	}
 
 
 	if(id === klass.ROOT_ID) {
 	if(id === klass.ROOT_ID) {
-		throw new Error("mError 17199: can't use Variables#setValue to set ROOT");	
-	}	
+		throw new Error("mError 17199: can't use Variables#setValue to set ROOT");
+	}
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 		throw new Error("You have more variables than _numVars");
 		throw new Error("You have more variables than _numVars");
 	}
 	}
@@ -88,7 +90,7 @@ proto.getValue = function getValue(id) {
 
 
 	if(id === klass.ROOT_ID) {
 	if(id === klass.ROOT_ID) {
 		return this._root;
 		return this._root;
-	}	
+	}
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 		throw new Error("Cannot get value; id was greater than _numVars");
 		throw new Error("Cannot get value; id was greater than _numVars");
 	}
 	}
@@ -111,7 +113,7 @@ proto.getDocument = function getDocument(id) {
 
 
 	if(id === klass.ROOT_ID) {
 	if(id === klass.ROOT_ID) {
 		return this._root;
 		return this._root;
-	}	
+	}
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
 		throw new Error("Cannot get value; id was greater than _numVars");
 		throw new Error("Cannot get value; id was greater than _numVars");
 	}
 	}

+ 54 - 24
test/lib/pipeline/expressions/FieldPathExpression.js

@@ -1,6 +1,7 @@
 "use strict";
 "use strict";
 var assert = require("assert"),
 var assert = require("assert"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression");
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+    Variables = require("../../../../lib/pipeline/expressions/Variables");
 
 
 
 
 module.exports = {
 module.exports = {
@@ -20,79 +21,108 @@ module.exports = {
 		"#evaluate()": {
 		"#evaluate()": {
 
 
 			"should return undefined if field path is missing": function testMissing(){
 			"should return undefined if field path is missing": function testMissing(){
-				assert.strictEqual(new FieldPathExpression('a').evaluate({}), undefined);
+				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1, {})), undefined);
 			},
 			},
 
 
 			"should return value if field path is present": function testPresent(){
 			"should return value if field path is present": function testPresent(){
-				assert.strictEqual(new FieldPathExpression('a').evaluate({a:123}), 123);
+                            var vars = new Variables(1, {a:123}),
+                                fieldPath = FieldPathExpression.create('a'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.strictEqual(results, 123);
 			},
 			},
 
 
 			"should return undefined if field path is nested below null": function testNestedBelowNull(){
 			"should return undefined if field path is nested below null": function testNestedBelowNull(){
-				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:null}), undefined);
+                            var vars = new Variables(1,{a:null}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+				assert.strictEqual(results, undefined);
 			},
 			},
 
 
 			"should return undefined if field path is nested below undefined": function NestedBelowUndefined(){
 			"should return undefined if field path is nested below undefined": function NestedBelowUndefined(){
-				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:undefined}), undefined);
+                            var vars = new Variables(1,{a:undefined}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+				assert.strictEqual(results, undefined);
 			},
 			},
 
 
 			"should return undefined if field path is nested below Number": function testNestedBelowInt(){
 			"should return undefined if field path is nested below Number": function testNestedBelowInt(){
-				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:2}), undefined);
+                            var vars = new Variables(1,{a:2}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.strictEqual(results, undefined);
 			},
 			},
 
 
 			"should return value if field path is nested": function testNestedValue(){
 			"should return value if field path is nested": function testNestedValue(){
-				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:{b:55}}), 55);
+                            var vars = new Variables(1,{a:{b:55}}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.strictEqual(results, 55);
 			},
 			},
 
 
 			"should return undefined if field path is nested below empty Object": function testNestedBelowEmptyObject(){
 			"should return undefined if field path is nested below empty Object": function testNestedBelowEmptyObject(){
-				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:{}}), undefined);
+                            var vars = new Variables(1,{a:{}}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.strictEqual(results, undefined);
 			},
 			},
 
 
 			"should return empty Array if field path is nested below empty Array": function testNestedBelowEmptyArray(){
 			"should return empty Array if field path is nested below empty Array": function testNestedBelowEmptyArray(){
-				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[]}), []);
+                            var vars = new Variables(1,{a:[]}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.deepEqual(results, []);
 			},
 			},
 
 
 			"should return Array with null if field path is nested below Array containing null": function testNestedBelowArrayWithNull(){
 			"should return Array with null if field path is nested below Array containing null": function testNestedBelowArrayWithNull(){
-				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[null]}), [null]);
+                            debugger;
+                            var vars = new Variables(1,{a:[null]}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.deepEqual(results, []);
 			},
 			},
 
 
 			"should return Array with undefined if field path is nested below Array containing undefined": function testNestedBelowArrayWithUndefined(){
 			"should return Array with undefined if field path is nested below Array containing undefined": function testNestedBelowArrayWithUndefined(){
-				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[undefined]}), [undefined]);
+                            var vars = new Variables(1,{a:[undefined]}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.deepEqual(results, []);
 			},
 			},
 
 
 			"should throw Error if field path is nested below Array containing a Number": function testNestedBelowArrayWithInt(){
 			"should throw Error if field path is nested below Array containing a Number": function testNestedBelowArrayWithInt(){
-				assert.throws(function(){
-					new FieldPathExpression('a.b').evaluate({a:[1]});
-				});
+                            var vars = new Variables(1,{a:[9]}),
+                                fieldPath = FieldPathExpression.create('a.b'),
+                                results = fieldPath.evaluateInternal(vars);
+                            assert.deepEqual(results, []);
 			},
 			},
 
 
 			"should return Array with value if field path is in Object within Array": function testNestedWithinArray(){
 			"should return Array with value if field path is in Object within Array": function testNestedWithinArray(){
-				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[{b:9}]}), [9]);
+				assert.deepEqual(FieldPathExpression.create('a.b').evaluateInternal(new Variables(1,{a:[{b:9}]})), [9]);
 			},
 			},
 
 
 			"should return Array with multiple value types if field path is within Array with multiple value types": function testMultipleArrayValues(){
 			"should return Array with multiple value types if field path is within Array with multiple value types": function testMultipleArrayValues(){
 				var path = 'a.b',
 				var path = 'a.b',
 					doc = {a:[{b:9},null,undefined,{g:4},{b:20},{}]},
 					doc = {a:[{b:9},null,undefined,{g:4},{b:20},{}]},
-					expected = [9,null,undefined,undefined,20,undefined];
-				assert.deepEqual(new FieldPathExpression(path).evaluate(doc), expected);
+					expected = [9,20];
+				assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
 			},
 			},
 
 
 			"should return Array with expanded values from nested multiple nested Arrays": function testExpandNestedArrays(){
 			"should return Array with expanded values from nested multiple nested Arrays": function testExpandNestedArrays(){
 				var path = 'a.b.c',
 				var path = 'a.b.c',
 					doc = {a:[{b:[{c:1},{c:2}]},{b:{c:3}},{b:[{c:4}]},{b:[{c:[5]}]},{b:{c:[6,7]}}]},
 					doc = {a:[{b:[{c:1},{c:2}]},{b:{c:3}},{b:[{c:4}]},{b:[{c:[5]}]},{b:{c:[6,7]}}]},
 					expected = [[1,2],3,[4],[[5]],[6,7]];
 					expected = [[1,2],3,[4],[[5]],[6,7]];
-				assert.deepEqual(new FieldPathExpression(path).evaluate(doc), expected);
+				assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
 			},
 			},
 
 
 			"should return null if field path points to a null value": function testPresentNull(){
 			"should return null if field path points to a null value": function testPresentNull(){
-				assert.strictEqual(new FieldPathExpression('a').evaluate({a:null}), null);
+				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:null})), null);
 			},
 			},
 
 
 			"should return undefined if field path points to a undefined value": function testPresentUndefined(){
 			"should return undefined if field path points to a undefined value": function testPresentUndefined(){
-				assert.strictEqual(new FieldPathExpression('a').evaluate({a:undefined}), undefined);
+				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:undefined})), undefined);
 			},
 			},
 
 
 			"should return Number if field path points to a Number value": function testPresentNumber(){
 			"should return Number if field path points to a Number value": function testPresentNumber(){
-				assert.strictEqual(new FieldPathExpression('a').evaluate({a:42}), 42);
+				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:42})), 42);
 			}
 			}
 
 
 		},
 		},
@@ -100,7 +130,7 @@ module.exports = {
 		"#optimize()": {
 		"#optimize()": {
 
 
 			"should not optimize anything": function testOptimize(){
 			"should not optimize anything": function testOptimize(){
-				var expr = new FieldPathExpression('a');
+				var expr = FieldPathExpression.create('a');
 				assert.strictEqual(expr, expr.optimize());
 				assert.strictEqual(expr, expr.optimize());
 			}
 			}
 
 
@@ -110,7 +140,7 @@ module.exports = {
 
 
 			"should return the field path itself as a dependency": function testDependencies(){
 			"should return the field path itself as a dependency": function testDependencies(){
 				var deps = {};
 				var deps = {};
-				var fpe = new FieldPathExpression('a.b');
+				var fpe = FieldPathExpression.create('a.b');
 				fpe.addDependencies(deps);
 				fpe.addDependencies(deps);
 				assert.strictEqual(Object.keys(deps).length, 1);
 				assert.strictEqual(Object.keys(deps).length, 1);
 				assert.ok(deps['a.b']);
 				assert.ok(deps['a.b']);
@@ -121,7 +151,7 @@ module.exports = {
 		"#toJSON()": {
 		"#toJSON()": {
 
 
 			"should output path String with a '$'-prefix": function testJson(){
 			"should output path String with a '$'-prefix": function testJson(){
-				assert.equal(new FieldPathExpression('a.b.c').toJSON(), "$a.b.c");
+				assert.equal(FieldPathExpression.create('a.b.c').serialize(), "$a.b.c");
 			}
 			}
 
 
 		}
 		}