From d485a72706874b5f81d76df61197185a99d30523 Mon Sep 17 00:00:00 2001 From: samuelms1 Date: Wed, 9 Nov 2016 12:14:56 -0700 Subject: [PATCH] {?exists} and {^exists} resolve Promises and check if the result exists (#753) Closes #752 --- lib/dust.js | 96 +++++++++++++++++++++++++++++++----------- test/templates.spec.js | 6 +-- test/templates/all.js | 19 +++++++++ 3 files changed, 93 insertions(+), 28 deletions(-) diff --git a/lib/dust.js b/lib/dust.js index bb59338c..dd1025ad 100644 --- a/lib/dust.js +++ b/lib/dust.js @@ -725,6 +725,11 @@ return this; }; + /** + * Inserts a new chunk that can be used to asynchronously render or write to it + * @param callback {Function} The function that will be called with the new chunk + * @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk + */ Chunk.prototype.map = function(callback) { var cursor = new Chunk(this.root, this.next, this.taps), branch = new Chunk(this.root, cursor, this.taps); @@ -740,6 +745,35 @@ return cursor; }; + /** + * Like Chunk#map but additionally resolves a thenable. If the thenable succeeds the callback is invoked with + * a new chunk that can be used to asynchronously render or write to it, otherwise if the thenable is rejected + * then the error body is rendered if available, an error is logged, and the callback is never invoked. + * @param {Chunk} The current chunk to insert a new chunk + * @param thenable {Thenable} the target thenable to await + * @param context {Context} context to use to render the deferred chunk + * @param bodies {Object} may optionally contain an "error" for when the thenable is rejected + * @param callback {Function} The function that will be called with the new chunk + * @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk + */ + function mapThenable(chunk, thenable, context, bodies, callback) { + return chunk.map(function(asyncChunk) { + thenable.then(function(data) { + try { + callback(asyncChunk, data); + } catch (err) { + // handle errors the same way Chunk#map would. This logic is only here since the thenable defers + // logic such that the try / catch in Chunk#map would not capture it. + dust.log(err, ERROR); + asyncChunk.setError(err); + } + }, function(err) { + dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO); + asyncChunk.renderError(err, context, bodies).end(); + }); + }); + } + Chunk.prototype.tap = function(tap) { var taps = this.taps; @@ -861,6 +895,12 @@ var body = bodies.block, skip = bodies['else']; + if (dust.isThenable(elem)) { + return mapThenable(this, elem, context, bodies, function(chunk, data) { + chunk.exists(data, context, bodies).end(); + }); + } + if (!dust.isEmpty(elem)) { if (body) { return body(this, context); @@ -876,6 +916,12 @@ var body = bodies.block, skip = bodies['else']; + if (dust.isThenable(elem)) { + return mapThenable(this, elem, context, bodies, function(chunk, data) { + chunk.notexists(data, context, bodies).end(); + }); + } + if (dust.isEmpty(elem)) { if (body) { return body(this, context); @@ -970,27 +1016,31 @@ * @return {Chunk} */ Chunk.prototype.await = function(thenable, context, bodies, auto, filters) { - return this.map(function(chunk) { - thenable.then(function(data) { - if (bodies) { - chunk = chunk.section(data, context, bodies); - } else { - // Actually a reference. Self-closing sections don't render - chunk = chunk.reference(data, context, auto, filters); - } - chunk.end(); - }, function(err) { - var errorBody = bodies && bodies.error; - if(errorBody) { - chunk.render(errorBody, context.push(err)).end(); - } else { - dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO); - chunk.end(); - } - }); + return mapThenable(this, thenable, context, bodies, function(chunk, data) { + if (bodies) { + chunk.section(data, context, bodies).end(); + } else { + // Actually a reference. Self-closing sections don't render + chunk.reference(data, context, auto, filters).end(); + } }); }; + /** + * Render an error body if available + * @param err {Error} error that occurred + * @param context {Context} context to use to render the error + * @param bodies {Object} may optionally contain an "error" which will be rendered + * @return {Chunk} + */ + Chunk.prototype.renderError = function(err, context, bodies) { + var errorBody = bodies && bodies.error; + if (errorBody) { + return this.render(errorBody, context.push(err)); + } + return this; + }; + /** * Reserve a chunk to be evaluated with the contents of a streamable. * Currently an error event will bomb out the stream. Once an error @@ -1002,8 +1052,7 @@ * @return {Chunk} */ Chunk.prototype.stream = function(stream, context, bodies, auto, filters) { - var body = bodies && bodies.block, - errorBody = bodies && bodies.error; + var body = bodies && bodies.block; return this.map(function(chunk) { var ended = false; stream @@ -1025,11 +1074,8 @@ if(ended) { return; } - if(errorBody) { - chunk.render(errorBody, context.push(err)); - } else { - dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO); - } + chunk.renderError(err, context, bodies); + dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO); if(!ended) { ended = true; chunk.end(); diff --git a/test/templates.spec.js b/test/templates.spec.js index 6309cf93..1aceec60 100644 --- a/test/templates.spec.js +++ b/test/templates.spec.js @@ -93,7 +93,7 @@ function render(test, dust) { expect(messageInLog(dust.logQueue, test.log)).toEqual(true); } if (typeof test.expected !== 'undefined') { - expect(test.expected).toEqual(output); + expect(output).toEqual(test.expected); } done(); }; @@ -124,7 +124,7 @@ function stream(test, dust) { expect(messageInLog(dust.logQueue, test.log)).toEqual(true); } if (typeof test.expected !== 'undefined') { - expect(test.expected).toEqual(result.output); + expect(result.output).toEqual(test.expected); } done(); }; @@ -196,7 +196,7 @@ function pipe(test, dust) { expect(messageInLog(dust.logQueue, test.log)).toEqual(true); } if (typeof test.expected !== 'undefined') { - expect(test.expected).toEqual(result.data); + expect(result.data).toEqual(test.expected); } if(calls === 2) { done(); diff --git a/test/templates/all.js b/test/templates/all.js index 9886f6ad..c732e5e1 100755 --- a/test/templates/all.js +++ b/test/templates/all.js @@ -262,6 +262,18 @@ return [ message: "should setup base template for next test. hi should not be part of base block name" }, + { + name: "{?exists} supports promises and uses correct context", + source: "{#a}{?b}{test}{/b}{/a}", + context: { + a: { + b: FalsePromise(null, { test: "BAD" }), + test: "GOOD" + } + }, + expected: "GOOD", + message: "{?exists} supports promises and uses correct context", + }, { name: "issue322 use base template picks up prefix chunk data", source: '{>issue322 name="abc"/}' + @@ -584,6 +596,13 @@ return [ expected: "false", message: "empty array is treated as empty in exists" }, + { + name: "empty array resolved from a Promise is treated as empty in exists", + source: "{?emptyArrayFromPromise}true{:else}false{/emptyArrayFromPromise}", + context: {"emptyArrayFromPromise": FalsePromise(null, [])}, + expected: "false", + message: "empty array resolved from a Promise is treated as empty in exists" + }, { name: "empty {} is treated as non empty in exists", source: "{?object}true{:else}false{/object}",