Skip to content

Instantly share code, notes, and snippets.

@dch
Forked from evanmiller/rr_db.erl
Created January 10, 2012 15:07
Show Gist options
  • Save dch/1589509 to your computer and use it in GitHub Desktop.
Save dch/1589509 to your computer and use it in GitHub Desktop.
code from @evanmiller for couchdb with chicago boss (Chicagosaurus Rex)
-module(rr_db, [Server, PhotoRoot]).
-define(MIME_TYPE, "application/json").
-compile(export_all).
init() ->
inets:start(),
inets:start(httpc, [{profile, database}]).
reset() ->
case http:request('delete', {Server, []}, [], []) of
{ok, _} ->
case http:request('put', {Server, [], [], []}, [], []) of
{ok, _} ->
create_view(user),
create_view(association),
ok;
_ ->
error
end;
_ ->
error
end.
dump() ->
case http:request(Server ++ "_all_docs") of
{ok, {Status, _Headers, Body}} ->
{ok, Body};
Error ->
Error
end.
fetch(DocId) ->
case http:request(Server ++ rr_util:quote_plus(DocId)) of
{ok, {Status, _Headers, Body}} ->
case Status of
{_, 200, _} ->
{struct, PropList} = mochijson2:decode(Body),
{ok, rr_util:atomize_proplist(PropList)};
{_, 404, _} ->
{error, not_found}
end;
Error ->
Error
end.
create_view(user) ->
Functions = [{<<"by_url">>, <<"function(doc) { if (doc.TYPE == 'user') { map(doc.url, doc); } }">>},
{<<"by_email">>, <<"function(doc) { if (doc.TYPE == 'user') { map(doc.email, doc); } }">>}],
create_view("user", Functions);
create_view(association) ->
Functions = [{<<"full">>, <<"function(doc) { for (var i in doc) { if (doc.TYPE && i.indexOf('_id', 1) != -1) { map([doc.TYPE, i, doc[i]], doc); } } }">>}],
create_view("association", Functions).
create_view(ViewName, Functions) ->
JsonObject = list_to_binary(mochijson2:encode(
{struct, [ {<<"views">>, {struct, Functions}}]})),
case http:request('put', {Server ++ rr_util:quote_plus("_design/" ++ ViewName),
[], ?MIME_TYPE, JsonObject}, [], []) of
{ok, {Status, _Headers, Body}} ->
case Status of
{_, 201, _} ->
{struct, ResultProps} = mochijson2:decode(Body),
{ok, proplists:get_value(<<"id">>, ResultProps),
proplists:get_value(<<"rev">>, ResultProps)};
Other ->
Other
end;
Other ->
Other
end.
lookup(Type, {Key, Value}) ->
case http:request(rr_util:concat([Server, "_view/", Type, "/by_", Key, "?key=",
rr_util:quote_plus(rr_util:concat(["\"", Value, "\""]))])) of
{ok, {Status, _Headers, Body}} ->
{ok, Rows} = process_rows(Status, Body),
case length(Rows) of
0 ->
undefined;
_N ->
lists:nth(1, Rows)
end;
Other ->
Other
end.
find(Type, {Key, Value}, [cache]) ->
case rr_cache:fetch({Type, Key, Value}) of
{ok, Result} ->
Result;
not_found ->
Result = find(Type, {Key, Value}),
rr_cache:store({Type, Key, Value}, Result),
Result
end.
find(Type, {Key, Value}) ->
case http:request(lists:concat([Server, "_view/association/full?key=",
rr_util:quote_plus(rr_util:concat(["[\"", Type, "\", \"", Key, "\", \"", Value, "\"]"]))])) of
{ok, {Status, _Headers, Body}} ->
process_rows(Status, Body);
Other ->
Other
end.
process_rows(Status, Body) ->
case Status of
{_, 200, _} ->
{struct, ResultSet} = mochijson2:decode(Body),
Rows = proplists:get_value(<<"rows">>, ResultSet),
{ok, lists:map(
fun({struct, Row}) ->
{struct, PropList} = proplists:get_value(<<"value">>, Row),
rr_util:atomize_proplist(PropList)
end, Rows)};
Other ->
Other
end.
create(PropList) ->
PropList1 = lists:map(fun ({Key, Val}) -> {rr_util:to_binary(Key), rr_util:to_binary(Val)} end, PropList),
JsonObject = list_to_binary(mochijson2:encode({struct, PropList1})),
case http:request('post', {Server, [], ?MIME_TYPE, JsonObject}, [], []) of
{ok, {Status, _Headers, Body}} ->
case Status of
{_, 201, _} ->
{struct, ResultProps} = mochijson2:decode(Body),
{ok, proplists:get_value(<<"id">>, ResultProps),
proplists:get_value(<<"rev">>, ResultProps)};
{_, 404, _} ->
{error, "Database does not exist!"};
{_, 500, _} ->
{error, "Internal server error; check server logs", JsonObject};
{_, Number, _} ->
{error, "Unhandled HTTP code " ++ integer_to_list(Number)}
end;
Error ->
Error
end.
update(DocId, PropList) ->
case current_revision(DocId) of
{ok, Revision} ->
update(DocId, Revision, PropList);
Error ->
Error
end.
update(DocId, Revision, PropList) ->
PropList1 = rr_util:binarize_proplist(PropList) ++
[{<<"_id">>, rr_util:to_binary(DocId)},
{<<"_rev">>, rr_util:to_binary(Revision)}],
JsonObject = list_to_binary(mochijson2:encode({struct, PropList1})),
case http:request('put', {Server ++ rr_util:quote_plus(DocId), [], ?MIME_TYPE, JsonObject}, [], []) of
{ok, {Status, _Headers, Body}} ->
case Status of
{_, 201, _} ->
{struct, ResultProps} = mochijson2:decode(Body),
{ok, proplists:get_value(<<"rev">>, ResultProps)};
{_, 409, _} ->
{error, "Edit conflict"};
{_, Number, _} ->
{error, "Unhandled HTTP code " ++ integer_to_list(Number)}
end;
Error ->
Error
end.
delete(DocId) when is_binary(DocId) ->
delete(binary_to_list(DocId));
delete(DocId) ->
case current_revision(DocId) of
{ok, Revision} ->
delete(DocId, Revision);
Error ->
Error
end.
delete(DocId, Revision) when is_integer(Revision) ->
delete(DocId, integer_to_list(Revision));
delete(DocId, Revision) ->
case http:request('delete',
{Server ++ rr_util:quote_plus(DocId)
++ "?rev=" ++ rr_util:quote_plus(Revision), []}, [], []) of
{ok, {Status, _Headers, Body}} ->
case Status of
{_, 202, _} ->
{struct, ResultProps} = mochijson2:decode(Body),
{ok, proplists:get_value(<<"rev">>, ResultProps)};
{_, 409, _} ->
{error, edit_conflict};
{_, Number, _} ->
{error, "Unhandled HTTP code " ++ integer_to_list(Number) ++ " " ++ Body}
end;
Error ->
Error
end.
parse_upload(Req, Type, Doc) ->
mochiweb_multipart:parse_multipart_request(Req,
fun(Part) ->
handle_upload(Part, {{Type, Doc, 0}, false})
end).
parse_upload_in_memory(Req) ->
mochiweb_multipart:parse_multipart_request(Req,
fun(Part) ->
handle_upload_in_memory(Part, [])
end).
delete_file(Id) ->
file:delete(rr_util:concat([PhotoRoot, Id])).
handle_upload({headers, Headers}, {{Type, Props, UploadCount}, _}) ->
case Headers of
[{"content-disposition", {"form-data", [{"name", _}, {"filename", [_NonEmpty|_Filename]}]}}|_] ->
{ok, DocId, _Rev} = create([
{"TYPE", Type},
{"upload_number", integer_to_list(UploadCount)}] ++ Props),
{ok, FileDev} = file:open(rr_util:concat(
[PhotoRoot, binary_to_list(DocId)]), [raw,write]),
fun(Part) -> handle_upload(Part, {{Type, Props, UploadCount + 1}, FileDev}) end;
_ ->
fun(Part) -> handle_upload(Part, {{Type, Props, UploadCount}, false}) end
end;
handle_upload({body, _Data}, {Info, false}) ->
fun(Part) -> handle_upload(Part, {Info, false}) end;
handle_upload({body, Data}, {Info, FileDev}) ->
file:write(FileDev, Data),
fun(Part) -> handle_upload(Part, {Info, FileDev}) end;
handle_upload(body_end, {Info, false}) ->
fun(Part) -> handle_upload(Part, {Info, false}) end;
handle_upload(body_end, {Info, FileDev}) ->
file:close(FileDev),
fun(Part) -> handle_upload(Part, {Info, false}) end;
handle_upload(eof, _) ->
ok.
handle_upload_in_memory({headers, _Header}, RawData) ->
fun(N) -> handle_upload_in_memory(N, RawData) end;
handle_upload_in_memory({body, Data}, RawData) ->
fun(N) -> handle_upload_in_memory(N, RawData ++ binary_to_list(Data)) end;
handle_upload_in_memory(body_end, RawData) ->
fun(N) -> handle_upload_in_memory(N, RawData) end;
handle_upload_in_memory(eof, RawData) ->
RawData.
current_revision(DocId) ->
case fetch(DocId) of
{ok, PropList} ->
{ok, proplists:get_value('_rev', PropList)};
Error ->
Error
end.
-module(rr_util).
-compile(export_all).
% more tolerant versions of list_to_integer and list_to_float
integerize("-" ++ Input) ->
integerize(Input, 0, negative);
integerize(Input) ->
integerize(Input, 0, positive).
integerize([C|Rest], Number, Sign) when C >= $0 andalso C =< $9->
integerize(Rest, Number * 10 + C - $0, Sign);
integerize(_, Number, negative) ->
-1 * Number;
integerize(_, Number, positive) ->
Number.
floatize(Input) ->
floatize(Input, 0.0).
floatize([C|Rest], Number) when C >= $0 andalso C =< $9 ->
floatize(Rest, Number * 10 + C - $0);
floatize([$.|Rest], Number) ->
floatize(Rest, Number, 0.1);
floatize(_, Number) ->
Number.
floatize([C|Rest], Number, DecimalPlace) when C >= $0 andalso C =< $9 ->
floatize(Rest, Number + (C - $0) * DecimalPlace, DecimalPlace / 10);
floatize(_, Number, _) ->
Number.
calendarize(Input) ->
{Month, Day, Year} = parse_date(Input),
list_to_integer(Year) * 100 * 100 +
list_to_integer(Month) * 100 +
list_to_integer(Day).
increment_property(undefined, Group) ->
Group;
increment_property(Property, Group) ->
case lists:keysearch(Property, 1, Group) of
false ->
[{Property, "1"} | Group];
{value, {_, N}} ->
lists:keyreplace(Property, 1, Group,
{Property, integer_to_list(list_to_integer(N) + 1)})
end.
mark_group(NarrowBy, NarrowTo, ThisGroup, Group) when is_list(NarrowTo) ->
mark_group(NarrowBy, list_to_binary(NarrowTo), ThisGroup, Group);
mark_group(NarrowBy, NarrowTo, ThisGroup, Group) ->
lists:map(fun({Name, Count}) ->
{Name, Count, NarrowBy =:= ThisGroup andalso NarrowTo =:= Name}
end, Group).
rows_of(Number, List) ->
lists:reverse(rows_of(Number, List, [])).
rows_of(Number, List, Acc) ->
case length(List) > Number of
true ->
rows_of(Number, lists:nthtail(Number, List), [lists:sublist(List, Number) | Acc]);
false ->
[lists:sublist(List, Number) | Acc]
end.
set_return_cookies(Url, Text) when is_binary(Text) ->
set_return_cookies(Url, binary_to_list(Text));
set_return_cookies(Url, Text) ->
[mochiweb_cookies:cookie("return",
mochiweb_util:quote_plus(Url), [{path, "/"}]),
mochiweb_cookies:cookie("return_text",
mochiweb_util:quote_plus(Text), [{path, "/"}])].
get_return_cookies(Req) ->
[{return, case Req:get_cookie_value("return") of
undefined -> "/train/collection";
Return -> mochiweb_util:unquote(Return)
end},
{return_text, case Req:get_cookie_value("return_text") of
undefined -> "collection";
Text -> mochiweb_util:unquote(Text) end}].
first(_, _, []) ->
undefined;
first(Attribute, Value, [H|T]) ->
case proplists:get_value(Attribute, H) of
Value ->
H;
_ ->
first(Attribute, Value, T)
end.
concat([H|T]) when is_atom(H) ->
atom_to_list(H) ++ concat(T);
concat([H|T]) when is_binary(H) ->
binary_to_list(H) ++ concat(T);
concat([H|T]) when is_list(H) ->
H ++ concat(T);
concat(Other) ->
Other.
parse_date(Input) when is_binary(Input) ->
parse_date(binary_to_list(Input));
parse_date(Input) when is_list(Input) ->
Tokens = string:tokens(Input, "/"),
case Tokens of
[Year] ->
{"0", "0", Year};
[Month,Year] ->
{Month, "0", Year};
[Month,Day,Year] ->
{Month, Day, Year};
_ ->
{"0", "0", "0"}
end;
parse_date(_) ->
{"0", "0", "0"}.
atomize_proplist(PropList) ->
lists:map(fun
({K, {struct, Props}}) ->
{to_atom(K), {struct, atomize_proplist(Props)}};
({K, V}) ->
{to_atom(K), to_binary(V)}
end, PropList).
sort_proplist(PropList) ->
lists:sort(fun({KeyA, _}, {KeyB, _}) -> KeyA =< KeyB end, PropList).
binarize_proplist(PropList) ->
lists:map(fun({K, V}) -> {to_binary(K), to_binary(V)} end, PropList).
to_atom(Atom) when is_atom(Atom) ->
Atom;
to_atom(List) when is_list(List) ->
list_to_atom(List);
to_atom(Bin) when is_binary(Bin) ->
to_atom(binary_to_list(Bin)).
to_binary(Atom) when is_atom(Atom) ->
to_binary(atom_to_list(Atom));
to_binary(List) when is_list(List) ->
list_to_binary(List);
to_binary(Bin) ->
Bin.
to_integer(List) when is_list(List) ->
list_to_integer(List);
to_integer(Bin) when is_binary(Bin) ->
to_integer(binary_to_list(Bin)).
quote_plus(Bin) when is_binary(Bin) ->
quote_plus(binary_to_list(Bin));
quote_plus(List) ->
mochiweb_util:quote_plus(List).
format_now(Time) ->
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Time),
integer_to_list(Year) ++ "." ++ integer_to_list(Month) ++ "." ++ integer_to_list(Day) ++
" "++integer_to_list(Hour)++":"++integer_to_list(Minute)++":"++integer_to_list(Second).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment