Skip to content

Commit

Permalink
Initial commit of jep-11, the let() function
Browse files Browse the repository at this point in the history
Proposed in jmespath/jmespath.site#6.

I think the code could use a bit of cleanup, but all the
compliance tests are passing.
  • Loading branch information
jamesls committed Feb 25, 2015
1 parent 1874cda commit 25c22b2
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 3 deletions.
54 changes: 51 additions & 3 deletions jmespath.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,9 @@


function TreeInterpreter(runtime) {
this.scopeChain = new ScopeChain();
this.runtime = runtime;
this.runtime.scopeChain = this.scopeChain;
}

TreeInterpreter.prototype = {
Expand All @@ -815,7 +817,9 @@
} else if (isObject(value)) {
var field = value[node.name];
if (field === undefined) {
return null;
// If the field is not defined in the current scope,
// fall back to the scope chain.
return this.scopeChain.resolveReference(node.name);
} else {
return field;
}
Expand Down Expand Up @@ -1081,17 +1085,46 @@
return this.runtime.callFunction(node.name, resolvedArgs);
},

visitExpressionReference: function(node) {
visitExpressionReference: function(node, value) {
var refNode = node.children[0];
// Tag the node with a specific attribute so the type
// checker verify the type.
refNode.jmespathType = "Expref";
refNode.context = value;
return refNode;
}
};

function Runtime(interpreter) {
function ScopeChain() {
this.scopes = [];
}

ScopeChain.prototype = {
pushScope: function(scope) {
this.scopes.push(scope);
},

popScope: function() {
this.scopes.pop();
},

resolveReference: function(name) {
var currentScope;
var currentValue;
for (var i = this.scopes.length - 1; i >= 0; i--) {
currentScope = this.scopes[i];
currentValue = currentScope[name];
if (currentValue !== undefined) {
return currentValue;
}
}
return null;
}
};

function Runtime(interpreter, scopeChain) {
this.interpreter = interpreter;
this.scopeChain = scopeChain;
this.functionTable = {
// name: [function, <signature>]
// The <signature> can be:
Expand Down Expand Up @@ -1119,6 +1152,9 @@
length: {
func: this.functionLength,
signature: [{types: ["string", "array", "object"]}]},
let: {
func: this.functionLet,
signature: [{types: ["object"]}, {"types": ["expref"]}]},
max: {
func: this.functionMax,
signature: [{types: ["array-number", "array-string"]}]},
Expand Down Expand Up @@ -1527,6 +1563,18 @@
return minRecord;
},

functionLet: function(resolvedArgs) {
var scope = resolvedArgs[0];
var expref = resolvedArgs[1];
var interpreter = this.interpreter;
this.scopeChain.pushScope(scope);
try {
return interpreter.visit(expref, expref.context);
} finally {
this.scopeChain.popScope();
}
},

createKeyFunction: function(exprefNode, allowedTypes) {
var that = this;
var interpreter = this.interpreter;
Expand Down
117 changes: 117 additions & 0 deletions test/compliance/functions.json
Original file line number Diff line number Diff line change
Expand Up @@ -692,4 +692,121 @@
]
}
]
}, {
"given":
{
"search_for": "foo",
"people": [
{"name": "a"},
{"name": "b"},
{"name": "c"},
{"name": "foo"},
{"name": "bar"},
{"name": "baz"},
{"name": "qux"},
{"name": "x"},
{"name": "y"},
{"name": "z"}
]
},
"cases": [
{
"description": "Let function with filters",
"expression": "let({search_for: search_for}, &people[?name==search_for].name | [0])",
"result": "foo"
}
]
}, {
"given":
{
"a": {
"mylist": [
{"l1": "1", "result": "foo"},
{"l2": "2", "result": "bar"},
{"l1": "8", "l2": "9"},
{"l1": "8", "l2": "9"}
],
"level2": "2"
},
"level1": "1",
"nested": {
"a": {
"b": {
"c": {
"fourth": "fourth"
},
"third": "third"
},
"second": "second"
},
"first": "first"
},
"precedence": {
"a": {
"b": {
"c": {
"variable": "fourth"
},
"variable": "third",
"other": "y"
},
"variable": "second",
"other": "x"
},
"variable": "first",
"other": "w"
}
},
"cases": [
{
"description": "Basic let from scope",
"expression": "let({level1: level1}, &a.[level2, level1])",
"result": ["2", "1"]
},
{
"description": "Current object has precedence",
"expression": "let({level1: `other`}, &level1)",
"result": "1"
},
{
"description": "No scope specified using literal hash",
"expression": "let(`{}`, &a.level2)",
"result": "2"
},
{
"description": "Arbitrary variable added",
"expression": "let({foo: `anything`}, &[level1, foo])",
"result": ["1", "anything"]
},
{
"description": "Basic let from current object",
"expression": "let({other: level1}, &level1)",
"result": "1"
},
{
"description": "Nested let function with filters",
"expression": "let({level1: level1}, &a.[mylist[?l1==level1].result, let({level2: level2}, &mylist[?l2==level2].result)])[]",
"result": ["foo", "bar"]
},
{
"description": "Nested let function with filters with literal scope binding",
"expression": "let(`{\"level1\": \"1\"}`, &a.[mylist[?l1==level1].result, let({level2: level2}, &mylist[?l2==level2].result)])[]",
"result": ["foo", "bar"]
},
{
"description": "Nested let functions",
"expression": "nested.let({level1: first}, &a.let({level2: second}, &b.let({level3: third}, &c.{first: level1, second: level2, third: level3, fourth: fourth})))",
"result": {"first": "first", "second": "second", "third": "third", "fourth": "fourth"}
},
{
"description": "Precedence of lexical vars from scope object",
"expression": "precedence.let({other: other}, &a.let({other: other}, &b.let({other: other}, &c.{other: other})))",
"result": {"other": "y"}
},
{
"description": "Precedence of lexical vars from current object",
"expression": "precedence.let({variable: variable}, &a.let({variable: variable}, &b.let({variable: variable}, &c.let({variable: `override`}, &variable))))",
"result": "fourth"
}
]
}]

0 comments on commit 25c22b2

Please sign in to comment.