Browse Source

Fixes #1046, #996, #1009: Finished porting `ObjectExpression` and related test cases. Misc documentation updates.

http://source.rd.rcg.local/trac/eagle6/changeset/1348/Eagle6_SVN
Kyle Davis 12 years ago
parent
commit
9e3e807e7f

+ 22 - 9
README.md

@@ -2,33 +2,42 @@ munge
 =====
 A JavaScript data munging pipeline based on the MongoDB aggregation framework.
 
+In general, this code is a port from the MongoDB C++ code (v2.4.0) to JavaScript.
+
 
 
 exports
-=======
+-------
 **TODO:** document the major exports and a little about each here
 
 
 
 Deviations
-===========
-Here is a list of the major items where I have deviated from the MongoDB code and why:
-
-  * Pipeline Expressions
-    * Value class
-      * DESIGN: `Value` now provides static helpers rather than instance helpers since that seems to make more sense here
+----------
+Here is a list of the major items where we have deviated from the MongoDB code and a little bit about why:
+
+  * Pipeline classes
+    * the `Document` class
+      * DESIGN: `Document` now provides static helpers rather than instance helpers to avoid unecessary boxing/unboxing since that seems to make more sense here (we treat any `Object` like a `Document`)
+    * the `Expression` class
+      * DESIGN: the nested `ObjectCtx` class no longer uses contants and a bit flags but instead uses similarly named boolean; e.g., `isDocumentOk` rather than `DOCUMENT_OK`
+    * the `Value` class
+      * DESIGN: `Value` now provides static helpers rather than instance helpers to avoid unecessary boxing/unboxing since that seems to make more sense here (we treat any `Object` like a `Value)
       * NAMING: `Value#get{TYPE}` methods have been renamed to `Value.verify{TYPE}` since that seemed to make more sense given what they're really doing for us as statics
       * DESIGN: `Value.coerceToDate` static returns a JavaScript `Date` object rather than milliseconds since that seems to make more sense where possible
     * NAMING: The `Expression{FOO}` classes have all been renamed to `{FOO}Expression` to satisfy my naming OCD.
     * DESIGN: The `{FOO}Expression` classes do not provide `create` statics since calling new is easy enough
       * DESIGN: To further this, the `CompareExpression` class doesn't provide any of it's additional `create{FOO}` helpers so instead I'm binding the appropriate args to the ctor
-    * TESTING: Most of the expression tests have been written without the expression test base classes
+    * TESTING: Many of the expression tests have been written without the use of the testing base classes to make things a little more clear (but not less complete)
+  * BSON vs JSON
+    * DESIGN: Basically all of the `BSON`-specific code has become equivalent `JSON`-specific code since that's what we're working with (no need for needless conversions)
+    * DESIGN: A lot of these have a `addToBson...` and other `BSONObjBuilder`-related methods that take in an instance to be modified but it's owned by the caller; in `munge` we build a new `Object` and return it because it's simpler and that's how they're generally used anyhow
   * Document sources
   	* we have implemented a 'reset' method for all document sources so that we can reuse them against different streams of data
 
 
 TODO
-====
+----
 Here is a list of global items that I know about that may need to be done in the future:
 
   * Go through the TODOs....
@@ -40,3 +49,7 @@ Here is a list of global items that I know about that may need to be done in the
   * Go through test cases and try to turn `assert.equal()` calls into `assert.strictEqual()` calls
   * Replace `exports = module.exports =` with `module.exports =` only
   * Go through uses of `throw` and make them actually use `UserException` vs `SystemException` (or whatever they're called)
+  * Go through and fix the `/** documentation **/` to ensure that they are YUIDoc-fiendly and exist on multiple lines
+  * Go through and modify classes to use advanced OO property settings properly (`seal`, `freeze`, etc.) where appropriate
+  * Make sure that nobody is using private (underscored) variables that they shouldn't be...might have broken encapsulation somewhere along the way...
+  * Make sure  that all of the pure `virtual`s (i.e., /virtual .* = 0;$/) are implemented as a proto with a throw new Error("NOT IMPLEMENTED BY INHERITOR") or similar

+ 59 - 2
lib/pipeline/Document.js

@@ -1,13 +1,70 @@
 var Document = module.exports = (function(){
 	// CONSTRUCTOR
+	/**
+	* Represents a `Document` (i.e., an `Object`) in `mongo` but in `munge` this is only a set of static helpers since we treat all `Object`s like `Document`s.
+	*
+	* @class Document
+	* @namespace munge.pipeline
+	* @module munge
+	* @constructor
+	**/
 	var klass = function Document(){
 		if(this.constructor == Document) throw new Error("Never create instances! Use static helpers only.");
 	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
+	// DEPENDENCIES
+	var Value = require("./Value");
+
 	// STATIC MEMBERS
-	klass.compare = function compare(l, r){
-throw new Error("NOT IMPLEMENTED");
+	/**
+	* Shared "_id"
+	*
+	* @static
+	* @property ID_PROPERTY_NAME
+	**/
+	klass.ID_PROPERTY_NAME = "_id";
+
+	/**
+	* Compare two documents.
+	* BSON document field order is significant, so this just goes through the fields in order.
+	* The comparison is done in roughly the same way as strings are compared, but comparing one field at a time instead of one character at a time.
+	*
+	* @static
+	* @method compare
+	* @param rL left document
+	* @param rR right document
+	* @returns an integer less than zero, zero, or an integer greater than zero, depending on whether rL < rR, rL == rR, or rL > rR
+	**/
+	klass.compare = function compare(l, r){	//TODO: might be able to replace this with a straight compare of docs using JSON.stringify()
+		var lPropNames = Object.getOwnPropertyNames(l),
+			lPropNamesLength = lPropNames.length,
+			rPropNames = Object.getOwnPropertyNames(r),
+			rPropNamesLength = rPropNames.length;
+
+		for(var i = 0; true; ++i) {
+			if (i >= lPropNamesLength) {
+				if (i >= rPropNamesLength) return 0; // documents are the same length
+				return -1; // left document is shorter
+			}
+
+			if (i >= rPropNamesLength) return 1; // right document is shorter
+
+			var nameCmp = Value.compare(lPropNames[i], rPropNames[i]);
+			if (nameCmp !== 0) return nameCmp; // field names are unequal
+
+
+			var valueCmp = Value.compare(l[lPropNames[i]], r[rPropNames[i]]);
+			if (valueCmp) return valueCmp; // fields are unequal
+		}
+
+		/* NOTREACHED */
+		throw new Error("This should never happen");	//verify(false)
+//		return 0;
 	};
 
+//	proto.addField = function addField(){ throw new Error("Instead of `Document#addField(key,val)` you should just use `obj[key] = val`"); }
+//	proto.setField = function addField(){ throw new Error("Instead of `Document#setField(key,val)` you should just use `obj[key] = val`"); }
+//  proto.getField = function getField(){ throw new Error("Instead of `Document#getField(key)` you should just use `var val = obj[key];`"); }
+
 	return klass;
 })();

+ 13 - 2
lib/pipeline/FieldPath.js

@@ -6,11 +6,15 @@ var FieldPath = module.exports = FieldPath = (function(){
 	* The constructed object will have getPathLength() > 0.
 	* Uassert if any component field names do not pass validation.
 	*
+	* @class FieldPath
+	* @namespace munge.pipeline
+	* @module munge
+	* @constructor
 	* @param fieldPath the dotted field path string or non empty pre-split vector.
 	**/
 	var klass = function FieldPath(path){
 		var fields = typeof(path) === "object" && typeof(path.length) === "number" ? path : path.split(".");
-		if(fields.length === 0) throw new Error("FieldPath cannot be constructed from an empty Strings or Arrays.; code 16409");
+		if(fields.length === 0) throw new Error("FieldPath cannot be constructed from an empty vector (String or Array).; code 16409");
 		for(var i = 0, n = fields.length; i < n; ++i){
 			var field = fields[i];
 			if(field.length === 0) throw new Error("FieldPath field names may not be empty strings; code 15998");
@@ -29,6 +33,7 @@ var FieldPath = module.exports = FieldPath = (function(){
 	/**
 	* Get the full path.
 	*
+	* @method getPath
 	* @param fieldPrefix whether or not to include the field prefix
 	* @returns the complete field path
 	*/
@@ -36,7 +41,11 @@ var FieldPath = module.exports = FieldPath = (function(){
 		return ( !! withPrefix ? FieldPath.PREFIX : "") + this.fields.join(".");
 	};
 
-	/** A FieldPath like this but missing the first element (useful for recursion). Precondition getPathLength() > 1. **/
+	/**
+	* A FieldPath like this but missing the first element (useful for recursion). Precondition getPathLength() > 1.
+	*
+	* @method tail
+	**/
 	proto.tail = function tail() {
 		return new FieldPath(this.fields.slice(1));
 	};
@@ -44,6 +53,7 @@ var FieldPath = module.exports = FieldPath = (function(){
 	/**
 	* Get a particular path element from the path.
 	*
+	* @method getFieldName
 	* @param i the zero based index of the path element.
 	* @returns the path element
 	*/
@@ -54,6 +64,7 @@ var FieldPath = module.exports = FieldPath = (function(){
 	/**
 	* Get the number of path elements in the field path.
 	*
+	* @method getPathLength
 	* @returns the number of path elements
 	**/
 	proto.getPathLength = function getPathLength() {

+ 20 - 20
lib/pipeline/Value.js

@@ -1,6 +1,14 @@
 var Value = module.exports = Value = (function(){
 
 	// CONSTRUCTOR
+	/**
+	* Represents a `Value` (i.e., an `Object`) in `mongo` but in `munge` this is only a set of static helpers since we treat all `Object`s like `Value`s.
+	*
+	* @class Value
+	* @namespace munge.pipeline
+	* @module munge
+	* @constructor
+	**/
 	var klass = function Value(){
 		if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
 	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
@@ -17,7 +25,7 @@ var Value = module.exports = Value = (function(){
 	// STATIC MEMBERS
 	klass.verifyNumber = getTypeVerifier("number", Number);	//NOTE: replaces #getDouble(), #getInt(), and #getLong()
 	klass.verifyString = getTypeVerifier("string", String);
-	klass.verifyDocument = getTypeVerifier("object", Object, true);	//TODO: change to verifyDocument?
+	klass.verifyDocument = getTypeVerifier("object", Object, true);	//TODO: change to verifyObject? since we're not using actual Document instances
 	klass.verifyArray = getTypeVerifier("object", Array, true);
 	klass.verifyDate = getTypeVerifier("object", Date, true);
 	klass.verifyRegExp = getTypeVerifier("object", RegExp, true);	//NOTE: renamed from #getRegex()
@@ -46,7 +54,7 @@ var Value = module.exports = Value = (function(){
 	klass.coerceToDate = function coerceToDate(value) {
 		//TODO: Support Timestamp BSON type?
 		if (value instanceof Date) return value;
-		throw new Error("can't convert from BSON type " + typeof(value) + " to Date; codes 16006");
+		throw new Error("can't convert from BSON type " + typeof(value) + " to Date; uassert code 16006");
 	};
 //TODO: klass.coerceToTimeT = ...?   try to use as Date first rather than having coerceToDate return Date.parse  or dateObj.getTime() or similar
 //TODO:	klass.coerceToTm = ...?
@@ -59,29 +67,21 @@ var Value = module.exports = Value = (function(){
 			return value.toString();
 		case "string":
 			return value;
-		case "object":
-			if (value instanceof Array) {
-				Value.verifyArray(r);
-				for (var i = 0, ll = l.length, rl = r.length, len = Math.min(ll, rl); i < len; i++) {
-					if (i >= ll) {
-						if (i >= rl) return 0; // arrays are same length
-						return -1; // left array is shorter
-					}
-					if (i >= rl) return 1; // right array is shorter
-					var cmp = Value.compare(l[i], r[i]);
-					if (cmp !== 0) return cmp;
-				}
-				throw new Error("logic error in Value.compare for Array types!");
-			} else if (value instanceof Date) { //TODO: Timestamp ??
-				return value.toISOString();
-			}
-			/* falls through */
 		default:
-			throw new Error("can't convert from BSON type " + typeof(value) + " to String; code 16007");
+			throw new Error("can't convert from BSON type " + typeof(value) + " to String; uassert code 16007");
 		}
 	};
 //TODO:	klass.coerceToTimestamp = ...?
 
+	/**
+	* Compare two Values.
+	*
+	* @static
+	* @method compare
+	* @param rL left value
+	* @param rR right value
+	* @returns an integer less than zero, zero, or an integer greater than zero, depending on whether rL < rR, rL == rR, or rL > rR
+	**/
 	klass.compare = function compare(l, r) {
 		var lt = typeof(l),
 			rt = typeof(r);

+ 62 - 21
lib/pipeline/expressions/Expression.js

@@ -1,18 +1,35 @@
 var Expression = module.exports = (function(){
 	// CONSTRUCTOR
-	/** A base class for all pipeline expressions; Performs common expressions within an Op.
-	| NOTE:
-	| An object expression can take any of the following forms:
-	|	f0: {f1: ..., f2: ..., f3: ...}
-	|	f0: {$operator:[operand1, operand2, ...]}
+	/**
+	* A base class for all pipeline expressions; Performs common expressions within an Op.
+	*
+	* NOTE: An object expression can take any of the following forms:
+	*
+	*	f0: {f1: ..., f2: ..., f3: ...}
+	*	f0: {$operator:[operand1, operand2, ...]}
+	*
+	* @class Expression
+	* @namespace munge.pipeline.expressions
+	* @module munge
+	* @constructor
 	**/
 	var klass = module.exports = Expression = function Expression(opts){
 		if(arguments.length !== 0) throw new Error("zero args expected");
 	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
+	// DEPENDENCIES
+	var Document = require("../Document");
 
 	// NESTED CLASSES
-	/** Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context. **/
+	/**
+	* Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context.
+	*
+	* NOTE: deviation from Mongo code: accepts an object of settings rather than a bitmask to help cleanup the interface a little bit
+	*
+	* @class ObjectCtx
+	* @namespace munge.pipeline.expressions.Expression
+	* @module munge
+	**/
 	var ObjectCtx = Expression.ObjectCtx = (function(){
 		// CONSTRUCTOR
 		var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/){
@@ -28,8 +45,14 @@ var Expression = module.exports = (function(){
 		return klass;
 	})();
 
-	/** Decribes how and when to create an Op instance **/
-	var OpDesc = (function(){
+	/**
+	* Decribes how and when to create an Op instance
+	*
+	* @class OpDesc
+	* @namespace munge.pipeline.expressions.Expression
+	* @module munge
+	**/
+	var OpDesc = Expression.OpDesc = (function(){
 		// CONSTRUCTOR
 		var klass = function OpDesc(name, factory, flags, argCount){
 			if (arguments[0] instanceof Object && arguments[0].constructor == Object) { //TODO: using this?
@@ -62,6 +85,12 @@ var Expression = module.exports = (function(){
 
 		return klass;
 	})();
+	// END OF NESTED CLASSES
+	/**
+	* @class Expression
+	* @namespace munge.pipeline.expressions
+	* @module munge
+	**/
 
 	var kinds = {
 		UNKNOWN: "UNKNOWN",
@@ -71,7 +100,12 @@ var Expression = module.exports = (function(){
 
 
 	// STATIC MEMBERS
-	/** Enumeration of comparison operators.  These are shared between a few expression implementations, so they are factored out here. **/
+	/**
+	* Enumeration of comparison operators.  These are shared between a few expression implementations, so they are factored out here.
+	*
+	* @static
+	* @property CmpOp
+	**/
 	klass.CmpOp = {
 		EQ: "$eq",		// return true for a == b, false otherwise
 		NE: "$ne",		// return true for a != b, false otherwise
@@ -82,24 +116,24 @@ var Expression = module.exports = (function(){
 		CMP: "$cmp"		// return -1, 0, 1 for a < b, a == b, a > b
 	};
 
-	// DEPENDENCIES (later in this file as compared to others to ensure that statics are setup first)
+	// DEPENDENCIES (later in this file as compared to others to ensure that the required statics are setup first)
 	var FieldPathExpression = require("./FieldPathExpression"),
 		ObjectExpression = require("./ObjectExpression"),
 		ConstantExpression = require("./ConstantExpression"),
 		CompareExpression = require("./CompareExpression");
 
 	// DEFERRED DEPENDENCIES
-	/** Expressions, as exposed to users **/
+	/**
+	* Expressions, as exposed to users
+	*
+	* @static
+	* @property opMap
+	**/
 	process.nextTick(function(){ // Even though `opMap` is deferred, force it to load early rather than later to prevent even *more* potential silliness
 		Object.defineProperty(klass, "opMap", {value:klass.opMap});
 	});
 	Object.defineProperty(klass, "opMap", {	//NOTE: deferred requires using a getter to allow circular requires (to maintain the ported API)
 		configurable: true,
-		/**
-		* Autogenerated docs! Please modify if you you touch this method
-		*
-		* @method get
-		**/
 		get: function getOpMapOnce() {
 			return Object.defineProperty(klass, "opMap", {
 				value: [	//NOTE: rather than OpTable because it gets converted to a dict via OpDesc#name in the Array#reduce() below
@@ -142,13 +176,16 @@ var Expression = module.exports = (function(){
 	/**
 	* Parse an Object.  The object could represent a functional expression or a Document expression.
 	*
-	* @param obj	the element representing the object
-	* @param ctx	a MiniCtx representing the options above
-	* @returns the parsed Expression
-	*
 	* An object expression can take any of the following forms:
+	*
 	*	f0: {f1: ..., f2: ..., f3: ...}
 	*	f0: {$operator:[operand1, operand2, ...]}
+	*
+	* @static
+	* @method parseObject
+	* @param obj	the element representing the object
+	* @param ctx	a MiniCtx representing the options above
+	* @returns the parsed Expression
 	**/
 	klass.parseObject = function parseObject(obj, ctx){
 		if(!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
@@ -194,7 +231,7 @@ var Expression = module.exports = (function(){
 						if (!ctx.isInclusionOk) throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
 						exprObj.includePath(fn);
 					} else {
-						if (!(ctx.isTopLevel && fn == "_id")) throw new Error("The top-level _id field is the only field currently supported for exclusion; code 16406");
+						if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME)) throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
 						exprObj.excludeId(true);
 					}
 					break;
@@ -326,5 +363,9 @@ var Expression = module.exports = (function(){
 		return false;
 	};
 
+	proto.toMatcherBson = function toMatcherBson(){
+		throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");	//verify(false && "Expression::toMatcherBson()");
+	};
+
 	return klass;
 })();

+ 8 - 1
lib/pipeline/expressions/FieldPathExpression.js

@@ -3,7 +3,12 @@ var FieldPathExpression = module.exports = (function(){
 	/**
 	* Create a field path expression. Evaluation will extract the value associated with the given field path from the source document.
 	*
-	* @param fieldPath the field path string, without any leading document indicator
+	* @class FieldPathExpression
+	* @namespace munge.pipeline.expressions
+	* @module munge
+	* @extends munge.pipeline.expressions.Expression
+	* @constructor
+	* @param {String} fieldPath the field path string, without any leading document indicator
 	**/
 	var klass = function FieldPathExpression(path){
 		if(arguments.length !== 1) throw new Error("args expected: path");
@@ -80,5 +85,7 @@ var FieldPathExpression = module.exports = (function(){
 //TODO: proto.addToBsonObj = ...?
 //TODO: proto.addToBsonArray = ...?
 
+//proto.writeFieldPath = ...?   use #getFieldPath instead
+
 	return klass;
 })();

+ 17 - 2
lib/pipeline/expressions/NaryExpression.js

@@ -1,5 +1,14 @@
 var NaryExpression = module.exports = (function(){
 	// CONSTRUCTOR
+	/**
+	* The base class for all n-ary `Expression`s
+	*
+	* @class NaryExpression
+	* @namespace munge.pipeline.expressions
+	* @module munge
+	* @extends munge.pipeline.expressions.Expression
+	* @constructor
+	**/
 	var klass = module.exports = function NaryExpression(){
 		if(arguments.length !== 0) throw new Error("zero args expected");
 		this.operands = [];
@@ -12,6 +21,10 @@ var NaryExpression = module.exports = (function(){
 	// PROTOTYPE MEMBERS
 	proto.evaluate = undefined; // evaluate(doc){ ... defined by inheritor ... }
 
+	proto.getOpName = function getOpName(doc){
+		throw new Error("NOT IMPLEMENTED BY INHERITOR");
+	};
+
 	proto.optimize = function optimize(){
 		var constsFound = 0,
 			stringsFound = 0;
@@ -81,6 +94,7 @@ var NaryExpression = module.exports = (function(){
 	/**
 	* Add an operand to the n-ary expression.
 	*
+	* @method addOperand
 	* @param pExpression the expression to add
 	**/
 	proto.addOperand = function addOperand(expr) {
@@ -98,8 +112,7 @@ var NaryExpression = module.exports = (function(){
 		});
 		return o;
 	};
-
-//TODO:	proto.toBson  ?
+//TODO:	proto.toBson  ?   DONE NOW???
 //TODO:	proto.addToBsonObj  ?
 //TODO: proto.addToBsonArray  ?
 
@@ -107,6 +120,7 @@ var NaryExpression = module.exports = (function(){
 	* Checks the current size of vpOperand; if the size equal to or greater than maxArgs, fires a user assertion indicating that this operator cannot have this many arguments.
 	* The equal is there because this is intended to be used in addOperand() to check for the limit *before* adding the requested argument.
 	*
+	* @method checkArgLimit
 	* @param maxArgs the maximum number of arguments the operator accepts
 	**/
 	proto.checkArgLimit = function checkArgLimit(maxArgs) {
@@ -117,6 +131,7 @@ var NaryExpression = module.exports = (function(){
 	* Checks the current size of vpOperand; if the size is not equal to reqArgs, fires a user assertion indicating that this must have exactly reqArgs arguments.
 	* This is meant to be used in evaluate(), *before* the evaluation takes place.
 	*
+	* @method checkArgCount
 	* @param reqArgs the number of arguments this operator requires
 	**/
 	proto.checkArgCount = function checkArgCount(reqArgs) {

+ 152 - 251
lib/pipeline/expressions/ObjectExpression.js

@@ -1,25 +1,42 @@
 var ObjectExpression = module.exports = (function(){
 	// CONSTRUCTOR
-	/** Create an empty expression.  Until fields are added, this will evaluate to an empty document (object). **/
+	/**
+	* Create an empty expression.  Until fields are added, this will evaluate to an empty document (object).
+	*
+	* @class ObjectExpression
+	* @namespace munge.pipeline.expressions
+	* @module munge
+	* @extends munge.pipeline.expressions.Expression
+	* @constructor
+	**/
 	var klass = function ObjectExpression(){
 		if(arguments.length !== 0) throw new Error("zero args expected");
-		this._excludeId = false;	/// <Boolean> for if _id is to be excluded
+		this.excludeId = false;	/// <Boolean> for if _id is to be excluded
 		this._expressions = {};	/// <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
 		this._order = []; /// <Array<String>> this is used to maintain order for generated fields not in the source document
-	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	}, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 	// DEPENDENCIES
-	var Value = require("../Value");
-
+	var Document = require("../Document"),
+		FieldPath = require("../FieldPath");
 
 	// INSTANCE VARIABLES
-	/** <Boolean> for if _id is to be excluded **/
-	proto._excludeId = undefined;
+	/**
+	* <Boolean> for if _id is to be excluded
+	*
+	* @property excludeId
+	**/
+	proto.excludeId = undefined;
 
-	/** <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document **/
+	/**
+	* <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
+	**/
 	proto._expressions = undefined;
 
-	/** <Array<String>> this is used to maintain order for generated fields not in the source document **/
+	//TODO: might be able to completely ditch _order everywhere in here since `Object`s are mostly ordered anyhow but need to come back and revisit that later
+	/**
+	* <Array<String>> this is used to maintain order for generated fields not in the source document
+	**/
 	proto._order = [];
 
 
@@ -28,29 +45,19 @@ var ObjectExpression = module.exports = (function(){
 	/**
 	* evaluate(), but return a Document instead of a Value-wrapped Document.
 	*
+	* @method evaluateDocument
 	* @param pDocument the input Document
 	* @returns the result document
 	**/
-	proto.evaluateDocument = function evaluateDocument(doc){
-		throw new Error("FINISH evaluateDocument");	//TODO:...
-		/*
-		intrusive_ptr<Document> ExpressionObject::evaluateDocument(
-			const intrusive_ptr<Document> &pDocument) const {
-			// create and populate the result
-			intrusive_ptr<Document> pResult(
-				Document::create(getSizeHint()));
-
-			addToDocument(pResult,
-						Document::create(), // No inclusion field matching.
-						pDocument);
-			return pResult;
-		}
-		*/
+	proto.evaluateDocument = function evaluateDocument(doc) {
+		// create and populate the result
+		var pResult = {};
+		this.addToDocument(pResult, pResult, doc); // No inclusion field matching.
+		return pResult;
 	};
 
-	proto.evaluate = function evaluate(doc){
-		throw new Error("FINISH evaluate");	//TODO:...
-		//return Value::createDocument(evaluateDocument(pDocument));
+	proto.evaluate = function evaluate(doc) { //TODO: collapse with #evaluateDocument()?
+		return this.evaluateDocument(doc);
 	};
 
 	proto.optimize = function optimize(){
@@ -70,247 +77,179 @@ var ObjectExpression = module.exports = (function(){
 	};
 
 	proto.addDependencies = function addDependencies(deps, path){
-		var pathStr;
+		var depsSet = {};
+		var pathStr = "";
 		if (path instanceof Array) {
 			if (path.length === 0) {
 				// we are in the top level of a projection so _id is implicit
-				if (!this._excludeId) deps.insert("_id");
+				if (!this.excludeId) depsSet[Document.ID_PROPERTY_NAME] = 1;
 			} else {
 				pathStr = new FieldPath(path).getPath() + ".";
 			}
 		} else {
-			if (this._excludeId) throw new Error("_excludeId is true!");
+			if (this.excludeId) throw new Error("excludeId is true!");
 		}
 		for (var key in this._expressions) {
 			var expr = this._expressions[key];
-			if (expr !== undefined && expr !== null){
+			if (expr !== undefined && expr !== null) {
 				if (path instanceof Array) path.push(key);
 				expr.addDependencies(deps, path);
 				if (path instanceof Array) path.pop();
-			}else{ // inclusion
-				if(path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; code 16407");
-				deps.insert(pathStr + key);
+			} else { // inclusion
+				if (path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
+				depsSet[pathStr + key] = 1;
 			}
 		}
-		return deps;
+		Array.prototype.push.apply(deps, Object.getOwnPropertyNames(depsSet));
+		return deps;	// NOTE: added to munge as a convenience
 	};
 
 	/**
 	* evaluate(), but add the evaluated fields to a given document instead of creating a new one.
 	*
+	* @method addToDocument
 	* @param pResult the Document to add the evaluated expressions to
 	* @param pDocument the input Document for this level
 	* @param rootDoc the root of the whole input document
 	**/
-	proto.addToDocument = function addToDocument(result, document, rootDoc){
-		throw new Error("FINISH addToDocument");	//TODO:...
-/*
-	void ExpressionObject::addToDocument(
-		const intrusive_ptr<Document> &pResult,
-		const intrusive_ptr<Document> &pDocument,
-		const intrusive_ptr<Document> &rootDoc
-		) const
-	{
-		const bool atRoot = (pDocument == rootDoc);
-
-		ExpressionMap::const_iterator end = _expressions.end();
-
-		// This is used to mark fields we've done so that we can add the ones we haven't
-		set<string> doneFields;
+	proto.addToDocument = function addToDocument(pResult, pDocument, rootDoc){
+		var atRoot = (pDocument === rootDoc);
 
-		FieldIterator fields(pDocument);
-		while(fields.more()) {
-			Document::FieldPair field (fields.next());
+		var doneFields = {};	// This is used to mark fields we've done so that we can add the ones we haven't
 
-			ExpressionMap::const_iterator exprIter = _expressions.find(field.first);
+		for(var fieldName in pDocument){
+			if (!pDocument.hasOwnProperty(fieldName)) continue;
+			var fieldValue = pDocument[fieldName];
 
 			// This field is not supposed to be in the output (unless it is _id)
-			if (exprIter == end) {
-				if (!_excludeId && atRoot && field.first == Document::idName) {
+			if (!this._expressions.hasOwnProperty(fieldName)) {
+				if (!this.excludeId && atRoot && fieldName == Document.ID_PROPERTY_NAME) {
 					// _id from the root doc is always included (until exclusion is supported)
 					// not updating doneFields since "_id" isn't in _expressions
-					pResult->addField(field.first, field.second);
+					pResult[fieldName] = fieldValue;
 				}
 				continue;
 			}
 
 			// make sure we don't add this field again
-			doneFields.insert(exprIter->first);
+			doneFields[fieldName] = true;
 
-			Expression* expr = exprIter->second.get();
-
-			if (!expr) {
-				// This means pull the matching field from the input document
-				pResult->addField(field.first, field.second);
+			// This means pull the matching field from the input document
+			var expr = this._expressions[fieldName];
+			if (!(expr instanceof Expression)) {
+				pResult[fieldName] = fieldValue;
 				continue;
 			}
 
-			ExpressionObject* exprObj = dynamic_cast<ExpressionObject*>(expr);
-			BSONType valueType = field.second->getType();
-			if ((valueType != Object && valueType != Array) || !exprObj ) {
-				// This expression replace the whole field
-
-				intrusive_ptr<const Value> pValue(expr->evaluate(rootDoc));
+			// Check if this expression replaces the whole field
+			if ((fieldValue.constructor !== Object && fieldValue.constructor !== Array) || !(expr instanceof ObjectExpression)) {
+				var pValue = expr.evaluate(rootDoc);
 
 				// don't add field if nothing was found in the subobject
-				if (exprObj && pValue->getDocument()->getFieldCount() == 0)
-					continue;
+				if (expr instanceof ObjectExpression && pValue instanceof Object && Object.getOwnPropertyNames(pValue).length === 0) continue;
 
 				// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
 				// TODO make missing distinct from Undefined
-				if (pValue->getType() != Undefined)
-					pResult->addField(field.first, pValue);
-
-
+				if (pValue !== undefined) pResult[fieldName] = pValue;
 				continue;
 			}
+
 			// Check on the type of the input value.  If it's an object, just walk down into that recursively, and add it to the result.
-			if (valueType == Object) {
-				intrusive_ptr<Document> doc = Document::create(exprObj->getSizeHint());
-				exprObj->addToDocument(doc,
-									field.second->getDocument(),
-									rootDoc);
-				pResult->addField(field.first, Value::createDocument(doc));
-			}
-			else if (valueType == Array) {
+			if (fieldValue.constructor === Object) {
+				pResult[fieldName] = expr.addToDocument({}, fieldValue, rootDoc);	//TODO: pretty sure this is broken;
+			} else if (fieldValue.constructor == Array) {
 				// If it's an array, we have to do the same thing, but to each array element.  Then, add the array of results to the current document.
-				vector<intrusive_ptr<const Value> > result;
-				intrusive_ptr<ValueIterator> pVI(field.second->getArray());
-				while(pVI->more()) {
-					intrusive_ptr<const Value> next =  pVI->next();
-
-					// can't look for a subfield in a non-object value.
-					if (next->getType() != Object)
-						continue;
-
-					intrusive_ptr<Document> doc = Document::create(exprObj->getSizeHint());
-					exprObj->addToDocument(doc,
-										next->getDocument(),
-										rootDoc);
-					result.push_back(Value::createDocument(doc));
+				var result = [];
+				for(var fvi = 0, fvl = fieldValue.length; fvi < fvl; fvi++){
+					var subValue = fieldValue[fvi];
+					if (subValue.constructor !== Object) continue;	// can't look for a subfield in a non-object value.
+					result.push(expr.addToDocument({}, subValue, rootDoc));
 				}
-
-				pResult->addField(field.first,
-									Value::createArray(result));
-			}
-			else {
-				verify( false );
+				pResult[fieldName] = result;
+			} else {
+				throw new Error("should never happen");	//verify( false );
 			}
 		}
-		if (doneFields.size() == _expressions.size())
-			return;
+
+		if (Object.getOwnPropertyNames(doneFields).length == Object.getOwnPropertyNames(this._expressions).length) return pResult;	//NOTE: munge returns result as a convenience
 
 		// add any remaining fields we haven't already taken care of
-		for (vector<string>::const_iterator i(_order.begin()); i!=_order.end(); ++i) {
-			ExpressionMap::const_iterator it = _expressions.find(*i);
-			string fieldName(it->first);
+		for(var i = 0, l = this._order.length; i < l; i++){
+			var fieldName2 = this._order[i];
+			var expr2 = this._expressions[fieldName2];
 
 			// if we've already dealt with this field, above, do nothing
-			if (doneFields.count(fieldName))
-				continue;
+			if (doneFields.hasOwnProperty(fieldName2)) continue;
 
 			// this is a missing inclusion field
-			if (!it->second)
-				continue;
+			if (!expr2) continue;
 
-			intrusive_ptr<const Value> pValue(it->second->evaluate(rootDoc));
+			var value = expr2.evaluate(rootDoc);
 
 			// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
-			if (pValue->getType() == Undefined)
-				continue;
+			if (value === undefined) continue;
 
 			// don't add field if nothing was found in the subobject
-			if (dynamic_cast<ExpressionObject*>(it->second.get())
-					&& pValue->getDocument()->getFieldCount() == 0)
-				continue;
-
+			if (expr2 instanceof ObjectExpression && value && value instanceof Object && Object.getOwnPropertyNames(value).length === 0) continue;
 
-			pResult->addField(fieldName, pValue);
+			pResult[fieldName2] = value;
 		}
-	}
-*/
+
+		return pResult;	//NOTE: munge returns result as a convenience
 	};
 
-	/** estimated number of fields that will be output **/
+	/**
+	* estimated number of fields that will be output
+	*
+	* @method getSizeHint
+	**/
 	proto.getSizeHint = function getSizeHint(){
-		throw new Error("FINISH getSizeHint");	//TODO:...
-		/*
 		// Note: this can overestimate, but that is better than underestimating
-		return _expressions.size() + (_excludeId ? 0 : 1);
-		*/
+		return Object.getOwnPropertyNames(this._expressions).length + (this.excludeId ? 0 : 1);
 	};
 
 	/**
 	* Add a field to the document expression.
 	*
+	* @method addField
 	* @param fieldPath the path the evaluated expression will have in the result Document
 	* @param pExpression the expression to evaluate obtain this field's Value in the result Document
 	**/
-	proto.addField = function addField(path, pExpression){
-		var fieldPart = path.fields[0],
+	proto.addField = function addField(fieldPath, pExpression){
+		if(!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
+		var fieldPart = fieldPath.fields[0],
 			haveExpr = this._expressions.hasOwnProperty(fieldPart),
-			expr = this._expressions[fieldPart];
-var subObj = expr instanceof ObjectExpression ? expr : undefined;
+			subObj = this._expressions[fieldPart];	// inserts if !haveExpr //NOTE: not in munge & JS it doesn't, handled manually below
 
-		if(!haveExpr){
+		if (!haveExpr) {
 			this._order.push(fieldPart);
-		}
-
-		throw new Error("FINISH addField");	//TODO:...
-		/*
-		void ExpressionObject::addField(const FieldPath &fieldPath, const intrusive_ptr<Expression> &pExpression) {
-			const string fieldPart = fieldPath.getFieldName(0);
-			const bool haveExpr = _expressions.count(fieldPart);
-
-			intrusive_ptr<Expression>& expr = _expressions[fieldPart]; // inserts if !haveExpr
-			intrusive_ptr<ExpressionObject> subObj = dynamic_cast<ExpressionObject*>(expr.get());
-
-			if (!haveExpr) {
-				_order.push_back(fieldPart);
-			}
-			else { // we already have an expression or inclusion for this field
-				if (fieldPath.getPathLength() == 1) {
-					// This expression is for right here
-
-					ExpressionObject* newSubObj = dynamic_cast<ExpressionObject*>(pExpression.get());
-					uassert(16400, str::stream()
-									<< "can't add an expression for field " << fieldPart
-									<< " because there is already an expression for that field"
-									<< " or one of its sub-fields.",
-							subObj && newSubObj); // we can merge them
-
-					// Copy everything from the newSubObj to the existing subObj
-					// This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
-					for (vector<string>::const_iterator it (newSubObj->_order.begin());
-														it != newSubObj->_order.end();
-														++it) {
+		} else { // we already have an expression or inclusion for this field
+			if (fieldPath.getPathLength() == 1) { // This expression is for right here
+				if (!(subObj instanceof ObjectExpression && typeof pExpression == "object" && pExpression instanceof ObjectExpression)) throw new Error("can't add an expression for field `" + fieldPart + "` because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
+
+				// Copy everything from the newSubObj to the existing subObj
+				// This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
+				for(var key in pExpression._expressions){
+					if(pExpression._expressions.hasOwnProperty(key)){
 						// asserts if any fields are dupes
-						subObj->addField(*it, newSubObj->_expressions[*it]);
+						subObj.addField(key, pExpression._expressions[key]);
 					}
-					return;
-				}
-				else {
-					// This expression is for a subfield
-					uassert(16401, str::stream()
-							<< "can't add an expression for a subfield of " << fieldPart
-							<< " because there is already an expression that applies to"
-							<< " the whole field",
-							subObj);
 				}
-			}
-
-			if (fieldPath.getPathLength() == 1) {
-				verify(!haveExpr); // haveExpr case handled above.
-				expr = pExpression;
 				return;
+			} else { // This expression is for a subfield
+				if(!subObj) throw new Error("can't add an expression for a subfield of `" + fieldPart + "` because there is already an expression that applies to the whole field; uassert code 16401");
 			}
+		}
 
-			if (!haveExpr)
-				expr = subObj = ExpressionObject::create();
-
-			subObj->addField(fieldPath.tail(), pExpression);
+		if (fieldPath.getPathLength() == 1) {
+			if(haveExpr) throw new Error("Internal error."); // haveExpr case handled above.
+			this._expressions[fieldPart] = pExpression;
+			return;
 		}
-		*/
+
+		if (!haveExpr) subObj = this._expressions[fieldPart] = new ObjectExpression();
+
+		subObj.addField(fieldPath.tail(), pExpression);
 	};
 
 	/**
@@ -318,92 +257,54 @@ var subObj = expr instanceof ObjectExpression ? expr : undefined;
 	*
 	* Note that including a nested field implies including everything on the path leading down to it.
 	*
+	* @method includePath
 	* @param fieldPath the name of the field to be included
 	**/
 	proto.includePath = function includePath(path){
-		this.addField(path);
+		this.addField(path, undefined);
 	};
 
 	/**
 	* Get a count of the added fields.
 	*
+	* @method getFieldCount
 	* @returns how many fields have been added
 	**/
 	proto.getFieldCount = function getFieldCount(){
-		var e; console.warn(e=new Error("CALLER SHOULD BE USING #expressions.length instead!")); console.log(e.stack);
-		return this._expressions.length;
+		return Object.getOwnPropertyNames(this._expressions).length;
 	};
 
-/*TODO: ... remove this?
-	inline ExpressionObject::BuilderPathSink::BuilderPathSink(
-		BSONObjBuilder *pB):
-		pBuilder(pB) {
-	}
-*/
-
-/*TODO: ... remove this?
-	inline ExpressionObject::PathPusher::PathPusher(
-		vector<string> *pTheVPath, const string &s):
-		pvPath(pTheVPath) {
-		pvPath->push_back(s);
-	}
-
-	inline ExpressionObject::PathPusher::~PathPusher() {
-		pvPath->pop_back();
-	}
-*/
-
-/**
-* Specialized BSON conversion that allows for writing out a $project specification.
-* This creates a standalone object, which must be added to a containing object with a name
-*
-* @param pBuilder where to write the object to
-* @param requireExpression see Expression::addToBsonObj
-**/
+///**
+//* Specialized BSON conversion that allows for writing out a $project specification.
+//* This creates a standalone object, which must be added to a containing object with a name
+//*
+//* @param pBuilder where to write the object to
+//* @param requireExpression see Expression::addToBsonObj
+//**/
 //TODO:	proto.documentToBson = ...?
 //TODO:	proto.addToBsonObj = ...?
 //TODO: proto.addToBsonArray = ...?
 
-/*
-/// Visitor abstraction used by emitPaths().  Each path is recorded by calling path().
-		class PathSink {
-		public:
-			virtual ~PathSink() {};
-			/// Record a path.
-			/// @param path the dotted path string
-			/// @param include if true, the path is included; if false, the path is excluded
-			virtual void path(const string &path, bool include) = 0;
-		};
-
-/// Utility object for collecting emitPaths() results in a BSON object.
-		class BuilderPathSink :
-			public PathSink {
-		public:
-			// virtuals from PathSink
-			virtual void path(const string &path, bool include);
-
-/// Create a PathSink that writes paths to a BSONObjBuilder, to create an object in the form of { path:is_included,...}
-/// This object uses a builder pointer that won't guarantee the lifetime of the builder, so make sure it outlasts the use of this for an emitPaths() call.
-/// @param pBuilder to the builder to write paths to
-			BuilderPathSink(BSONObjBuilder *pBuilder);
-
-		private:
-			BSONObjBuilder *pBuilder;
-		};
-
-/// utility class used by emitPaths()
-		class PathPusher :
-			boost::noncopyable {
-		public:
-			PathPusher(vector<string> *pvPath, const string &s);
-			~PathPusher();
-
-		private:
-			vector<string> *pvPath;
-		};
-*/
-
-//void excludeId(bool b) { _excludeId = b; }
+//NOTE: in `munge` we're not passing the `Object`s in and allowing `toJson` (was `documentToBson`) to modify it directly and are instead building and returning a new `Object` since that's the way it's actually used
+proto.toJson = function toJson(requireExpression){	//TODO: requireExpression doesn't seem to really get used in the mongo code; remove it?
+	var o = {};
+	if(this.excludeId)
+		o[Document.ID_PROPERTY_NAME] = false;
+	for(var i = 0, l = this._order.length; i < l; i++){
+		var fieldName = this._order[i];
+		if(!this._expressions.hasOwnProperty(fieldName)) throw new Error("internal error: fieldName from _ordered list not found in _expressions");
+		var fieldValue = this._expressions[fieldName];
+		if(fieldValue === undefined){
+			// this is inclusion, not an expression
+            o[fieldName] = true;
+		}else{
+			o[fieldName] = fieldValue.toJson(requireExpression);
+		}
+	}
+	return o;
+};
+
+//TODO: where's toJson? or is that what documentToBson really is up above?
 
 	return klass;
 })();

+ 1 - 1
test/lib/pipeline/expressions/NaryExpression.js

@@ -4,7 +4,7 @@ var assert = require("assert"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-/** A dummy child of NaryExpression used for testing **/
+// A dummy child of NaryExpression used for testing
 var TestableExpression = (function(){
 	// CONSTRUCTOR
 	var klass = module.exports = function TestableExpression(operands, haveFactory){

+ 691 - 0
test/lib/pipeline/expressions/ObjectExpression.cpp

@@ -0,0 +1,691 @@
+
+/** Empty object spec. */
+class Empty : public ExpectedResultBase {
+public:
+	void prepareExpression() {}
+	BSONObj expected() { return BSON( "_id" << 0 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() { return BSONObj(); }
+};
+
+/** Include 'a' field only. */
+class Include : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "a" ); }
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << 1 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << true );
+	}
+};
+
+/** Cannot include missing 'a' field. */
+class MissingInclude : public ExpectedResultBase {
+public:
+	virtual BSONObj source() { return BSON( "_id" << 0 << "b" << 2 ); }
+	void prepareExpression() { expression()->includePath( "a" ); }
+	BSONObj expected() { return BSON( "_id" << 0 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << true );
+	}
+};
+
+/** Include '_id' field only. */
+class IncludeId : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "_id" ); }
+	BSONObj expected() { return BSON( "_id" << 0 ); }            
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "_id" << true );
+	}
+};
+
+/** Exclude '_id' field. */
+class ExcludeId : public ExpectedResultBase {
+public:
+	void prepareExpression() {
+		expression()->includePath( "b" );
+		expression()->excludeId( true );
+	}
+	BSONObj expected() { return BSON( "b" << 2 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "_id" << false << "b" << true );
+	}
+};
+
+/** Result order based on source document field order, not inclusion spec field order. */
+class SourceOrder : public ExpectedResultBase {
+public:
+	void prepareExpression() {
+		expression()->includePath( "b" );
+		expression()->includePath( "a" );
+	}
+	BSONObj expected() { return source(); }            
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a" << "b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "b" << true << "a" << true );
+	}
+};
+
+/** Include a nested field. */
+class IncludeNested : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "a.b" ); }
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 5 ) ); }
+	BSONObj source() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << 5 << "c" << 6 ) << "z" << 2 );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) );
+	}
+};
+
+/** Include two nested fields. */
+class IncludeTwoNested : public ExpectedResultBase {
+public:
+	void prepareExpression() {
+		expression()->includePath( "a.b" );
+		expression()->includePath( "a.c" );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 5 << "c" << 6 ) ); }
+	BSONObj source() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << 5 << "c" << 6 ) << "z" << 2 );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" << "a.c" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true << "c" << true ) );
+	}
+};
+
+/** Include two fields nested within different parents. */
+class IncludeTwoParentNested : public ExpectedResultBase {
+public:
+	void prepareExpression() {
+		expression()->includePath( "a.b" );
+		expression()->includePath( "c.d" );
+	}
+	BSONObj expected() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << 5 ) << "c" << BSON( "d" << 6 ) );
+	}
+	BSONObj source() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << 5 )
+					 << "c" << BSON( "d" << 6 ) << "z" << 2 );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" << "c.d" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) << "c" << BSON( "d" << true ) );
+	}
+};
+
+/** Attempt to include a missing nested field. */
+class IncludeMissingNested : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "a.b" ); }
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSONObj() ); }
+	BSONObj source() {
+		return BSON( "_id" << 0 << "a" << BSON( "c" << 6 ) << "z" << 2 );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) );
+	}
+};
+
+/** Attempt to include a nested field within a non object. */
+class IncludeNestedWithinNonObject : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "a.b" ); }
+	BSONObj expected() { return BSON( "_id" << 0 ); }
+	BSONObj source() {
+		return BSON( "_id" << 0 << "a" << 2 << "z" << 2 );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) );
+	}
+};
+
+/** Include a nested field within an array. */
+class IncludeArrayNested : public ExpectedResultBase {
+public:
+	void prepareExpression() { expression()->includePath( "a.b" ); }
+	BSONObj expected() { return fromjson( "{_id:0,a:[{b:5},{b:2},{}]}" ); }
+	BSONObj source() {
+		return fromjson( "{_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1}" );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) );
+	}
+};
+
+/** Don't include not root '_id' field implicitly. */
+class ExcludeNonRootId : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 << "a" << BSON( "_id" << 1 << "b" << 1 ) );
+	}
+	void prepareExpression() { expression()->includePath( "a.b" ); }
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 1 ) ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "a.b" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << true ) );
+	}
+};
+
+/** Project a computed expression. */
+class Computed : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a" ),
+								ExpressionConstant::create( Value::createInt( 5 ) ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << 5 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "$const" << 5 ) );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Project a computed expression replacing an existing field. */
+class ComputedReplacement : public Computed {
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 << "a" << 99 );
+	}
+};
+
+/** An undefined value is not projected.. */
+class ComputedUndefined : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a" ),
+								ExpressionConstant::create( Value::getUndefined() ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{$const:undefined}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Project a computed expression replacing an existing field with Undefined. */
+class ComputedUndefinedReplacement : public ComputedUndefined {
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 << "a" << 99 );
+	}
+};
+
+/** A null value is projected. */
+class ComputedNull : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a" ),
+								ExpressionConstant::create( Value::getNull() ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSONNULL ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "$const" << BSONNULL ) );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** A nested value is projected. */
+class ComputedNested : public ExpectedResultBase {
+public:
+	virtual BSONObj source() { return BSON( "_id" << 0 ); }
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b" ),
+								ExpressionConstant::create( Value::createInt( 5 ) ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 5 ) ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return BSON( "a" << BSON( "b" << BSON( "$const" << 5 ) ) );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** A field path is projected. */
+class ComputedFieldPath : public ExpectedResultBase {
+public:
+	virtual BSONObj source() { return BSON( "_id" << 0 << "x" << 4 ); }
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a" ),
+								ExpressionFieldPath::create( "x" ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << 4 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "x" ); }
+	BSONObj expectedBsonRepresentation() { return BSON( "a" << "$x" ); }
+	bool expectedIsSimple() { return false; }
+};
+
+/** A nested field path is projected. */
+class ComputedNestedFieldPath : public ExpectedResultBase {
+public:
+	virtual BSONObj source() { return BSON( "_id" << 0 << "x" << BSON( "y" << 4 ) ); }
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b" ),
+								ExpressionFieldPath::create( "x.y" ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 4 ) ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" << "x.y" ); }
+	BSONObj expectedBsonRepresentation() { return BSON( "a" << BSON( "b" << "$x.y" ) ); }
+	bool expectedIsSimple() { return false; }
+};
+
+/** An empty subobject expression for a missing field is not projected. */
+class EmptyNewSubobject : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		// Create a sub expression returning an empty object.
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "b" ),
+								 ExpressionConstant::create( Value::getUndefined() ) );
+		expression()->addField( mongo::FieldPath( "a" ), subExpression );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{b:{$const:undefined}}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** A non empty subobject expression for a missing field is projected. */
+class NonEmptyNewSubobject : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		// Create a sub expression returning an empty object.
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "b" ),
+								 ExpressionConstant::create( Value::createInt( 6 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), subExpression );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 6 ) ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{b:{$const:6}}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Two computed fields within a common parent. */
+class AdjacentDottedComputedFields : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b" ),
+								ExpressionConstant::create( Value::createInt( 6 ) ) );
+		expression()->addField( mongo::FieldPath( "a.c" ),
+								ExpressionConstant::create( Value::createInt( 7 ) ) );
+	}
+	BSONObj expected() { return BSON( "_id" << 0 << "a" << BSON( "b" << 6 << "c" << 7 ) ); }
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{b:{$const:6},c:{$const:7}}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Two computed fields within a common parent, in one case dotted. */
+class AdjacentDottedAndNestedComputedFields : public AdjacentDottedComputedFields {
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b" ),
+								ExpressionConstant::create( Value::createInt( 6 ) ) );
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "c" ),
+								 ExpressionConstant::create( Value::createInt( 7 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), subExpression );
+	}
+};
+
+/** Two computed fields within a common parent, in another case dotted. */
+class AdjacentNestedAndDottedComputedFields : public AdjacentDottedComputedFields {
+	void prepareExpression() {
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "b" ),
+								 ExpressionConstant::create( Value::createInt( 6 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), subExpression );
+		expression()->addField( mongo::FieldPath( "a.c" ),
+								ExpressionConstant::create( Value::createInt( 7 ) ) );
+	}
+};
+
+/** Two computed fields within a common parent, nested rather than dotted. */
+class AdjacentNestedComputedFields : public AdjacentDottedComputedFields {
+	void prepareExpression() {
+		intrusive_ptr<ExpressionObject> firstSubExpression = ExpressionObject::create();
+		firstSubExpression->addField( mongo::FieldPath( "b" ),
+									  ExpressionConstant::create( Value::createInt( 6 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), firstSubExpression );
+		intrusive_ptr<ExpressionObject> secondSubExpression = ExpressionObject::create();
+		secondSubExpression->addField( mongo::FieldPath( "c" ),
+									   ExpressionConstant::create
+										( Value::createInt( 7 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), secondSubExpression );
+	}            
+};
+
+/** Field ordering is preserved when nested fields are merged. */
+class AdjacentNestedOrdering : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b" ),
+								ExpressionConstant::create( Value::createInt( 6 ) ) );
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
+		subExpression->addField( mongo::FieldPath( "d" ),
+								 ExpressionConstant::create( Value::createInt( 7 ) ) );
+		subExpression->addField( mongo::FieldPath( "c" ),
+								 ExpressionConstant::create( Value::createInt( 8 ) ) );
+		expression()->addField( mongo::FieldPath( "a" ), subExpression );
+	}
+	BSONObj expected() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << 6 << "d" << 7 << "c" << 8 ) );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{b:{$const:6},d:{$const:7},c:{$const:8}}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Adjacent fields two levels deep. */
+class MultipleNestedFields : public ExpectedResultBase {
+public:
+	virtual BSONObj source() {
+		return BSON( "_id" << 0 );
+	}
+	void prepareExpression() {
+		expression()->addField( mongo::FieldPath( "a.b.c" ),
+								ExpressionConstant::create( Value::createInt( 6 ) ) );
+		intrusive_ptr<ExpressionObject> bSubExpression = ExpressionObject::create();
+		bSubExpression->addField( mongo::FieldPath( "d" ),
+								  ExpressionConstant::create( Value::createInt( 7 ) ) );
+		intrusive_ptr<ExpressionObject> aSubExpression = ExpressionObject::create();
+		aSubExpression->addField( mongo::FieldPath( "b" ), bSubExpression );
+		expression()->addField( mongo::FieldPath( "a" ), aSubExpression );
+	}
+	BSONObj expected() {
+		return BSON( "_id" << 0 << "a" << BSON( "b" << BSON( "c" << 6 << "d" << 7 ) ) );
+	}
+	BSONArray expectedDependencies() { return BSON_ARRAY( "_id" ); }
+	BSONObj expectedBsonRepresentation() {
+		return fromjson( "{a:{b:{c:{$const:6},d:{$const:7}}}}" );
+	}
+	bool expectedIsSimple() { return false; }
+};
+
+/** Two expressions cannot generate the same field. */
+class ConflictingExpressionFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ), // Duplicate field.
+											 ExpressionConstant::create
+											  ( Value::createInt( 6 ) ) ),
+					   UserException );
+	}
+};        
+
+/** An expression field conflicts with an inclusion field. */
+class ConflictingInclusionExpressionFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->includePath( "a" );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ),
+											 ExpressionConstant::create
+											  ( Value::createInt( 6 ) ) ),
+					   UserException );
+	}
+};        
+
+/** An inclusion field conflicts with an expression field. */
+class ConflictingExpressionInclusionFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->includePath( "a" ),
+					   UserException );
+	}
+};        
+
+/** An object expression conflicts with a constant expression. */
+class ConflictingObjectConstantExpressionFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->includePath( "b" );
+		expression->addField( mongo::FieldPath( "a" ), subExpression );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a.b" ),
+											 ExpressionConstant::create
+											  ( Value::createInt( 6 ) ) ),
+					   UserException );
+	}
+};        
+
+/** A constant expression conflicts with an object expression. */
+class ConflictingConstantObjectExpressionFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a.b" ),
+							  ExpressionConstant::create( Value::createInt( 6 ) ) );
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->includePath( "b" );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ), subExpression ),
+					   UserException );
+	}
+};        
+
+/** Two nested expressions cannot generate the same field. */
+class ConflictingNestedFields : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a.b" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a.b" ), // Duplicate field.
+											 ExpressionConstant::create
+											  ( Value::createInt( 6 ) ) ),
+					   UserException );
+	}
+};        
+
+/** An expression cannot be created for a subfield of another expression. */
+class ConflictingFieldAndSubfield : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a.b" ),
+											 ExpressionConstant::create
+											  ( Value::createInt( 5 ) ) ),
+					   UserException );
+	}
+};
+
+/** An expression cannot be created for a nested field of another expression. */
+class ConflictingFieldAndNestedField : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "b" ),
+								 ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ), subExpression ),
+					   UserException );
+	}
+};
+
+/** An expression cannot be created for a parent field of another expression. */
+class ConflictingSubfieldAndField : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a.b" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ),
+											 ExpressionConstant::create
+											  ( Value::createInt( 5 ) ) ),
+					   UserException );
+	}
+};
+
+/** An expression cannot be created for a parent of a nested field. */
+class ConflictingNestedFieldAndField : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		intrusive_ptr<ExpressionObject> subExpression = ExpressionObject::create();
+		subExpression->addField( mongo::FieldPath( "b" ),
+								 ExpressionConstant::create( Value::createInt( 5 ) ) );
+		expression->addField( mongo::FieldPath( "a" ), subExpression );
+		ASSERT_THROWS( expression->addField( mongo::FieldPath( "a" ),
+											 ExpressionConstant::create
+											  ( Value::createInt( 5 ) ) ),
+					   UserException );
+	}
+};
+
+/** Dependencies for non inclusion expressions. */
+class NonInclusionDependencies : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		assertDependencies( BSON_ARRAY( "_id" ), expression, true );
+		assertDependencies( BSONArray(), expression, false );
+		expression->addField( mongo::FieldPath( "b" ),
+							  ExpressionFieldPath::create( "c.d" ) );
+		assertDependencies( BSON_ARRAY( "_id" << "c.d" ), expression, true );
+		assertDependencies( BSON_ARRAY( "c.d" ), expression, false );
+	}
+};
+
+/** Dependencies for inclusion expressions. */
+class InclusionDependencies : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->includePath( "a" );
+		assertDependencies( BSON_ARRAY( "_id" << "a" ), expression, true );
+		set<string> unused;
+		// 'path' must be provided for inclusion expressions.
+		ASSERT_THROWS( expression->addDependencies( unused ), UserException );
+	}
+};
+
+/** Optimizing an object expression optimizes its sub expressions. */
+class Optimize : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		// Add inclusion.
+		expression->includePath( "a" );
+		// Add non inclusion.
+		expression->addField( mongo::FieldPath( "b" ), ExpressionAnd::create() );
+		expression->optimize();
+		// Optimizing 'expression' optimizes its non inclusion sub expressions, while
+		// inclusion sub expressions are passed through.
+		ASSERT_EQUALS( BSON( "a" << true << "b" << BSON( "$const" << true ) ),
+					   expressionToBson( expression ) );
+	}
+};
+
+/** Serialize to a BSONObj. */
+class AddToBsonObj : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		BSONObjBuilder bob;
+		expression->addToBsonObj( &bob, "foo", false );
+		ASSERT_EQUALS( BSON( "foo" << BSON( "a" << 5 ) ), bob.obj() );
+	}
+};
+
+/** Serialize to a BSONObj, with constants represented by expressions. */
+class AddToBsonObjRequireExpression : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		BSONObjBuilder bob;
+		expression->addToBsonObj( &bob, "foo", true );
+		ASSERT_EQUALS( BSON( "foo" << BSON( "a" << BSON( "$const" << 5 ) ) ), bob.obj() );
+	}
+};
+
+/** Serialize to a BSONArray. */
+class AddToBsonArray : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->addField( mongo::FieldPath( "a" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		BSONArrayBuilder bab;
+		expression->addToBsonArray( &bab );
+		ASSERT_EQUALS( BSON_ARRAY( BSON( "a" << 5 ) ), bab.arr() );
+	}
+};
+
+/**
+ * evaluate() does not supply an inclusion document.  Inclusion spec'd fields are not
+ * included.  (Inclusion specs are not generally expected/allowed in cases where evaluate
+ * is called instead of addToDocument.)
+ */
+class Evaluate : public Base {
+public:
+	void run() {
+		intrusive_ptr<ExpressionObject> expression = ExpressionObject::create();
+		expression->includePath( "a" );
+		expression->addField( mongo::FieldPath( "b" ),
+							  ExpressionConstant::create( Value::createInt( 5 ) ) );
+		expression->addField( mongo::FieldPath( "c" ),
+							  ExpressionFieldPath::create( "a" ) );
+		ASSERT_EQUALS( BSON( "b" << 5 << "c" << 1 ),
+					   toBson( expression->evaluate
+							   ( fromBson
+								 ( BSON( "_id" << 0 << "a" << 1 ) ) )->getDocument() ) );
+	}
+};

+ 639 - 0
test/lib/pipeline/expressions/ObjectExpression.js

@@ -0,0 +1,639 @@
+var assert = require("assert"),
+	ObjectExpression = require("../../../../lib/pipeline/expressions/ObjectExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression");
+
+
+function assertEqualJson(actual, expected, message){
+	assert.strictEqual(message + ":  " + JSON.stringify(actual), message + ":  " + JSON.stringify(expected));
+}
+
+/// An assertion for `ObjectExpression` instances based on Mongo's `ExpectedResultBase` class
+function assertExpectedResult(args) {
+	{// check for required args
+		if (args === undefined) throw new TypeError("missing arg: `args` is required");
+		if (!("expected" in args)) throw new Error("missing arg: `args.expected` is required");
+		if (!("expectedDependencies" in args)) throw new Error("missing arg: `args.expectedDependencies` is required");
+		if (!("expectedJsonRepresentation" in args)) throw new Error("missing arg: `args.expectedJsonRepresentation` is required");
+	}// check for required args
+	{// base args if none provided
+		if (args.source === undefined) args.source = {_id:0, a:1, b:2};
+		if (args.expectedIsSimple === undefined) args.expectedIsSimple = true;
+		if (args.expression === undefined) args.expression = new ObjectExpression(); //NOTE: replaces prepareExpression + _expression assignment
+	}// base args if none provided
+	// run implementation
+	var result = args.expression.addToDocument({}, args.source, args.source);
+	assertEqualJson(result, args.expected, "unexpected results");
+	var dependencies = args.expression.addDependencies([], [/*FAKING: includePath=true*/]);
+	dependencies.sort(), args.expectedDependencies.sort();	// NOTE: this is a minor hack added for munge because I'm pretty sure order doesn't matter for this anyhow
+	assertEqualJson(dependencies, args.expectedDependencies, "unexpected dependencies");
+	assertEqualJson(args.expression.toJson(true), args.expectedJsonRepresentation, "unexpected JSON representation");
+	assertEqualJson(args.expression.getIsSimple(), args.expectedIsSimple, "unexpected isSimple status");
+}
+
+
+module.exports = {
+
+	"ObjectExpression": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new ObjectExpression();
+				});
+			},
+
+		},
+
+		"#addDependencies":{
+
+			"should be able to get dependencies for non-inclusion expressions": function testNonInclusionDependencies(){
+				/** Dependencies for non inclusion expressions. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assertEqualJson(expr.addDependencies([], [/*FAKING: includePath=true*/]), ["_id"], "unexpected dependencies (including _id)");
+				assertEqualJson(expr.addDependencies([]), [], "unexpected dependencies (excluding _id)");
+				expr.addField("b", new FieldPathExpression("c.d"));
+				assertEqualJson(expr.addDependencies([], [/*FAKING: includePath=true*/]), ["c.d", "_id"], "unexpected dependencies (including _id)");
+				assertEqualJson(expr.addDependencies([]), ["c.d"], "unexpected dependencies (excluding _id)");
+			},
+
+			"should be able to get dependencies for inclusion expressions": function testInclusionDependencies(){
+				/** Dependencies for inclusion expressions. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a" );
+				assertEqualJson(expr.addDependencies([], [/*FAKING: includePath=true*/]), ["_id", "a"], "unexpected dependencies (including _id)");
+				assert.throws(function(){
+					expr.addDependencies([]);
+				}, Error);
+			},
+
+		},
+
+		"#toJson": {
+
+			"should be able to convert to JSON representation and have constants represented by expressions": function testJson(){
+				/** Serialize to a BSONObj, with constants represented by expressions. */
+				var expr = new ObjectExpression();
+				expr.addField("foo.a", new ConstantExpression(5));
+				assertEqualJson({foo:{a:{$const:5}}}, expr.toJson(true));
+			}
+
+		},
+
+		"#optimize": {
+
+			"should be able to optimize expression and sub-expressions": function testOptimize(){
+				/** Optimizing an object expression optimizes its sub expressions. */
+				var expr = new ObjectExpression();
+				// Add inclusion.
+				expr.includePath( "a" );
+				// Add non inclusion.
+				expr.addField( "b", new AndExpression());
+				expr.optimize();
+				// Optimizing 'expression' optimizes its non inclusion sub expressions, while inclusion sub expressions are passed through.
+				assertEqualJson({a:true, b:{$const:true}}, expr.toJson(true));
+			},
+
+		},
+
+		"#evaluate()": {
+
+			"should be able to provide an empty object": function testEmpty(){
+				/** Empty object spec. */
+				var expr = new ObjectExpression();
+				assertExpectedResult({
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {},
+				});
+			},
+
+			"should be able to include 'a' field only": function testInclude(){
+				/** Include 'a' field only. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a" );
+				assertExpectedResult({
+					expression: expr,
+					expected: {"_id":0, "a":1},
+					expectedDependencies: ["_id", "a"],
+					expectedJsonRepresentation: {"a":true},
+				});
+			},
+
+			"should NOT be able to include missing 'a' field": function testMissingInclude(){
+				/** Cannot include missing 'a' field. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a" );
+				assertExpectedResult({
+					source: {"_id":0, "b":2},
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id", "a"],
+					expectedJsonRepresentation: {"a":true},
+				});
+			},
+
+			"should be able to include '_id' field only": function testIncludeId(){
+				/** Include '_id' field only. */
+				var expr = new ObjectExpression();
+				expr.includePath( "_id" );
+				assertExpectedResult({
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"_id":true},
+				});
+			},
+
+			"should be able to exclude '_id' field": function testExcludeId(){
+				/** Exclude '_id' field. */
+				var expr = new ObjectExpression();
+				expr.includePath( "b" );
+				expr.excludeId = true;
+				assertExpectedResult({
+					expression: expr,
+					expected: {"b":2},
+					expectedDependencies: ["b"],
+					expectedJsonRepresentation: {"_id":false, "b":true},
+				});
+			},
+
+			"should be able to include fields in source document order regardless of inclusion order": function testSourceOrder(){
+				/** Result order based on source document field order, not inclusion spec field order. */
+				var expr = new ObjectExpression();
+				expr.includePath( "b" );
+				expr.includePath( "a" );
+				assertExpectedResult({
+					expression: expr,
+					get expected() { return this.source; },
+					expectedDependencies: ["_id", "a", "b"],
+					expectedJsonRepresentation: {"b":true, "a":true},
+				});
+			},
+
+			"should be able to include a nested field": function testIncludeNested(){
+				/** Include a nested field. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				assertExpectedResult({
+					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":5} },
+					expectedDependencies: ["_id", "a.b"],
+					expectedJsonRepresentation: {"a":{ "b":true} },
+				});
+			},
+
+			"should be able to include two nested fields": function testIncludeTwoNested(){
+				/** Include two nested fields. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				expr.includePath( "a.c" );
+				assertExpectedResult({
+					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":5, "c":6} },
+					expectedDependencies: ["_id", "a.b", "a.c"],
+					expectedJsonRepresentation: {"a":{ "b":true, "c":true} },
+				});
+			},
+
+			"should be able to include two fields nested within different parents": function testIncludeTwoParentNested(){
+				/** Include two fields nested within different parents. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				expr.includePath( "c.d" );
+				assertExpectedResult({
+					source: {"_id":0, "a":{ "b":5 }, "c":{"d":6} },
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":5}, "c":{"d":6} },
+					expectedDependencies: ["_id", "a.b", "c.d"],
+					expectedJsonRepresentation: {"a":{"b":true}, "c":{"d":true} }
+				});
+			},
+
+			"should be able to attempt to include a missing nested field": function testIncludeMissingNested(){
+				/** Attempt to include a missing nested field. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				assertExpectedResult({
+					source: {"_id":0, "a":{ "c":6}, "z":2 },
+					expression: expr,
+					expected: {"_id":0, "a":{} },
+					expectedDependencies: ["_id", "a.b"],
+					expectedJsonRepresentation: {"a":{ "b":true} },
+				});
+			},
+
+			"should be able to attempt to include a nested field within a non object": function testIncludeNestedWithinNonObject(){
+				/** Attempt to include a nested field within a non object. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				assertExpectedResult({
+					source: {"_id":0, "a":2, "z":2},
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id", "a.b"],
+					expectedJsonRepresentation: {"a":{ "b":true} },
+				});
+			},
+
+			"should be able to include a nested field within an array": function testIncludeArrayNested(){
+				/** Include a nested field within an array. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				assertExpectedResult({
+					source: {_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1},
+					expression: expr,
+					expected: {_id:0,a:[{b:5},{b:2},{}]},
+					expectedDependencies: ["_id", "a.b"],
+					expectedJsonRepresentation: {"a":{ "b":true} },
+				});
+			},
+
+			"should NOT include non-root '_id' field implicitly": function testExcludeNonRootId(){
+				/** Don't include not root '_id' field implicitly. */
+				var expr = new ObjectExpression();
+				expr.includePath( "a.b" );
+				assertExpectedResult({
+					source: {"_id":0, "a":{ "_id":1, "b":1} },
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":1} },
+					expectedDependencies: ["_id", "a.b"],
+					expectedJsonRepresentation: {"a":{ "b":true} },
+				});
+			},
+
+			"should be able to project a computed expression": function testComputed(){
+				/** Project a computed expression. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":5},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"a":{ "$const":5} },
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a computed expression replacing an existing field": function testComputedReplacement(){
+				/** Project a computed expression replacing an existing field. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assertExpectedResult({
+					source: {"_id":0, "a":99},
+					expression: expr,
+					expected: {"_id": 0, "a": 5},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"a": {"$const": 5}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should NOT be able to project an undefined value": function testComputedUndefined(){
+				/** An undefined value is not projected.. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(undefined));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{$const:undefined}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a computed expression replacing an existing field with Undefined": function testComputedUndefinedReplacement(){
+				/** Project a computed expression replacing an existing field with Undefined. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assertExpectedResult({
+					source: {"_id":0, "a":99},
+					expression: expr,
+					expected: {"_id":0, "a":5},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"a":{"$const":5}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a null value": function testComputedNull(){
+				/** A null value is projected. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(null));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":null},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"a":{"$const":null}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a nested value": function testComputedNested(){
+				/** A nested value is projected. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(5));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{"b":5}},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {"a":{"b":{"$const":5}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a field path": function testComputedFieldPath(){
+				/** A field path is projected. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new FieldPathExpression("x"));
+				assertExpectedResult({
+					source: {"_id":0, "x":4},
+					expression: expr,
+					expected: {"_id":0, "a":4},
+					expectedDependencies: ["_id", "x"],
+					expectedJsonRepresentation: {"a":"$x"},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a nested field path": function testComputedNestedFieldPath(){
+				/** A nested field path is projected. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new FieldPathExpression("x.y"));
+				assertExpectedResult({
+					source: {"_id":0, "x":{"y":4}},
+					expression: expr,
+					expected: {"_id":0, "a":{"b":4}},
+					expectedDependencies: ["_id", "x.y"],
+					expectedJsonRepresentation: {"a":{"b":"$x.y"}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should NOT project an empty subobject expression for a missing field": function testEmptyNewSubobject(){
+				/** An empty subobject expression for a missing field is not projected. */
+				var expr = new ObjectExpression();
+				// Create a sub expression returning an empty object.
+				var subExpr = new ObjectExpression();
+				subExpr.addField("b", new ConstantExpression(undefined));
+				expr.addField( "a", subExpr );
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:undefined}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project a non-empty new subobject": function testNonEmptyNewSubobject(){
+				/** A non empty subobject expression for a missing field is projected. */
+				var expr = new ObjectExpression();
+				// Create a sub expression returning an empty object.
+				var subExpr = new ObjectExpression();
+				subExpr.addField("b", new ConstantExpression(6));
+				expr.addField( "a", subExpr );
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project two computed fields within a common parent": function testAdjacentDottedComputedFields(){
+				/** Two computed fields within a common parent. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(6));
+				expr.addField("a.c", new ConstantExpression(7));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6, "c":7} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project two computed fields within a common parent (w/ one case dotted)": function testAdjacentDottedAndNestedComputedFields(){
+				/** Two computed fields within a common parent, in one case dotted. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(6));
+				var subExpr = new ObjectExpression();
+				subExpr.addField("c", new ConstantExpression( 7 ) );
+				expr.addField("a", subExpr);
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6, "c":7} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project two computed fields within a common parent (in another case dotted)": function testAdjacentNestedAndDottedComputedFields(){
+				/** Two computed fields within a common parent, in another case dotted. */
+				var expr = new ObjectExpression();
+				var subExpr = new ObjectExpression();
+				subExpr.addField("b", new ConstantExpression(6));
+				expr.addField("a", subExpr );
+				expr.addField("a.c", new ConstantExpression(7));
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6, "c":7} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project two computed fields within a common parent (nested rather than dotted)": function testAdjacentNestedComputedFields(){
+				/** Two computed fields within a common parent, nested rather than dotted. */
+				var expr = new ObjectExpression();
+				var subExpr1 = new ObjectExpression();
+				subExpr1.addField("b", new ConstantExpression(6));
+				expr.addField("a", subExpr1);
+				var subExpr2 = new ObjectExpression();
+				subExpr2.addField("c", new ConstantExpression(7));
+				expr.addField("a", subExpr2);
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6, "c":7} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project multiple nested fields out of order without affecting output order": function testAdjacentNestedOrdering(){
+				/** Field ordering is preserved when nested fields are merged. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(6));
+				var subExpr = new ObjectExpression();
+				// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
+				subExpr.addField("d", new ConstantExpression(7));
+				subExpr.addField("c", new ConstantExpression(8));
+				expr.addField("a", subExpr);
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":6, "d":7, "c":8} },
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{$const:6},d:{$const:7},c:{$const:8}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should be able to project adjacent fields two levels deep": function testMultipleNestedFields(){
+				/** Adjacent fields two levels deep. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b.c", new ConstantExpression(6));
+				var bSubExpression = new ObjectExpression();
+				bSubExpression.addField("d", new ConstantExpression(7));
+				var aSubExpression = new ObjectExpression();
+				aSubExpression.addField("b", bSubExpression);
+				expr.addField("a", aSubExpression);
+				assertExpectedResult({
+					source: {"_id":0},
+					expression: expr,
+					expected: {"_id":0, "a":{ "b":{ "c":6, "d":7}}},
+					expectedDependencies: ["_id"],
+					expectedJsonRepresentation: {a:{b:{c:{$const:6},d:{$const:7}}}},
+					expectedIsSimple: false
+				});
+			},
+
+			"should throw an Error if two expressions generate the same field": function testConflictingExpressionFields(){
+				/** Two expressions cannot generate the same field. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.addField("a", new ConstantExpression(6)); // Duplicate field.
+				}, Error);
+			},
+
+			"should throw an Error if an expression field conflicts with an inclusion field": function testConflictingInclusionExpressionFields(){
+				/** An expression field conflicts with an inclusion field. */
+				var expr = new ObjectExpression();
+				expr.includePath("a");
+				assert.throws(function(){
+					expr.addField("a", new ConstantExpression(6));
+				}, Error);
+			},
+
+			"should throw an Error if an inclusion field conflicts with an expression field": function testConflictingExpressionInclusionFields(){
+				/** An inclusion field conflicts with an expression field. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.includePath("a");
+				}, Error);
+			},
+
+			"should throw an Error if an object expression conflicts with a constant expression": function testConflictingObjectConstantExpressionFields(){
+				/** An object expression conflicts with a constant expression. */
+				var expr = new ObjectExpression();
+				var subExpr = new ObjectExpression();
+				subExpr.includePath("b");
+				expr.addField("a", subExpr);
+				assert.throws(function(){
+					expr.addField("a.b", new ConstantExpression(6));
+				}, Error);
+			},
+
+			"should throw an Error if a constant expression conflicts with an object expression": function testConflictingConstantObjectExpressionFields(){
+				/** A constant expression conflicts with an object expression. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(6));
+				var subExpr = new ObjectExpression();
+				subExpr.includePath("b");
+				assert.throws(function(){
+					expr.addField("a", subExpr);
+				}, Error);
+			},
+
+			"should throw an Error if two nested expressions cannot generate the same field": function testConflictingNestedFields(){
+				/** Two nested expressions cannot generate the same field. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.addField("a.b", new ConstantExpression(6));	// Duplicate field.
+				}, Error);
+			},
+
+			"should throw an Error if an expression is created for a subfield of another expression": function testConflictingFieldAndSubfield(){
+				/** An expression cannot be created for a subfield of another expression. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.addField("a.b", new ConstantExpression(5));
+				}, Error);
+			},
+
+			"should throw an Error if an expression is created for a nested field of another expression.": function testConflictingFieldAndNestedField(){
+				/** An expression cannot be created for a nested field of another expression. */
+				var expr = new ObjectExpression();
+				expr.addField("a", new ConstantExpression(5));
+				var subExpr = new ObjectExpression();
+				subExpr.addField("b", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.addField("a", subExpr);
+				}, Error);
+			},
+
+			"should throw an Error if an expression is created for a parent field of another expression": function testConflictingSubfieldAndField(){
+				/** An expression cannot be created for a parent field of another expression. */
+				var expr = new ObjectExpression();
+				expr.addField("a.b", new ConstantExpression(5));
+				assert.throws(function(){
+					expr.addField("a", new ConstantExpression(5));
+				}, Error);
+			},
+
+			"should throw an Error if an expression is created for a parent of a nested field": function testConflictingNestedFieldAndField(){
+				/** An expression cannot be created for a parent of a nested field. */
+				var expr = new ObjectExpression();
+				var subExpr = new ObjectExpression();
+				subExpr.addField("b", new ConstantExpression(5));
+				expr.addField("a", subExpr);
+				assert.throws(function(){
+					expr.addField("a", new ConstantExpression(5));
+				}, Error);
+			},
+
+			"should be able to evaluate expressions in general": function testEvaluate(){
+				/**
+				 * evaluate() does not supply an inclusion document.
+				 * Inclusion spec'd fields are not included.
+				 * (Inclusion specs are not generally expected/allowed in cases where evaluate is called instead of addToDocument.)
+				 */
+				var expr = new ObjectExpression();
+				expr.includePath("a");
+				expr.addField("b", new ConstantExpression(5));
+				expr.addField("c", new FieldPathExpression("a"));
+				assertEqualJson({"b":5, "c":1}, expr.evaluate({_id:0, a:1}));
+			}
+		},
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);