Skip to content

Commit

Permalink
Thenable support
Browse files Browse the repository at this point in the history
Dust handles thenables (promises) in context, either directly or as the return value of a context helper. Dust will reserve a chunk asynchronously for the eventual return value of the promise.

If the promise is invoked as a reference, the reference will become the eventual return value. If the promise is invoked as a section, the eventual return value will be pushed onto the stack.
  • Loading branch information
Seth Kinast committed Mar 24, 2015
1 parent 6f15f85 commit 458c608
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 8 deletions.
48 changes: 45 additions & 3 deletions lib/dust.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,16 @@
}
}
return true;

/**
* Decide somewhat-naively if something is a Thenable.
* @param elem {*} object to inspect
* @return {Boolean} is `elem` a Thenable?
*/
dust.isThenable = function(elem) {
return elem &&
typeof elem === 'object' &&
typeof elem.then === 'function';
};

// apply the filter chain and return the output string
Expand Down Expand Up @@ -655,13 +665,15 @@
Chunk.prototype.reference = function(elem, context, auto, filters) {
if (typeof elem === 'function') {
// Changed the function calling to use apply with the current context to make sure
// that "this" is wat we expect it to be inside the function
// that `this` is what we expect it to be inside the function
elem = elem.apply(context.current(), [this, context, null, {auto: auto, filters: filters}]);
if (elem instanceof Chunk) {
return elem;
}
}
if (!dust.isEmpty(elem)) {
if (dust.isThenable(elem)) {
return this.await(elem, context);
} else if (!dust.isEmpty(elem)) {
return this.write(dust.filter(elem, auto, filters));
} else {
return this;
Expand Down Expand Up @@ -722,7 +734,9 @@
return skip(this, context);
}
}
} else if (elem === true) {
} else if (dust.isThenable(elem)) {
return this.await(elem, context, bodies);
} else if (elem === true) {
// true is truthy but does not change context
if (body) {
return body(this, context);
Expand Down Expand Up @@ -825,6 +839,34 @@
}
};

/**
* Reserve a chunk to be evaluated once a thenable is resolved or rejected
* @param thenable {Thenable} the target thenable to await
* @param context {Context} context to use to render the deferred chunk
* @param bodies {Object} must contain a "body", may contain an "error"
* @return {Chunk}
*/
Chunk.prototype.await = function(thenable, context, bodies) {
var body = bodies && bodies.block,
errorBody = bodies && bodies.error;
return this.map(function(chunk) {
thenable.then(function(data) {
if(body) {
chunk.render(body, context.push(data)).end();
} else {
chunk.end(data);
}
}, function(err) {
if(errorBody) {
chunk.render(errorBody, context.push(err)).end();
} else {
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`');
chunk.end();
}
});
});
};

Chunk.prototype.capture = function(body, context, callback) {
return this.map(function(chunk) {
var stub = new Stub(function(err, out) {
Expand Down
8 changes: 6 additions & 2 deletions test/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports.coreSetup = function(suite, auto) {
auto.forEach(function(test) {
suite.test(test.name, function(){
testRender(this, test.source, test.context, test.expected, test.options, test.base, test.error || {}, test.log, test.config);
testRender(this, test.source, test.context, test.expected, test.options, test.base, test.error, test.log, test.config);
});
});

Expand Down Expand Up @@ -153,7 +153,11 @@ function testRender(unit, source, context, expected, options, baseContext, error
}
dust.render(name, context, function(err, output) {
var log = dust.logQueue;
unit.ifError(err);
if(error) {
unit.contains(error, err.message || err);
} else {
unit.ifError(err);
}
if(logMessage) {
for(var i=0; i<log.length; i++) {
if(log[i].message === logMessage) {
Expand Down
63 changes: 61 additions & 2 deletions test/jasmine-test/spec/coreTests.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
/**
* A naive fake-Promise that simply waits for callbacks
* to be bound by calling `.then` and then invokes
* one of the callbacks asynchronously.
* @param err {*} Invokes the `error` callback with this value
* @param data {*} Invokes the `success` callback with this value, if `err` is not set
* @return {Object} a fake Promise with a `then` method
*/
function FakePromise(err, data) {
function then(success, failure) {
setTimeout(function() {
if(err) {
failure(err);
} else {
success(data);
}
}, 0);
}

return {
"then": then
};
}

var coreTests = [
/**
* CORE TESTS
Expand Down Expand Up @@ -747,8 +771,43 @@ var coreTests = [
context: { foo: {bar: "Hello!"} },
expected: "Hello!",
message: "should test an object path"
}
]
},
{
name: "thenable reference",
source: "Eventually {magic}!",
context: { "magic": new FakePromise(null, "magic") },
expected: "Eventually magic!",
message: "should reserve an async chunk for a thenable reference"
},
{
name: "thenable section",
source: "{#promise}Eventually {magic}!{/promise}",
context: { "promise": new FakePromise(null, {"magic": "magic"}) },
expected: "Eventually magic!",
message: "should reserve an async section for a thenable"
},
{
name: "thenable section from function",
source: "{#promise}Eventually {magic}!{/promise}",
context: { "promise": function() { return new FakePromise(null, {"magic": "magic"}); } },
expected: "Eventually magic!",
message: "should reserve an async section for a thenable returned from a function"
},
{
name: "thenable error",
source: "{promise}",
context: { "promise": new FakePromise("promise error") },
log: "Unhandled promise rejection in `thenable error`",
message: "rejected thenable reference logs"
},
{
name: "thenable error with error block",
source: "{#promise}No magic{:error}{message}{/promise}",
context: { "promise": new FakePromise(new Error("promise error")) },
expected: "promise error",
message: "rejected thenable renders error block"
}
]
},
/**
* CONDITINOAL TESTS
Expand Down
2 changes: 1 addition & 1 deletion test/jasmine-test/spec/renderTestSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ describe ('Test the basic functionality of dust', function() {
});

function render(test) {
var messageInLog = false;
return function() {
var messageInLog = false;
var context;
try {
dust.isDebug = !!(test.error || test.log);
Expand Down

0 comments on commit 458c608

Please sign in to comment.