Created
July 4, 2009 21:24
-
-
Save jasondavies/140738 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in | |
index 2550961..1e45a62 100644 | |
--- a/etc/couchdb/default.ini.tpl.in | |
+++ b/etc/couchdb/default.ini.tpl.in | |
@@ -82,3 +82,4 @@ _view = {couch_httpd_view, handle_view_req} | |
_show = {couch_httpd_show, handle_doc_show_req} | |
_list = {couch_httpd_show, handle_view_list_req} | |
_info = {couch_httpd_db, handle_design_info_req} | |
+_rewrite = {couch_httpd_rewrite, handle_rewrite_req} | |
diff --git a/share/Makefile.am b/share/Makefile.am | |
index 2352a94..43eb0dc 100644 | |
--- a/share/Makefile.am | |
+++ b/share/Makefile.am | |
@@ -136,5 +136,6 @@ nobase_dist_localdata_DATA = \ | |
www/script/test/config.js \ | |
www/script/test/security_validation.js \ | |
www/script/test/stats.js \ | |
+ www/script/test/rewrite.js \ | |
www/script/test/changes.js \ | |
www/style/layout.css | |
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js | |
index f7c26d0..7f9b12c 100644 | |
--- a/share/www/script/couch_tests.js | |
+++ b/share/www/script/couch_tests.js | |
@@ -75,6 +75,7 @@ loadTest("form_submit.js"); | |
loadTest("security_validation.js"); | |
loadTest("stats.js"); | |
loadTest("rev_stemming.js"); | |
+loadTest("rewrite.js"); | |
function makeDocs(start, end, templateDoc) { | |
var templateDocSrc = templateDoc ? JSON.stringify(templateDoc) : "{}" | |
diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js | |
new file mode 100644 | |
index 0000000..a05b15a | |
--- /dev/null | |
+++ b/share/www/script/test/rewrite.js | |
@@ -0,0 +1,62 @@ | |
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
+// use this file except in compliance with the License. You may obtain a copy | |
+// of the License at | |
+// | |
+// http://www.apache.org/licenses/LICENSE-2.0 | |
+// | |
+// Unless required by applicable law or agreed to in writing, software | |
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
+// License for the specific language governing permissions and limitations under | |
+// the License. | |
+ | |
+couchTests.rewrite = function(debug) { | |
+ | |
+ var db = new CouchDB("test_suite_db"); | |
+ db.deleteDb(); | |
+ db.createDb(); | |
+ if (debug) debugger; | |
+ | |
+ var designDoc = { | |
+ _id:"_design/rewrite_test", | |
+ language: "javascript", | |
+ views: { | |
+ test: { | |
+ map: "function (doc) { emit(doc._id, null) }" | |
+ } | |
+ }, | |
+ rewrites: [{ | |
+ match: ["any_view/<view>/<key>"], | |
+ rewrite: ["_design/rewrite_test/_view/<view>", {key: "<key>"}] | |
+ },{ | |
+ match: ["any_doc/<doc_id>/<*>"], | |
+ rewrite: ["<doc_id>/<*>"] | |
+ },{ | |
+ match: ["design_doc/<name>/<*>"], | |
+ rewrite: ["_design/<name>/<*>"] | |
+ }] | |
+ }; | |
+ | |
+ T(db.save(designDoc).ok); | |
+ T(db.save({_id: "foo"}).ok); | |
+ T(db.save({_id: "foo/bar"}).ok); | |
+ | |
+ var prefix = "/test_suite_db/_design/rewrite_test/_rewrite/"; | |
+ | |
+ // Test any_view rewrite rule | |
+ var xhr = CouchDB.request("GET", prefix+"any_view/test/foo"); | |
+ T(xhr.status == 200); | |
+ T(JSON.parse(xhr.responseText).total_rows == 2); | |
+ T(JSON.parse(xhr.responseText).rows.length == 1); | |
+ | |
+ // Test any_doc rewrite rule | |
+ var xhr = CouchDB.request("GET", prefix+"any_doc/foo"); | |
+ T(xhr.status == 200); | |
+ var xhr = CouchDB.request("GET", prefix+"any_doc/foo%2Fbar"); | |
+ T(xhr.status == 200); | |
+ | |
+ // Test design_doc rewrite rule | |
+ var xhr = CouchDB.request("GET", prefix+"design_doc/rewrite_test/_view/test"); | |
+ T(xhr.status == 200); | |
+ T(JSON.parse(xhr.responseText).total_rows == 2); | |
+}; | |
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am | |
index b34155c..8878344 100644 | |
--- a/src/couchdb/Makefile.am | |
+++ b/src/couchdb/Makefile.am | |
@@ -64,6 +64,7 @@ source_files = \ | |
couch_httpd_view.erl \ | |
couch_httpd_misc_handlers.erl \ | |
couch_httpd_stats_handlers.erl \ | |
+ couch_httpd_rewrite.erl \ | |
couch_key_tree.erl \ | |
couch_log.erl \ | |
couch_os_process.erl \ | |
@@ -82,7 +83,8 @@ source_files = \ | |
couch_view_compactor.erl \ | |
couch_view_updater.erl \ | |
couch_view_group.erl \ | |
- couch_db_updater.erl | |
+ couch_db_updater.erl \ | |
+ webmachine_dispatcher.erl | |
EXTRA_DIST = $(source_files) couch_db.hrl couch_stats.hrl | |
@@ -108,6 +110,7 @@ compiled_files = \ | |
couch_httpd_view.beam \ | |
couch_httpd_misc_handlers.beam \ | |
couch_httpd_stats_handlers.beam \ | |
+ couch_httpd_rewrite.beam \ | |
couch_key_tree.beam \ | |
couch_log.beam \ | |
couch_os_process.beam \ | |
@@ -126,7 +129,8 @@ compiled_files = \ | |
couch_view_compactor.beam \ | |
couch_view_updater.beam \ | |
couch_view_group.beam \ | |
- couch_db_updater.beam | |
+ couch_db_updater.beam \ | |
+ webmachine_dispatcher.beam | |
# doc_base = \ | |
# erlang.png \ | |
diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl | |
new file mode 100644 | |
index 0000000..2de891b | |
--- /dev/null | |
+++ b/src/couchdb/couch_httpd_rewrite.erl | |
@@ -0,0 +1,82 @@ | |
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
+% use this file except in compliance with the License. You may obtain a copy of | |
+% the License at | |
+% | |
+% http://www.apache.org/licenses/LICENSE-2.0 | |
+% | |
+% Unless required by applicable law or agreed to in writing, software | |
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
+% License for the specific language governing permissions and limitations under | |
+% the License. | |
+ | |
+-module(couch_httpd_rewrite). | |
+ | |
+-export([handle_rewrite_req/2]). | |
+ | |
+-include("couch_db.hrl"). | |
+ | |
+handle_rewrite_req(#httpd{ | |
+ mochi_req=MochiReq, | |
+ method='GET', | |
+ path_parts=[DbName, _Design, DesignName, _Rewrite | _Rest] | |
+ }=Req, Db) -> | |
+ DesignId = <<"_design/", DesignName/binary>>, | |
+ % TODO cache the design doc and only reload when it changes | |
+ #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), | |
+ DispatchList = [json_to_dispatch_list(X) || {X} <- proplists:get_value(<<"rewrites">>, Props, [])], | |
+ {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(MochiReq:get(raw_path)), | |
+ [_, _, _, _ | MatchPath] = string:tokens(Path, "/"), | |
+ Dispatched = webmachine_dispatcher:dispatch(string:join(MatchPath, "/"), DispatchList), | |
+ case Dispatched of | |
+ {no_dispatch_match, _} -> | |
+ couch_httpd:send_error(Req, 404, <<"rewrite_error">>, <<"Invalid path.">>); | |
+ {Action, {MatchOpts, Extra}, PathTokens, Bindings, AppRoot, StringPath} -> | |
+ ?LOG_DEBUG("Successful Dispatch: ~p", [{Action, MatchOpts, PathTokens, Bindings, AppRoot, StringPath}]), | |
+ TargetPath = [DbName | lists:flatten([case X of | |
+ '*' -> [?l2b(mochiweb_util:unquote(Y)) || Y <- PathTokens]; | |
+ Y when is_atom(Y) -> [?l2b(mochiweb_util:unquote(proplists:get_value(Y, Bindings, "")))]; | |
+ Y -> [?l2b(mochiweb_util:unquote(Y))] end || X <- MatchOpts])], | |
+ {_Path, QueryString, _Fragment} = mochiweb_util:urlsplit_path(MochiReq:get(raw_path)), | |
+ QueryStringParsed = mochiweb_util:parse_qs(QueryString), | |
+ ?LOG_DEBUG("Internal rewrite to: ~p", [TargetPath]), | |
+ RawPath = string:join([?b2l(X) || X <- TargetPath], "/") ++ "?" ++ mochiweb_util:urlencode(QueryStringParsed ++ [ | |
+ {K, ?b2l(iolist_to_binary(?JSON_ENCODE(V)))} || {K, V} <- replace(Extra, Bindings)]), | |
+ ?LOG_DEBUG("Internal rewrite to: ~p", [RawPath]), | |
+ couch_httpd_db:handle_request(Req#httpd{ | |
+ path_parts=TargetPath, | |
+ mochi_req=mochiweb_request:new(MochiReq:get(socket), MochiReq:get(method), RawPath, MochiReq:get(version), MochiReq:get(headers)) | |
+ }) | |
+ end. | |
+ | |
+replace({X, Y}, Bindings) -> | |
+ {replace(X, Bindings), replace(Y, Bindings)}; | |
+replace([X | Rest], Bindings) -> | |
+ [replace(X, Bindings) | replace(Rest, Bindings)]; | |
+replace(X, Bindings) when is_atom(X) -> | |
+ ?l2b(mochiweb_util:unquote(proplists:get_value(X, Bindings, ""))); | |
+replace(X, Bindings) -> X. | |
+ | |
+json_to_dispatch_list(Props) -> | |
+ {ok, SlashRE} = re:compile(<<"\\/">>), | |
+ [Match | _MatchRest] = proplists:get_value(<<"match">>, Props, []), | |
+ [Rewrite | QueryParams] = proplists:get_value(<<"rewrite">>, Props, []), | |
+ PathTermList = json_to_erlang(re:split(Match, SlashRE)), | |
+ MatchOpts = { | |
+ json_to_erlang(re:split(Rewrite, SlashRE)), | |
+ case QueryParams of | |
+ [{QueryList}] -> json_to_erlang(QueryList); | |
+ _Else -> [] | |
+ end | |
+ }, | |
+ {PathTermList, not_used, MatchOpts}. | |
+ | |
+json_to_erlang([X | Rest]) -> [json_to_erlang(X) | json_to_erlang(Rest)]; | |
+json_to_erlang([]) -> []; | |
+json_to_erlang({K, V}) -> {json_to_erlang(K), json_to_erlang(V)}; | |
+json_to_erlang(<<String/binary>>) -> | |
+ AtomSize = size(String) - 2, | |
+ case String of | |
+ <<"<", Atom:AtomSize/binary, ">">> -> binary_to_atom(Atom, utf8); | |
+ _Else -> ?b2l(String) | |
+ end. | |
diff --git a/src/couchdb/webmachine_dispatcher.erl b/src/couchdb/webmachine_dispatcher.erl | |
new file mode 100644 | |
index 0000000..9749dd0 | |
--- /dev/null | |
+++ b/src/couchdb/webmachine_dispatcher.erl | |
@@ -0,0 +1,112 @@ | |
+%% @author Robert Ahrens <rahrens@basho.com> | |
+%% @author Justin Sheehy <justin@basho.com> | |
+%% @copyright 2007-2009 Basho Technologies | |
+%% | |
+%% Licensed under the Apache License, Version 2.0 (the "License"); | |
+%% you may not use this file except in compliance with the License. | |
+%% You may obtain a copy of the License at | |
+%% | |
+%% http://www.apache.org/licenses/LICENSE-2.0 | |
+%% | |
+%% Unless required by applicable law or agreed to in writing, software | |
+%% distributed under the License is distributed on an "AS IS" BASIS, | |
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+%% See the License for the specific language governing permissions and | |
+%% limitations under the License. | |
+ | |
+%% @doc Module for URL-dispatch by pattern matching. | |
+ | |
+-module(webmachine_dispatcher). | |
+-author('Robert Ahrens <rahrens@basho.com>'). | |
+-author('Justin Sheehy <justin@basho.com>'). | |
+ | |
+-export([dispatch/2]). | |
+ | |
+-define(SEPARATOR, $\/). | |
+-define(MATCH_ALL, '*'). | |
+ | |
+%% @spec dispatch(Path::string(), DispatchList::[matchterm()]) -> | |
+%% dispterm() | dispfail() | |
+%% @doc Interface for URL dispatching. | |
+%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration | |
+dispatch(PathAsString, DispatchList) -> | |
+ Path = string:tokens(PathAsString, [?SEPARATOR]), | |
+ % URIs that end with a trailing slash are implicitly one token | |
+ % "deeper" than we otherwise might think as we are "inside" | |
+ % a directory named by the last token. | |
+ ExtraDepth = case lists:last(PathAsString) == ?SEPARATOR of | |
+ true -> 1; | |
+ _ -> 0 | |
+ end, | |
+ try_binding(DispatchList, Path, ExtraDepth). | |
+ | |
+%% @type matchterm() = {[pathterm()], matchmod(), matchopts()}. | |
+% The dispatch configuration is a list of these terms, and the | |
+% first one whose list of pathterms matches the input path is used. | |
+ | |
+%% @type pathterm() = '*' | string() | atom(). | |
+% A list of pathterms is matched against a '/'-separated input path. | |
+% The '*' pathterm matches all remaining tokens. | |
+% A string pathterm will match a token of exactly the same string. | |
+% Any atom pathterm other than '*' will match any token and will | |
+% create a binding in the result if a complete match occurs. | |
+ | |
+%% @type matchmod() = atom(). | |
+% This atom, if present in a successful matchterm, will appear in | |
+% the resulting dispterm. In Webmachine this is used to name the | |
+% resource module that will handle the matching request. | |
+ | |
+%% @type matchopts() = [term()]. | |
+% This term, if present in a successful matchterm, will appear in | |
+% the resulting dispterm. In Webmachine this is used to provide | |
+% arguments to the resource module handling the matching request. | |
+ | |
+%% @type dispterm() = {matchmod(), matchopts(), pathtokens(), | |
+%% bindings(), approot(), stringpath()}. | |
+ | |
+%% @type pathtokens() = [pathtoken()]. | |
+% This is the list of tokens matched by a trailing '*' pathterm. | |
+ | |
+%% @type pathtoken() = string(). | |
+ | |
+%% @type bindings() = [{bindingterm(),pathtoken()}]. | |
+% This is a proplist of bindings indicated by atom terms in the | |
+% matching spec, bound to the matching tokens in the request path. | |
+ | |
+%% @type approot() = string(). | |
+ | |
+%% @type stringpath() = string(). | |
+% This is the path portion matched by a trailing '*' pathterm. | |
+ | |
+%% @type dispfail() = {no_dispatch_match, pathtokens()}. | |
+ | |
+try_binding([], PathTokens, _) -> | |
+ {no_dispatch_match, PathTokens}; | |
+try_binding([{PathSchema, Mod, Props}|Rest], PathTokens, ExtraDepth) -> | |
+ case bind_path(PathSchema, PathTokens, [], 0) of | |
+ {ok, Remainder, Bindings, Depth} -> | |
+ {Mod, Props, Remainder, Bindings, | |
+ calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)}; | |
+ fail -> | |
+ try_binding(Rest, PathTokens, ExtraDepth) | |
+ end. | |
+ | |
+bind_path([], [], Bindings, Depth) -> | |
+ {ok, [], Bindings, Depth}; | |
+bind_path([?MATCH_ALL], PathRest, Bindings, Depth) when is_list(PathRest) -> | |
+ {ok, PathRest, Bindings, Depth + length(PathRest)}; | |
+bind_path(_, [], _, _) -> | |
+ fail; | |
+bind_path([Token|Rest],[Match|PathRest],Bindings,Depth) when is_atom(Token) -> | |
+ bind_path(Rest, PathRest, [{Token, Match}|Bindings], Depth + 1); | |
+bind_path([Token|Rest], [Token|PathRest], Bindings, Depth) -> | |
+ bind_path(Rest, PathRest, Bindings, Depth + 1); | |
+bind_path(_, _, _, _) -> | |
+ fail. | |
+ | |
+reconstitute([]) -> ""; | |
+reconstitute(UnmatchedTokens) -> string:join(UnmatchedTokens, [?SEPARATOR]). | |
+ | |
+calculate_app_root(1) -> "."; | |
+calculate_app_root(N) when N > 1 -> | |
+ string:join(lists:duplicate(N, ".."), [?SEPARATOR]). |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment