From 4ae77952e4f2453425d3ad0a85a453ca102b322f Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Wed, 5 Aug 2009 04:09:11 +0000 Subject: Upgraded JavaScript Accept header handling to make it useful. After user@ thread with Adam Jacob [1] http://tinyurl.com/kuhl2j I realized that giving users the option to set a server preference of mime-types was crucial. Without ordering, you see nasty side effects like a browser getting an Atom feed by default. With ordering, you can ensure that browsers get HTML, API clients see XML, and Ajax apps use JSON in a no-hassle way. Example new API: function(doc, req) { provides("html", function() { return "Hello " + doc.name + "."; }); provides("xml", function() { var xml = new XML(''); xml.hello = doc.name; return xml; } }; If a client sends an Accept header like "application/xml, text/html" this will return html. If the client sends just "application/xml" they will get xml. respondsWith() has been removed. I don't think it's worth the cost to maintain a parallel implementation just to be deprecated as buggy. This patch also continues us on the path to a cleaner, more organized query server. Cheers and enjoy. [1] http://mail-archives.apache.org/mod_mbox/couchdb-user/200907.mbox/%3cb8602b350907241906l7c7f97fdg9d78facacd8605fd@mail.gmail.com%3e git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@801056 13f79535-47bb-0310-9956-ffa450edef68 --- share/server/loop.js | 2 +- share/server/render.js | 192 +++++++++++++++++++------------- share/www/script/test/list_views.js | 73 ++++++------ share/www/script/test/show_documents.js | 59 +++++----- 4 files changed, 183 insertions(+), 143 deletions(-) (limited to 'share') diff --git a/share/server/loop.js b/share/server/loop.js index af656121..34354769 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -19,7 +19,7 @@ try { sandbox.sum = sum; sandbox.log = log; sandbox.toJSON = toJSON; - sandbox.respondWith = respondWith; + sandbox.provides = provides; sandbox.registerType = registerType; sandbox.start = start; sandbox.send = send; diff --git a/share/server/render.js b/share/server/render.js index 9537cd54..51472672 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -10,40 +10,16 @@ // License for the specific language governing permissions and limitations under // the License. -var respCT; -var respTail; -// this function provides a shortcut for managing responses by Accept header -respondWith = function(req, responders) { - var bestKey = null, accept = req.headers["Accept"]; - if (accept && !req.query.format) { - var provides = []; - for (key in responders) { - if (mimesByKey[key]) { - provides = provides.concat(mimesByKey[key]); - } - } - var bestMime = Mimeparse.bestMatch(provides, accept); - bestKey = keysByMime[bestMime]; - } else { - bestKey = req.query.format; - } - var rFunc = responders[bestKey || responders.fallback || "html"]; - if (rFunc) { - if (isShow) { - var resp = maybeWrapResponse(rFunc()); - resp["headers"] = resp["headers"] || {}; - resp["headers"]["Content-Type"] = bestMime; - respond(["resp", resp]); - } else { - respCT = bestMime; - respTail = rFunc(); - } - } else { - throw({code:406, body:"Not Acceptable: "+accept}); - } -}; -// whoever registers last wins. +// registerType(name, mime-type, mime-type, ...) +// +// Available in query server sandbox. TODO: The list is cleared on reset. +// This registers a particular name with the set of mimetypes it can handle. +// Whoever registers last wins. +// +// Example: +// registerType("html", "text/html; charset=utf-8"); + mimesByKey = {}; keysByMime = {}; registerType = function() { @@ -75,32 +51,33 @@ registerType("csv", "text/csv"); registerType("rss", "application/rss+xml"); registerType("atom", "application/atom+xml"); registerType("yaml", "application/x-yaml", "text/yaml"); - // just like Rails registerType("multipart_form", "multipart/form-data"); registerType("url_encoded_form", "application/x-www-form-urlencoded"); - // http://www.ietf.org/rfc/rfc4627.txt registerType("json", "application/json", "text/x-json"); - - - // Start chunks var startResp = {}; function start(resp) { startResp = resp || {}; }; -function sendStart(label) { - startResp = startResp || {}; - startResp["headers"] = startResp["headers"] || {}; - startResp["headers"]["Content-Type"] = startResp["headers"]["Content-Type"] || respCT; - +function sendStart() { + startResp = applyContentType((startResp || {}), responseContentType); respond(["start", chunks, startResp]); chunks = []; startResp = {}; } + +function applyContentType(resp, responseContentType) { + resp["headers"] = resp["headers"] || {}; + if (responseContentType) { + resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; + } + return resp; +} + // Send chunk var chunks = []; function send(chunk) { @@ -119,12 +96,12 @@ function getRow() { gotRow = true; sendStart(); } else { - blowChunks() + blowChunks(); } var line = readline(); var json = eval(line); if (json[0] == "list_end") { - lastRow = true + lastRow = true; return null; } if (json[0] != "list_row") { @@ -136,28 +113,65 @@ function getRow() { return json[1]; }; +var mimeFuns = [], providesUsed, responseContentType; +function provides(type, fun) { + providesUsed = true; + mimeFuns.push([type, fun]); +}; + +function runProvides(req) { + var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; + if (req.query && req.query.format) { + bestKey = req.query.format; + responseContentType = mimesByKey[bestKey][0]; + } else if (accept) { + // log("using accept header: "+accept); + mimeFuns.reverse().forEach(function(mimeFun) { + var mimeKey = mimeFun[0]; + if (mimesByKey[mimeKey]) { + supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); + } + }); + responseContentType = Mimeparse.bestMatch(supportedMimes, accept); + bestKey = keysByMime[responseContentType]; + } else { + // just do the first one + bestKey = mimeFuns[0][0]; + responseContentType = mimesByKey[bestKey][0]; + } + + for (var i=0; i < mimeFuns.length; i++) { + if (mimeFuns[i][0] == bestKey) { + bestFun = mimeFuns[i][1]; + break; + } + }; + + if (bestFun) { + // log("responding with: "+bestKey); + return bestFun(); + } else { + throw({code:406, body:"Not Acceptable: "+accept||bestKey}); + } +}; + + + //// //// Render dispatcher //// //// //// //// -var isShow = false; -var Render = (function() { - var row_info; - - return { - show : function(funSrc, doc, req) { - isShow = true; - var formFun = compileFunction(funSrc); - runShowRenderFunction(formFun, [doc, req], funSrc, true); - }, - list : function(head, req) { - isShow = false; - runListRenderFunction(funs[0], [head, req], funsrc[0], false); - } +var Render = { + show : function(funSrc, doc, req) { + var showFun = compileFunction(funSrc); + runShow(showFun, doc, req, funSrc); + }, + list : function(head, req) { + runList(funs[0], head, req, funsrc[0]); } -})(); +}; function maybeWrapResponse(resp) { var type = typeof resp; @@ -168,38 +182,64 @@ function maybeWrapResponse(resp) { } }; -function runShowRenderFunction(renderFun, args, funSrc, htmlErrors) { +function resetProvides() { + // set globals + providesUsed = false; + mimeFuns = []; + responseContentType = null; +}; + +function runShow(showFun, doc, req, funSrc) { try { - var resp = renderFun.apply(null, args); + resetProvides(); + var resp = showFun.apply(null, [doc, req]); + + if (providesUsed) { + resp = runProvides(req); + resp = applyContentType(maybeWrapResponse(resp), responseContentType); + } + if (resp) { respond(["resp", maybeWrapResponse(resp)]); } else { - renderError("undefined response from render function"); + renderError("undefined response from show function"); } } catch(e) { - respondError(e, funSrc, htmlErrors); + respondError(e, funSrc, true); } }; -function runListRenderFunction(renderFun, args, funSrc, htmlErrors) { + +function resetList() { + gotRow = false; + lastRow = false; + chunks = []; + startResp = {}; +}; + +function runList(listFun, head, req, funSrc) { try { - gotRow = false; - lastRow = false; - respTail = ""; - if (renderFun.arity > 2) { + if (listFun.arity > 2) { throw("the list API has changed for CouchDB 0.10, please upgrade your code"); } - var resp = renderFun.apply(null, args); + + resetProvides(); + resetList(); + + var tail = listFun.apply(null, [head, req]); + + if (providesUsed) { + tail = runProvides(req); + } + if (!gotRow) { getRow(); } - if (typeof resp != "undefined") { - chunks.push(resp); - } else if (respTail) { - chunks.push(respTail); + if (typeof tail != "undefined") { + chunks.push(tail); } blowChunks("end"); } catch(e) { - respondError(e, funSrc, htmlErrors); + respondError(e, funSrc, false); } }; @@ -207,7 +247,6 @@ function renderError(m) { respond({error : "render_error", reason : m}); } - function respondError(e, funSrc, htmlErrors) { var logMessage = "function raised error: "+e.toString(); log(logMessage); @@ -235,4 +274,3 @@ function htmlRenderError(e, funSrc) { ""].join(''); return {body:msg}; }; - diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index 4b089bac..5871e10e 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -83,34 +83,33 @@ couchTests.list_views = function(debug) { }), acceptSwitch: stringFun(function(head, req) { // respondWith takes care of setting the proper headers - respondWith(req, { - html : function() { - send("HTML '; - }, - xml : function() { - send('' - +'Test XML Feed'); - - while (row = getRow()) { - var entry = new XML(''); - entry.id = row.id; - entry.title = row.key; - entry.content = row.value; - send(entry); - } - return ""; + provides("html", function() { + send("HTML '; + }); + + provides("xml", function() { + send('' + +'Test XML Feed'); + + while (row = getRow()) { + var entry = new XML(''); + entry.id = row.id; + entry.title = row.key; + entry.content = row.value; + send(entry); + } + return ""; }); }), qsParams: stringFun(function(head, req) { @@ -127,17 +126,15 @@ couchTests.list_views = function(debug) { return " tail"; }), stopIter2: stringFun(function(head, req) { - respondWith(req, { - html: function() { - send("head"); - var row, row_number = 0; - while(row = getRow()) { - if(row_number > 2) break; - send(" " + row_number); - row_number += 1; - }; - return " tail"; - } + provides("html", function() { + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; }); }), tooManyGetRows : stringFun(function() { diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index c87cfb0b..7181a926 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -101,28 +101,24 @@ couchTests.show_documents = function(debug) { }; } }), - "respondWith" : stringFun(function(doc, req) { + "provides" : stringFun(function(doc, req) { registerType("foo", "application/foo","application/x-foo"); - return respondWith(req, { - html : function() { - return "Ha ha, you said \"" + doc.word + "\"."; - }, - xml : function() { - var xml = new XML(''); - // Becase Safari can't stand to see that dastardly - // E4X outside of a string. Outside of tests you - // can just use E4X literals. - eval('xml.node.@foo = doc.word'); - return { - body: xml - }; - }, - foo : function() { - return { - body: "foofoo" - }; - }, - fallback : "html" + + provides("html", function() { + return "Ha ha, you said \"" + doc.word + "\"."; + }); + + provides("xml", function() { + var xml = new XML(''); + // Becase Safari can't stand to see that dastardly + // E4X outside of a string. Outside of tests you + // can just use E4X literals. + eval('xml.node.@foo = doc.word'); + return xml; + }); + + provides("foo", function() { + return "foofoo"; }); }) } @@ -289,8 +285,8 @@ couchTests.show_documents = function(debug) { etag = xhr.getResponseHeader("etag"); T(etag != "skipped") - // test the respondWith mime matcher - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/respondWith/"+docid, { + // test the provides mime matcher + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, { headers: { "Accept": 'text/html,application/atom+xml; q=0.9' } @@ -301,7 +297,7 @@ couchTests.show_documents = function(debug) { T(xhr.responseText == "Ha ha, you said \"plankton\"."); // now with xml - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/respondWith/"+docid, { + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, { headers: { "Accept": 'application/xml' } @@ -311,7 +307,7 @@ couchTests.show_documents = function(debug) { T(xhr.responseText.match(/plankton/)); // registering types works - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/respondWith/"+docid, { + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, { headers: { "Accept": "application/x-foo" } @@ -319,8 +315,8 @@ couchTests.show_documents = function(debug) { T(xhr.getResponseHeader("Content-Type") == "application/x-foo"); T(xhr.responseText.match(/foofoo/)); - // test the respondWith mime matcher without - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/respondWith/"+docid, { + // test the provides mime matcher without a match + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, { headers: { "Accept": 'text/html,application/atom+xml; q=0.9' } @@ -330,6 +326,15 @@ couchTests.show_documents = function(debug) { T(/text\/html/.test(ct)) T(xhr.responseText == "Ha ha, you said \"plankton\"."); + // should fallback on the first one + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, { + headers: { + "Accept": 'application/x-foo, application/xml' + } + }); + var ct = xhr.getResponseHeader("Content-Type"); + T(/application\/xml/.test(ct)); + // test inclusion of conflict state var doc1 = {_id:"foo", a:1}; var doc2 = {_id:"foo", a:2}; -- cgit v1.2.3