couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jch...@apache.org
Subject svn commit: r801056 - in /couchdb/trunk/share: server/loop.js server/render.js www/script/test/list_views.js www/script/test/show_documents.js
Date Wed, 05 Aug 2009 04:09:11 GMT
Author: jchris
Date: Wed Aug  5 04:09:11 2009
New Revision: 801056

URL: http://svn.apache.org/viewvc?rev=801056&view=rev
Log:
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


Modified:
    couchdb/trunk/share/server/loop.js
    couchdb/trunk/share/server/render.js
    couchdb/trunk/share/www/script/test/list_views.js
    couchdb/trunk/share/www/script/test/show_documents.js

Modified: couchdb/trunk/share/server/loop.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/loop.js?rev=801056&r1=801055&r2=801056&view=diff
==============================================================================
--- couchdb/trunk/share/server/loop.js (original)
+++ couchdb/trunk/share/server/loop.js Wed Aug  5 04:09:11 2009
@@ -19,7 +19,7 @@
   sandbox.sum = sum;
   sandbox.log = log;
   sandbox.toJSON = toJSON;
-  sandbox.respondWith = respondWith;
+  sandbox.provides = provides;
   sandbox.registerType = registerType;
   sandbox.start = start;
   sandbox.send = send;

Modified: couchdb/trunk/share/server/render.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/render.js?rev=801056&r1=801055&r2=801056&view=diff
==============================================================================
--- couchdb/trunk/share/server/render.js (original)
+++ couchdb/trunk/share/server/render.js Wed Aug  5 04:09:11 2009
@@ -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("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 @@
     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 @@
   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 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 @@
   respond({error : "render_error", reason : m});
 }
 
-
 function respondError(e, funSrc, htmlErrors) {
   var logMessage = "function raised error: "+e.toString();
   log(logMessage);
@@ -235,4 +274,3 @@
     "</pre></code></body></html>"].join('');
   return {body:msg};
 };
-

Modified: couchdb/trunk/share/www/script/test/list_views.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/list_views.js?rev=801056&r1=801055&r2=801056&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/list_views.js (original)
+++ couchdb/trunk/share/www/script/test/list_views.js Wed Aug  5 04:09:11 2009
@@ -83,34 +83,33 @@
       }),
       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 @@
         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() {

Modified: couchdb/trunk/share/www/script/test/show_documents.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/show_documents.js?rev=801056&r1=801055&r2=801056&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/show_documents.js (original)
+++ couchdb/trunk/share/www/script/test/show_documents.js Wed Aug  5 04:09:11 2009
@@ -101,28 +101,24 @@
           };
         }
       }),
-      "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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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};



Mime
View raw message