From cd39ebe7d12d999324ff2cc9842567b34dc4d4c7 Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Sun, 14 Jun 2009 18:45:49 +0000 Subject: merge list-iterator branch to trunk. changes JavaScript _list API git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@784601 13f79535-47bb-0310-9956-ffa450edef68 --- share/server/loop.js | 25 +-- share/server/render.js | 183 ++++++++++++++----- share/server/util.js | 5 +- share/www/script/test/list_views.js | 311 ++++++++++++++++++-------------- share/www/script/test/show_documents.js | 14 +- 5 files changed, 335 insertions(+), 203 deletions(-) (limited to 'share') diff --git a/share/server/loop.js b/share/server/loop.js index db6a9702..188692ba 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -21,6 +21,9 @@ try { sandbox.toJSON = toJSON; sandbox.respondWith = respondWith; sandbox.registerType = registerType; + sandbox.start = start; + sandbox.send = send; + sandbox.getRow = getRow; } catch (e) {} // Commands are in the form of json arrays: @@ -31,21 +34,19 @@ try { var line, cmd, cmdkey; var dispatch = { - "reset" : State.reset, - "add_fun" : State.addFun, - "map_doc" : Views.mapDoc, - "reduce" : Views.reduce, - "rereduce" : Views.rereduce, - "validate" : Validate.validate, - "show_doc" : Render.showDoc, - "list_begin" : Render.listBegin, - "list_row" : Render.listRow, - "list_tail" : Render.listTail + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce, + "validate" : Validate.validate, + "show" : Render.show, + "list" : Render.list }; while (line = eval(readline())) { - cmd = eval(line) - line_length = line.length + cmd = eval(line); + line_length = line.length; try { cmdkey = cmd.shift(); if (dispatch[cmdkey]) { diff --git a/share/server/render.js b/share/server/render.js index 13ef1322..99541eab 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -12,9 +12,10 @@ // mimeparse.js // http://code.google.com/p/mimeparse/ +// MIT Licensed http://www.opensource.org/licenses/mit-license.php // Code with comments: http://mimeparse.googlecode.com/svn/trunk/mimeparse.js // Tests: http://mimeparse.googlecode.com/svn/trunk/mimeparse-js-test.html -// Ported from version 0.1.2 +// Ported by Chris Anderson from version 0.1.2 var Mimeparse = (function() { function strip(string) { @@ -111,6 +112,8 @@ var Mimeparse = (function() { return publicMethods; })(); +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"]; @@ -127,11 +130,16 @@ respondWith = function(req, responders) { bestKey = req.query.format; } var rFunc = responders[bestKey || responders.fallback || "html"]; - if (rFunc) { - var resp = maybeWrapResponse(rFunc()); - resp["headers"] = resp["headers"] || {}; - resp["headers"]["Content-Type"] = bestMime; - respond(resp); + 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}); } @@ -162,8 +170,6 @@ registerType("text", "text/plain", "txt"); registerType("html", "text/html"); registerType("xhtml", "application/xhtml+xml", "xhtml"); registerType("xml", "application/xml", "text/xml", "application/x-xml"); -// http://www.ietf.org/rfc/rfc4627.txt -registerType("json", "application/json", "text/x-json"); registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); registerType("css", "text/css"); registerType("ics", "text/calendar"); @@ -171,57 +177,148 @@ 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; + + respond(["start", chunks, startResp]); + chunks = []; + startResp = {}; +} +// Send chunk +var chunks = []; +function send(chunk) { + chunks.push(chunk.toString()); +}; + +function blowChunks(label) { + respond([label||"chunks", chunks]); + chunks = []; +}; + +var gotRow = false, lastRow = false; +function getRow() { + if (lastRow) return null; + if (!gotRow) { + gotRow = true; + sendStart(); + } else { + blowChunks() + } + var line = readline(); + var json = eval(line); + if (json[0] == "list_end") { + lastRow = true + return null; + } + if (json[0] != "list_row") { + respond({ + error: "query_server_error", + reason: "not a row '" + json[0] + "'"}); + quit(); + } + return json[1]; +}; + +//// +//// Render dispatcher +//// +//// +//// +//// +var isShow = false; var Render = (function() { var row_info; + return { - showDoc : function(funSrc, doc, req) { + show : function(funSrc, doc, req) { + isShow = true; var formFun = compileFunction(funSrc); - runRenderFunction(formFun, [doc, req], funSrc); - }, - listBegin : function(head, req) { - row_info = { first_key: null, row_number: 0, prev_key: null }; - runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); - }, - listRow : function(row, req) { - if (row_info.first_key == null) { - row_info.first_key = row.key; - } - runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); - row_info.prev_key = row.key; - row_info.row_number++; + runShowRenderFunction(formFun, [doc, req], funSrc, true); }, - listTail : function(req) { - runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]); + list : function(head, req) { + isShow = false; + runListRenderFunction(funs[0], [head, req], funsrc[0], false); } } })(); -function runRenderFunction(renderFun, args, funSrc, htmlErrors) { - responseSent = false; +function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } +}; + +function runShowRenderFunction(renderFun, args, funSrc, htmlErrors) { try { var resp = renderFun.apply(null, args); - if (!responseSent) { - if (resp) { - respond(maybeWrapResponse(resp)); - } else { - respond({error:"render_error",reason:"undefined response from render function"}); - } + if (resp) { + respond(["resp", maybeWrapResponse(resp)]); + } else { + renderError("undefined response from render function"); } } catch(e) { - var logMessage = "function raised error: "+e.toString(); - log(logMessage); - // log("stacktrace: "+e.stack); - var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; - respond({ - error:"render_error", - reason:errorMessage}); + respondError(e, funSrc, htmlErrors); } }; +function runListRenderFunction(renderFun, args, funSrc, htmlErrors) { + try { + gotRow = false; + lastRow = false; + respTail = ""; + if (renderFun.arity > 2) { + throw("the list API has changed for CouchDB 0.10, please upgrade your code"); + } + var resp = renderFun.apply(null, args); + if (!gotRow) { + getRow(); + } + if (typeof resp != "undefined") { + chunks.push(resp); + } else if (respTail) { + chunks.push(respTail); + } + blowChunks("end"); + } catch(e) { + respondError(e, funSrc, htmlErrors); + } +}; + +function renderError(m) { + respond({error : "render_error", reason : m}); +} + + +function respondError(e, funSrc, htmlErrors) { + var logMessage = "function raised error: "+e.toString(); + log(logMessage); + log("stacktrace: "+e.stack); + var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; + respond({ + error:"render_error", + reason:errorMessage}); +} function escapeHTML(string) { return string.replace(/&/g, "&") @@ -241,11 +338,3 @@ function htmlRenderError(e, funSrc) { return {body:msg}; }; -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { - return resp; - } -}; diff --git a/share/server/util.js b/share/server/util.js index 7faf2f0b..13b8a779 100644 --- a/share/server/util.js +++ b/share/server/util.js @@ -91,10 +91,8 @@ function recursivelySeal(obj) { } } -var responseSent; // prints the object as JSON, and rescues and logs any toJSON() related errors function respond(obj) { - responseSent = true; try { print(toJSON(obj)); } catch(e) { @@ -103,10 +101,11 @@ function respond(obj) { }; log = function(message) { + // return; if (typeof message == "undefined") { message = "Error: attempting to log message of 'undefined'."; } else if (typeof message != "string") { message = toJSON(message); } - print(toJSON({log: message})); + respond(["log", message]); }; diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index 663a5930..2b7e4cb3 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -39,106 +39,127 @@ couchTests.list_views = function(debug) { } }, lists: { - simpleForm: stringFun(function(head, row, req, row_info) { - if (row) { - // we ignore headers on rows and tail - return { - body : '\n
  • Key: '+row.key - +' Value: '+row.value - +' LineNo: '+row_info.row_number+'
  • ' - }; - } else if (head) { - // we return an object (like those used by external and show) - // so that we can specify headers - return { - body : '

    Total Rows: ' - + head.total_rows - + ' Offset: ' + head.offset - + '

    '+ - '

    FirstKey: '+(row_info ? row_info.first_key : '')+ - ' LastKey: '+(row_info ? row_info.prev_key : '')+'

    '}; + basicBasic : stringFun(function(head, req) { + send("head"); + var row; + while(row = getRow()) { + log("row: "+toJSON(row)); + send(row.key); + }; + return "tail"; + }), + basicJSON : stringFun(function(head, req) { + start({"headers":{"Content-Type" : "application/json"}}); + send('{"head":'+toJSON(head)+', '); + send('"req":'+toJSON(req)+', '); + send('"rows":['); + var row, sep = ''; + while (row = getRow()) { + send(sep + toJSON(row)); + sep = ', '; } + return "]}"; }), - acceptSwitch: stringFun(function(head, row, req, row_info) { - return respondWith(req, { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('

    Total Rows: ' + // + head.total_rows + // + ' Offset: ' + head.offset + + '

    FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'

    '; + }), + acceptSwitch: stringFun(function(head, req) { + // respondWith takes care of setting the proper headers + respondWith(req, { html : function() { - // If you're outputting text and you're not setting - // any headers, you can just return a string. - if (head) { - return "HTML '; + send("HTML '; }, xml : function() { - if (head) { - return '' - +'Test XML Feed'; - } else if (row) { - // Becase Safari can't stand to see that dastardly - // E4X outside of a string. Outside of tests you - // can just use E4X literals. + send('' + +'Test XML Feed'); + + while (row = getRow()) { var entry = new XML(''); entry.id = row.id; entry.title = row.key; entry.content = row.value; - // We'll also let you return just an E4X object - // if you aren't setting headers. - return entry; - } else { - return ""; + send(entry); } + return ""; } - }) + }); }), - qsParams: stringFun(function(head, row, req, row_info) { - if(head) return {body: req.query.foo}; - else return {body: "\n"}; + qsParams: stringFun(function(head, req) { + return toJSON(req.query) + "\n"; }), - stopIter: stringFun(function(head, row, req, row_info) { - if(head) { - return {body: "head"}; - } else if(row) { - if(row_info.row_number > 2) return {stop: true}; - return {body: " " + row_info.row_number}; - } else { - return {body: " tail"}; - } + stopIter: stringFun(function(req) { + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; }), - stopIter2: stringFun(function(head, row, req, row_info) { - return respondWith(req, { + stopIter2: stringFun(function(head, req) { + respondWith(req, { html: function() { - if(head) { - return "head"; - } else if(row) { - if(row_info.row_number > 2) return {stop: true}; - return " " + row_info.row_number; - } else { - return " tail"; - } + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; } }); }), - emptyList: stringFun(function(head, row, req, row_info) { - return { body: "" }; + tooManyGetRows : stringFun(function() { + send("head"); + var row; + while(row = getRow()) { + send(row.key); + }; + getRow(); + getRow(); + getRow(); + row = getRow(); + return "after row: "+toJSON(row); }), - rowError : stringFun(function(head, row, req, row_info) { - if (head) { - return "head"; - } else if(row) { - return missingValue; - } else { - return "tail" - } + emptyList: stringFun(function() { + return " "; + }), + rowError : stringFun(function(head, req) { + send("head"); + var row = getRow(); + send(fooBarBam); // intentional error + return "tail"; }) } }; @@ -152,26 +173,40 @@ couchTests.list_views = function(debug) { T(view.total_rows == 10); // standard get - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView"); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView"); T(xhr.status == 200, "standard get should be 200"); - T(/Total Rows/.test(xhr.responseText)); - T(/Key: 1/.test(xhr.responseText)); - T(/LineNo: 0/.test(xhr.responseText)); - T(/LineNo: 5/.test(xhr.responseText)); - T(/FirstKey: 0/.test(xhr.responseText)); - T(/LastKey: 9/.test(xhr.responseText)); - - - var lines = xhr.responseText.split('\n'); - T(/LineNo: 5/.test(lines[6])); + T(/head0123456789tail/.test(xhr.responseText)); // test that etags are available var etag = xhr.getResponseHeader("etag"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView", { + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView", { headers: {"if-none-match": etag} }); T(xhr.status == 304); + // test the richness of the arguments + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView"); + T(xhr.status == 200, "standard get should be 200"); + var resp = JSON.parse(xhr.responseText); + TEquals(resp.head, {total_rows:10, offset:0}); + T(resp.rows.length == 10); + TEquals(resp.rows[0], {"id": "0","key": 0,"value": "0"}); + + TEquals(resp.req.info.db_name, "test_suite_db"); + TEquals(resp.req.verb, "GET"); + TEquals(resp.req.path, [ + "test_suite_db", + "_design", + "lists", + "_list", + "basicJSON", + "basicView" + ]); + T(resp.req.headers.Accept); + T(resp.req.headers.Host); + T(resp.req.headers["User-Agent"]); + T(resp.req.cookie); + // get with query params var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=3"); T(xhr.status == 200, "with query params"); @@ -179,26 +214,30 @@ couchTests.list_views = function(debug) { T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 9/.test(xhr.responseText)); - // with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=30"); T(xhr.status == 200, "0 rows"); T(/Total Rows/.test(xhr.responseText)); - T(/Offset: null/.test(xhr.responseText)); + + //too many Get Rows + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/tooManyGetRows/basicView"); + T(xhr.status == 200, "tooManyGetRows"); + T(/9after row: null/.test(xhr.responseText)); + // reduce with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?startkey=30"); T(xhr.status == 200, "reduce 0 rows"); T(/Total Rows/.test(xhr.responseText)); - T(/Offset: undefined/.test(xhr.responseText)); - + T(/LastKey: undefined/.test(xhr.responseText)); // when there is a reduce present, but not used var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?reduce=false"); T(xhr.status == 200, "reduce false"); T(/Total Rows/.test(xhr.responseText)); T(/Key: 1/.test(xhr.responseText)); + // when there is a reduce present, and used xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?group=true"); @@ -221,48 +260,12 @@ couchTests.list_views = function(debug) { headers: {"if-none-match": etag} }); T(xhr.status == 200, "reduce etag"); - - // with accept headers for HTML - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { - headers: { - "Accept": 'text/html' - } - }); - T(xhr.getResponseHeader("Content-Type") == "text/html"); - T(xhr.responseText.match(/HTML/)); - T(xhr.responseText.match(/Value/)); - - // now with xml - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { - headers: { - "Accept": 'application/xml' - } - }); - T(xhr.getResponseHeader("Content-Type") == "application/xml"); - T(xhr.responseText.match(/XML/)); - T(xhr.responseText.match(/entry/)); - - // now with extra qs params - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/qsParams/basicView?foo=blam"); - T(xhr.responseText.match(/blam/)); - - // aborting iteration - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/basicView"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); - - // aborting iteration with reduce - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); - T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); // empty list var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/emptyList/basicView"); - T(xhr.responseText.match(/^$/)); + T(xhr.responseText.match(/^ $/)); xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/emptyList/withReduce?group=true"); - T(xhr.responseText.match(/^$/)); + T(xhr.responseText.match(/^ $/)); // multi-key fetch var xhr = CouchDB.request("POST", "/test_suite_db/_design/lists/_list/simpleForm/basicView", { @@ -281,7 +284,45 @@ couchTests.list_views = function(debug) { }); T(xhr.status == 400); T(/query_parse_error/.test(xhr.responseText)); - + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/rowError/basicView"); - T(/

    Render Error<\/h1>/.test(xhr.responseText)); + T(/ReferenceError/.test(xhr.responseText)); + + + // now with extra qs params + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/qsParams/basicView?foo=blam"); + T(xhr.responseText.match(/blam/)); + + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/basicView"); + // T(xhr.getResponseHeader("Content-Type") == "text/plain"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); + + // aborting iteration with reduce + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); + T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); + + // with accept headers for HTML + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { + headers: { + "Accept": 'text/html' + } + }); + T(xhr.getResponseHeader("Content-Type") == "text/html"); + T(xhr.responseText.match(/HTML/)); + T(xhr.responseText.match(/Value/)); + + // now with xml + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/acceptSwitch/basicView", { + headers: { + "Accept": 'application/xml' + } + }); + T(xhr.getResponseHeader("Content-Type") == "application/xml"); + T(xhr.responseText.match(/XML/)); + T(xhr.responseText.match(/entry/)); }; diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index bf12e4c6..945bd1da 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -106,9 +106,7 @@ couchTests.show_documents = function(debug) { registerType("foo", "application/foo","application/x-foo"); return respondWith(req, { html : function() { - return { - body:"Ha ha, you said \"" + doc.word + "\"." - }; + return "Ha ha, you said \"" + doc.word + "\"."; }, xml : function() { var xml = new XML(''); @@ -145,10 +143,14 @@ couchTests.show_documents = function(debug) { // hello template world xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid); T(xhr.responseText == "Hello World"); +// +// }; +// +// function foo() { - // error stacktraces - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); - T(JSON.parse(xhr.responseText).error == "render_error"); + // // error stacktraces + // xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); + // T(JSON.parse(xhr.responseText).error == "render_error"); // hello template world (no docid) xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello"); -- cgit v1.2.3