Skip to content

Instantly share code, notes, and snippets.

@jasondavies
Created July 4, 2009 21:24
Show Gist options
  • Save jasondavies/140738 to your computer and use it in GitHub Desktop.
Save jasondavies/140738 to your computer and use it in GitHub Desktop.
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