From fbb028e15e85e78806884c9b1d390ae4556a1adb Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 10 Jun 2013 16:17:21 -0600 Subject: [PATCH] Call expressions --- src/ol/expression/expression.js | 78 ++++++++++++++++++---- src/ol/expression/lexer.js | 1 - src/ol/expression/parser.js | 4 +- test/spec/ol/expression/expression.test.js | 70 +++++++++++++++++++ 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/ol/expression/expression.js b/src/ol/expression/expression.js index 51e3aaab52..90f05219e0 100644 --- a/src/ol/expression/expression.js +++ b/src/ol/expression/expression.js @@ -1,3 +1,4 @@ +goog.provide('ol.expression.Call'); goog.provide('ol.expression.Comparison'); goog.provide('ol.expression.ComparisonOp'); goog.provide('ol.expression.Expression'); @@ -29,11 +30,62 @@ ol.expression.Expression = function() {}; * * @param {Object} scope Evaluation scope. All properties of this object * will be available as variables when evaluating the expression. + * @param {Object=} opt_fns Optional scope for looking up functions. If not + * provided, functions will be looked in the evaluation scope. + * @param {Object=} opt_this Object to use as this when evaluating call + * expressions. If not provided, `this` will resolve to a new object. * @return {*} Result of the expression. */ ol.expression.Expression.prototype.evaluate = goog.abstractMethod; + +/** + * A call expression (e.g. `foo(bar)`). + * + * @constructor + * @extends {ol.expression.Expression} + * @param {expr} expr An expression that resolves to a function. + * @param {Array.} args Arguments. + */ +ol.expression.Call = function(expr, args) { + + /** + * @type {expr} + * @private + */ + this.expr_ = expr; + + /** + * @type {Array.} + * @private + */ + this.args_ = args; + +}; +goog.inherits(ol.expression.Call, ol.expression.Expression); + + +/** + * @inheritDoc + */ +ol.expression.Call.prototype.evaluate = function(scope, opt_fns, opt_this) { + var fnScope = goog.isDefAndNotNull(opt_fns) ? opt_fns : scope; + var fn = this.expr_.evaluate(fnScope); + if (!fn || !goog.isFunction(fn)) { + throw new Error('No function in provided scope: ' + this.name_); + } + var thisArg = goog.isDef(opt_this) ? opt_this : {}; + + var len = this.args_.length; + var values = new Array(len); + for (var i = 0; i < len; ++i) { + values[i] = this.args_[i].evaluate(scope, opt_fns, opt_this); + } + return fn.apply(thisArg, values); +}; + + /** * @enum {string} */ @@ -86,10 +138,11 @@ goog.inherits(ol.expression.Comparison, ol.expression.Expression); /** * @inheritDoc */ -ol.expression.Comparison.prototype.evaluate = function(scope) { +ol.expression.Comparison.prototype.evaluate = function(scope, opt_this, + opt_fns) { var result; - var rightVal = this.right_.evaluate(scope); - var leftVal = this.left_.evaluate(scope); + var rightVal = this.right_.evaluate(scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(scope, opt_fns, opt_this); switch (this.operator_) { case ol.expression.ComparisonOp.EQ: @@ -174,7 +227,7 @@ goog.inherits(ol.expression.Literal, ol.expression.Expression); /** * @inheritDoc */ -ol.expression.Literal.prototype.evaluate = function(scope) { +ol.expression.Literal.prototype.evaluate = function() { return this.value_; }; @@ -225,10 +278,10 @@ goog.inherits(ol.expression.Logical, ol.expression.Expression); /** * @inheritDoc */ -ol.expression.Logical.prototype.evaluate = function(scope) { +ol.expression.Logical.prototype.evaluate = function(scope, opt_fns, opt_this) { var result; - var rightVal = this.right_.evaluate(scope); - var leftVal = this.left_.evaluate(scope); + var rightVal = this.right_.evaluate(scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(scope, opt_fns, opt_this); if (this.operator_ === ol.expression.LogicalOp.AND) { result = leftVal && rightVal; @@ -290,10 +343,10 @@ goog.inherits(ol.expression.Math, ol.expression.Expression); /** * @inheritDoc */ -ol.expression.Math.prototype.evaluate = function(scope) { +ol.expression.Math.prototype.evaluate = function(scope, opt_fns, opt_this) { var result; - var rightVal = this.right_.evaluate(scope); - var leftVal = this.left_.evaluate(scope); + var rightVal = this.right_.evaluate(scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(scope, opt_fns, opt_this); /** * TODO: throw if rightVal, leftVal not numbers - this would require the use * of a concat function for strings but it would let us serialize these as @@ -346,7 +399,6 @@ goog.inherits(ol.expression.Not, ol.expression.Expression); /** * @inheritDoc */ -ol.expression.Not.prototype.evaluate = function(scope) { - return !this.expr_.evaluate(scope); +ol.expression.Not.prototype.evaluate = function(scope, opt_fns, opt_this) { + return !this.expr_.evaluate(scope, opt_fns, opt_this); }; - diff --git a/src/ol/expression/lexer.js b/src/ol/expression/lexer.js index cefb950cff..e4a2138515 100644 --- a/src/ol/expression/lexer.js +++ b/src/ol/expression/lexer.js @@ -761,7 +761,6 @@ ol.expression.Lexer.prototype.scanStringLiteral_ = function(quote) { quote === ol.expression.Char.DOUBLE_QUOTE, 'Strings must start with a quote: ' + String.fromCharCode(quote)); - var start = this.index_; this.increment_(1); var str = ''; diff --git a/src/ol/expression/parser.js b/src/ol/expression/parser.js index a7d85db958..8cddb0d177 100644 --- a/src/ol/expression/parser.js +++ b/src/ol/expression/parser.js @@ -17,6 +17,7 @@ goog.provide('ol.expression.Parser'); goog.require('goog.asserts'); +goog.require('ol.expression.Call'); goog.require('ol.expression.Comparison'); goog.require('ol.expression.ComparisonOp'); goog.require('ol.expression.Expression'); @@ -148,10 +149,11 @@ ol.expression.Parser.prototype.createBinaryExpression_ = function(operator, * * @param {ol.expression.Identifier} expr Identifier expression for function. * @param {Array.} args Arguments array. + * @return {ol.expression.Call} Call expression. * @private */ ol.expression.Parser.prototype.createCallExpression_ = function(expr, args) { - throw new Error('Not implemented'); + return new ol.expressions.Call(expr, args); }; diff --git a/test/spec/ol/expression/expression.test.js b/test/spec/ol/expression/expression.test.js index b88ea17f3c..7080c55a5c 100644 --- a/test/spec/ol/expression/expression.test.js +++ b/test/spec/ol/expression/expression.test.js @@ -1,6 +1,75 @@ goog.provide('ol.test.expression.Expression'); +describe('ol.expression.Call', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expression.Call( + new ol.expression.Identifier('sqrt'), + [new ol.expression.Literal(42)]); + expect(expr).to.be.a(ol.expression.Expression); + expect(expr).to.be.a(ol.expression.Call); + }); + }); + + describe('#evaluate()', function() { + var fns = { + sqrt: function(value) { + return Math.sqrt(value); + }, + strConcat: function() { + return Array.prototype.join.call(arguments, ''); + }, + discouraged: function() { + return this.message; + } + }; + + it('calls method on scope with literal args', function() { + var expr = new ol.expression.Call( + new ol.expression.Identifier('sqrt'), + [new ol.expression.Literal(42)]); + expect(expr.evaluate(fns)).to.be(Math.sqrt(42)); + }); + + it('accepts a separate scope for functions', function() { + var expr = new ol.expression.Call( + new ol.expression.Identifier('sqrt'), + [new ol.expression.Identifier('foo')]); + expect(expr.evaluate({foo: 42}, fns)).to.be(Math.sqrt(42)); + }); + + it('accepts multiple expression arguments', function() { + var expr = new ol.expression.Call( + new ol.expression.Identifier('strConcat'), + [ + new ol.expression.Identifier('foo'), + new ol.expression.Literal(' comes after '), + new ol.expression.Math( + ol.expression.MathOp.SUBTRACT, + new ol.expression.Identifier('foo'), + new ol.expression.Literal(1)) + ]); + expect(expr.evaluate({foo: 42}, fns)).to.be('42 comes after 41'); + }); + + it('accepts optional this arg', function() { + var expr = new ol.expression.Call( + new ol.expression.Identifier('discouraged'), []); + + var thisArg = { + message: 'avoid this' + }; + + expect(expr.evaluate(fns, null, thisArg)).to.be('avoid this'); + }); + + }); + +}); + + describe('ol.expression.Comparison', function() { describe('constructor', function() { @@ -367,6 +436,7 @@ describe('ol.expression.Not', function() { }); +goog.require('ol.expression.Call'); goog.require('ol.expression.Comparison'); goog.require('ol.expression.ComparisonOp'); goog.require('ol.expression.Expression');