浏览代码

Refs #5131: Add RedactDocumentSource

Chris Sexton 11 年之前
父节点
当前提交
fb51d9b042

+ 116 - 78
lib/pipeline/documentSources/RedactDocumentSource.js

@@ -1,5 +1,12 @@
 "use strict";
 
+var async = require("async"),
+	DocumentSource = require("./DocumentSource"),
+	Expression = require("../expressions/Expression"),
+	Variables = require("../expressions/Variables"),
+	VariablesIdGenerator = require("../expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../expressions/VariablesParseState");
+
 /**
  * A document source skipper
  * @class RedactDocumentSource
@@ -8,7 +15,12 @@
  * @constructor
  * @param [ctx] {ExpressionContext}
  **/
-var RedactDocumentSource = module.exports = function RedactDocumentSource(ctx){
+var RedactDocumentSource = module.exports = function RedactDocumentSource(ctx, expression){
+	if (arguments.length > 2) throw new Error("up to two args expected");
+	base.call(this, ctx);
+	this._expression = expression;
+	this._variables = new Variables();
+	this._currentId = null;
 }, klass = RedactDocumentSource, base = require('./DocumentSource'), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 klass.redactName = "$redact";
@@ -16,88 +28,102 @@ proto.getSourceName = function getSourceName(){
 	return klass.redactName;
 };
 
-/**
- * Coalesce skips together
- * @param {Object} nextSource the next source
- * @return {bool} return whether we can coalese together
- **/
-proto.coalesce = function coalesce(nextSource) {
-	var nextSkip =	nextSource.constructor === RedactDocumentSource?nextSource:null;
-
-	// if it's not another $skip, we can't coalesce
-	if (!nextSkip) return false;
-
-	// we need to skip over the sum of the two consecutive $skips
-	this.skip += nextSkip.skip;
-	return true;
+var DESCEND_VAL = 'descend',
+	PRUNE_VAL = 'prune',
+	KEEP_VAL = 'keep';
+
+proto.getNext = function getNext(callback) {
+	var self = this,
+		doc;
+	async.whilst(
+		function() {
+			return doc !== DocumentSource.EOF;
+		},
+		function(cb) {
+			self.source.getNext(function(err, input) {
+				doc = input;
+				if (input === DocumentSource.EOF)
+					return cb();
+				self._variables.setRoot(input);
+				self._variables.setValue(self._currentId, input);
+				var result = self.redactObject();
+				if (result !== DocumentSource.EOF)
+					return cb(result);
+				return cb();
+			});
+		},
+		function(doc) {
+			if (doc)
+				return callback(null, doc);
+			return callback(null, DocumentSource.EOF);
+		}
+	);
 };
 
-proto.skipper = function skipper() {
-	if (this.count === 0) {
-		while (!this.source.eof() && this.count++ < this.skip) {
-			this.source.advance();
+proto.redactValue = function redactValue(input) {
+	// reorder to make JS happy with types
+	if (input instanceof Array) {
+		var newArr,
+			arr = input;
+		for (var i = 0; i < arr.length; i++) {
+			if (arr[i] instanceof Object || arr[i] instanceof Array) {
+				var toAdd = this.redactValue(arr[i]);
+				if (toAdd)
+					newArr.push(arr[i]);
+			} else {
+				newArr.push(arr[i]);
+			}
 		}
+		return newArr;
+	} else if (input instanceof Object) {
+		this._variables.setValue(this._currentId, input);
+		var result = this.redactObject();
+		if (result !== DocumentSource.EOF)
+			return result;
+		return null;
+	} else {
+		return input;
 	}
-
-	if (this.source.eof()) {
-		this.current = null;
-		return;
-	}
-
-	this.current = this.source.getCurrent();
-};
-
-
-/**
- * Is the source at EOF?
- * @method	eof
- **/
-proto.eof = function eof() {
-	this.skipper();
-	return this.source.eof();
 };
 
 /**
- * some implementations do the equivalent of verify(!eof()) so check eof() first
- * @method	getCurrent
- * @returns	{Document}	the current Document without advancing
+ * Redacts the current object
  **/
-proto.getCurrent = function getCurrent() {
-	this.skipper();
-	return this.source.getCurrent();
-};
+proto.redactObject = function redactObject() {
+	var expressionResult = this._expression.evaluate(this._variables);
+
+	if (expressionResult === KEEP_VAL) {
+		return this._variables.getDocument(this._currentId);
+	} else if (expressionResult === PRUNE_VAL) {
+		return DocumentSource.EOF;
+	} else if (expressionResult === DESCEND_VAL) {
+		var input = this._variables.getDocument(this._currentId);
+		var out;
+
+		var inputKeys = Object.keys(input);
+		for (var i = 0; i < inputKeys.length; i++) {
+			var field = inputKeys[i],
+				value = input[field];
+
+			var val = this.redactValue(value);
+			if (val)
+				out[field] = val;
+		}
 
-/**
- * Advance the state of the DocumentSource so that it will return the next Document.
- * The default implementation returns false, after checking for interrupts.
- * Derived classes can call the default implementation in their own implementations in order to check for interrupts.
- *
- * @method	advance
- * @returns	{Boolean}	whether there is another document to fetch, i.e., whether or not getCurrent() will succeed.  This default implementation always returns false.
- **/
-proto.advance = function advance() {
-	base.prototype.advance.call(this); // check for interrupts
-	if (this.eof()) {
-		this.current = null;
-		return false;
+		return out;
+	} else {
+		throw new Error("17053 $redact's expression should not return anything aside from the variables $$KEEP, $$DESCEND, and $$PRUNE, but returned " + expressionResult);
 	}
+};
 
-	this.current = this.source.getCurrent();
-	return this.source.advance();
+proto.optimize = function optimize() {
+	this._expression = this._expression.optimize();
 };
 
-/**
- * Create an object that represents the document source.  The object
- * will have a single field whose name is the source's name.  This
- * will be used by the default implementation of addToJsonArray()
- * to add this object to a pipeline being represented in JSON.
- *
- * @method	sourceToJson
- * @param	{Object} builder	JSONObjBuilder: a blank object builder to write to
- * @param	{Boolean}	explain	create explain output
- **/
-proto.sourceToJson = function sourceToJson(builder, explain) {
-	builder.$skip = this.skip;
+proto.serialize = function serialize(explain) {
+	var doc = {};
+	doc[this.getSourceName()] = this._expression.serialize(explain);
+	return doc;
 };
 
 /**
@@ -106,12 +132,24 @@ proto.sourceToJson = function sourceToJson(builder, explain) {
  * @param {Number} JsonElement this thing is *called* Json, but it expects a number
  **/
 klass.createFromJson = function createFromJson(jsonElement, ctx) {
-	if (typeof jsonElement !== "number") throw new Error("code 15972; the value to skip must be a number");
-
-	var nextSkip = new RedactDocumentSource(ctx);
-
-	nextSkip.skip = jsonElement;
-	if (nextSkip.skip < 0 || isNaN(nextSkip.skip)) throw new Error("code 15956; the number to skip cannot be negative");
-
-	return nextSkip;
+	if (!jsonElement)
+		throw new Error("#createFromJson requires at least one argument");
+
+	var idGenerator = new VariablesIdGenerator(),
+		vps = new VariablesParseState(idGenerator),
+		currentId = vps.defineVariable("CURRENT"),
+		descendId = vps.defineVariable("DESCEND"),
+		pruneId = vps.defineVariable("PRUNE"),
+		keepId = vps.defineVariable("KEEP");
+
+	var expression = new Expression.parseOperand(jsonElement, vps),
+		source = new RedactDocumentSource(ctx, expression);
+
+	source._currentId = currentId;
+	source._variables = new Variables(idGenerator.getIdCount());
+	source._variables.setValue(descendId, DESCEND_VAL);
+	source._variables.setValue(pruneId, PRUNE_VAL);
+	source._variables.setValue(keepId, KEEP_VAL);
+
+	return source;
 };

+ 109 - 0
test/lib/pipeline/documentSources/RedactDocumentSource.js

@@ -0,0 +1,109 @@
+"use strict";
+var assert = require("assert"),
+	async = require("async"),
+	DocumentSource = require("../../../../lib/pipeline/documentSources/DocumentSource"),
+	RedactDocumentSource = require("../../../../lib/pipeline/documentSources/RedactDocumentSource"),
+	CursorDocumentSource = require("../../../../lib/pipeline/documentSources/CursorDocumentSource"),
+	Cursor = require("../../../../lib/Cursor");
+
+var exampleRedact = {$cond: [
+	{$gt:[3, 0]},
+	"$$DESCEND",
+	"$$PRUNE"]
+};
+
+//TESTS
+module.exports = {
+
+	"RedactDocumentSource": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor() {
+				assert.doesNotThrow(function() {
+					new RedactDocumentSource();
+				});
+			}
+
+		},
+
+		"#getSourceName()": {
+
+			"should return the correct source name; $redact": function testSourceName() {
+				var rds = new RedactDocumentSource();
+				assert.strictEqual(rds.getSourceName(), "$redact");
+			}
+
+		},
+
+		"#getNext()": {
+
+			"should return EOF": function testEOF(next) {
+				var rds = RedactDocumentSource.createFromJson(exampleRedact);
+				rds.setSource({
+					getNext: function getNext(cb) {
+						return cb(null, DocumentSource.EOF);
+					}
+				});
+				rds.getNext(function(err, doc) {
+					assert.equal(DocumentSource.EOF, doc);
+					next();
+				});
+			},
+
+			"iterator state accessors consistently report the source is exhausted": function assertExhausted() {
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				var input = [{}];
+				cwc._cursor = new Cursor( input );
+				var cds = new CursorDocumentSource(cwc);
+				var rds = RedactDocumentSource.createFromJson(exampleRedact);
+				rds.setSource(cds);
+				rds.getNext(function(err, actual) {
+					rds.getNext(function(err, actual1) {
+						assert.equal(DocumentSource.EOF, actual1);
+						rds.getNext(function(err, actual2) {
+							assert.equal(DocumentSource.EOF, actual2);
+							rds.getNext(function(err, actual3) {
+								assert.equal(DocumentSource.EOF, actual3);
+							});
+						});
+					});
+				});
+			},
+
+			"callback is required": function requireCallback() {
+				var rds = new RedactDocumentSource();
+				assert.throws(rds.getNext.bind(rds));
+			},
+		},
+
+		"#optimize()": {
+
+			"Optimize the expression": function optimizeProject() {
+				var rds = RedactDocumentSource.createFromJson(exampleRedact);
+				assert.doesNotThrow(rds.optimize.bind(rds));
+			}
+
+		},
+
+		"#createFromJson()": {
+
+			"should error if called with non-object": function testNonObjectPassed() {
+				//Empty args
+				assert.throws(function() {
+					var rds = RedactDocumentSource.createFromJson();
+				});
+				//Invalid spec
+				assert.throws(function() {
+					var rds = RedactDocumentSource.createFromJson({$invalidOperator: 1});
+				});
+
+			}
+
+		},
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).grep(process.env.MOCHA_GREP || '').run(process.exit);
+