summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2009-08-05 04:09:11 +0000
committerJohn Christopher Anderson <jchris@apache.org>2009-08-05 04:09:11 +0000
commit4ae77952e4f2453425d3ad0a85a453ca102b322f (patch)
tree93ab4c114206b0640d9d99903f16c7a69f7adb04
parent9cddd68f4648620be9d81aedc125704e1824cf2d (diff)
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></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
-rw-r--r--share/server/loop.js2
-rw-r--r--share/server/render.js192
-rw-r--r--share/www/script/test/list_views.js73
-rw-r--r--share/www/script/test/show_documents.js59
4 files changed, 183 insertions, 143 deletions
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) {
"</pre></code></body></html>"].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 <ul>");
-
- var row, num = 0;
- while (row = getRow()) {
- num ++;
- send('\n<li>Key: '
- +row.key+' Value: '+row.value
- +' LineNo: '+num+'</li>');
- }
-
- // tail
- return '</ul>';
- },
- xml : function() {
- send('<feed xmlns="http://www.w3.org/2005/Atom">'
- +'<title>Test XML Feed</title>');
-
- while (row = getRow()) {
- var entry = new XML('<entry/>');
- entry.id = row.id;
- entry.title = row.key;
- entry.content = row.value;
- send(entry);
- }
- return "</feed>";
+ provides("html", function() {
+ send("HTML <ul>");
+
+ var row, num = 0;
+ while (row = getRow()) {
+ num ++;
+ send('\n<li>Key: '
+ +row.key+' Value: '+row.value
+ +' LineNo: '+num+'</li>');
}
+
+ // tail
+ return '</ul>';
+ });
+
+ provides("xml", function() {
+ send('<feed xmlns="http://www.w3.org/2005/Atom">'
+ +'<title>Test XML Feed</title>');
+
+ while (row = getRow()) {
+ var entry = new XML('<entry/>');
+ entry.id = row.id;
+ entry.title = row.key;
+ entry.content = row.value;
+ send(entry);
+ }
+ return "</feed>";
});
}),
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('<xml><node/></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('<xml><node/></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};