Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thenable support #569

Merged
merged 1 commit into from
Mar 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions lib/dust.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@
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
dust.filter = function(string, auto, filters) {
if (filters) {
Expand Down Expand Up @@ -655,13 +666,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 +735,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 +840,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want dust.streams to also support promises

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streams internally use Chunks.

  function Stream() {
    this.head = new Chunk(this);
  }

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}!",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would happen if someone tries to reference magic.foo. I remember we had some logic that would fire the magic function and look for foo in it's return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Thenable is not a function so it would look for a property on the Object called foo

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