blob: 14620a57840a904e0645048a5692b576399faf9d [file] [log] [blame]
% 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_views_http).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_views/include/couch_views.hrl").
-export([
parse_body_and_query/2,
parse_body_and_query/3,
parse_params/2,
parse_params/4,
row_to_obj/1,
row_to_obj/2,
view_cb/2,
paginated/5,
paginated/6,
transform_row/1
]).
-define(BOOKMARK_VSN, 1).
parse_body_and_query(#httpd{method = 'POST'} = Req, Keys) ->
Props = chttpd:json_body_obj(Req),
parse_body_and_query(Req, Props, Keys);
parse_body_and_query(Req, Keys) ->
parse_params(
chttpd:qs(Req),
Keys,
#mrargs{
keys = Keys,
group = undefined,
group_level = undefined
},
[keep_group_level]
).
parse_body_and_query(Req, {Props}, Keys) ->
Args = #mrargs{keys = Keys, group = undefined, group_level = undefined},
BodyArgs = parse_params(Props, Keys, Args, [decoded]),
parse_params(chttpd:qs(Req), Keys, BodyArgs, [keep_group_level]).
parse_params(#httpd{} = Req, Keys) ->
parse_params(chttpd:qs(Req), Keys);
parse_params(Props, Keys) ->
Args = #mrargs{},
parse_params(Props, Keys, Args).
parse_params(Props, Keys, Args) ->
parse_params(Props, Keys, Args, []).
parse_params([{"bookmark", Bookmark}], _Keys, #mrargs{}, _Options) ->
bookmark_decode(Bookmark);
parse_params(Props, Keys, #mrargs{} = Args, Options) ->
case couch_util:get_value("bookmark", Props, nil) of
nil ->
ok;
_ ->
throw({bad_request, "Cannot use `bookmark` with other options"})
end,
couch_views_http_util:parse_params(Props, Keys, Args, Options).
row_to_obj(Row) ->
Id = couch_util:get_value(id, Row),
row_to_obj(Id, Row).
row_to_obj(Id, Row) ->
couch_views_http_util:row_to_obj(Id, Row).
view_cb(Msg, #vacc{paginated = false} = Acc) ->
couch_views_http_util:view_cb(Msg, Acc);
view_cb(Msg, #vacc{paginated = true} = Acc) ->
paginated_cb(Msg, Acc).
paginated_cb({row, Row}, #vacc{buffer = Buf} = Acc) ->
{ok, Acc#vacc{buffer = [row_to_obj(Row) | Buf]}};
paginated_cb({error, Reason}, #vacc{} = _Acc) ->
throw({error, Reason});
paginated_cb(complete, #vacc{buffer = Buf} = Acc) ->
{ok, Acc#vacc{buffer = lists:reverse(Buf)}};
paginated_cb({meta, Meta}, #vacc{} = VAcc) ->
MetaMap = lists:foldl(
fun(MetaData, Acc) ->
case MetaData of
{_Key, undefined} ->
Acc;
{total, Value} ->
maps:put(total_rows, Value, Acc);
{Key, Value} ->
maps:put(list_to_binary(atom_to_list(Key)), Value, Acc)
end
end,
#{},
Meta
),
{ok, VAcc#vacc{meta = MetaMap}}.
paginated(Req, EtagTerm, #mrargs{page_size = PageSize} = Args, KeyFun, Fun) ->
Etag = couch_httpd:make_etag(EtagTerm),
chttpd:etag_respond(Req, Etag, fun() ->
hd(do_paginated(PageSize, [Args], KeyFun, Fun))
end).
paginated(Req, EtagTerm, PageSize, QueriesArgs, KeyFun, Fun) when is_list(QueriesArgs) ->
Etag = couch_httpd:make_etag(EtagTerm),
chttpd:etag_respond(Req, Etag, fun() ->
Results = do_paginated(PageSize, QueriesArgs, KeyFun, Fun),
#{results => Results}
end).
do_paginated(PageSize, QueriesArgs, KeyFun, Fun) when is_list(QueriesArgs) ->
{_N, Results} = lists:foldl(
fun(Args0, {Limit, Acc}) ->
case Limit > 0 of
true ->
{OriginalLimit, Args} = set_limit(Args0#mrargs{page_size = Limit}),
{Meta, Items} = Fun(Args),
Result0 = maybe_add_next_bookmark(
OriginalLimit, PageSize, Args, Meta, Items, KeyFun
),
Result = maybe_add_previous_bookmark(Args, Result0, KeyFun),
{Limit - length(maps:get(rows, Result)), [Result | Acc]};
false ->
Bookmark = bookmark_encode(Args0),
Result = #{
rows => [],
next => Bookmark
},
{Limit, [Result | Acc]}
end
end,
{PageSize, []},
QueriesArgs
),
lists:reverse(Results).
maybe_add_next_bookmark(OriginalLimit, PageSize, Args0, Response, Items, KeyFun) ->
#mrargs{
page_size = RequestedLimit,
extra = Extra0
} = Args0,
case check_completion(OriginalLimit, RequestedLimit, Items) of
{Rows, nil} ->
maps:merge(Response, #{
rows => Rows
});
{Rows, Next} ->
{FirstId, FirstKey} = first_key(KeyFun, Rows),
{NextId, NextKey} = KeyFun(Next),
Extra1 = lists:keystore(fid, 1, Extra0, {fid, FirstId}),
Extra2 = lists:keystore(fk, 1, Extra1, {fk, FirstKey}),
Args = Args0#mrargs{
page_size = PageSize,
start_key = NextKey,
start_key_docid = NextId,
extra = Extra2
},
Bookmark = bookmark_encode(Args),
maps:merge(Response, #{
rows => Rows,
next => Bookmark
})
end.
maybe_add_previous_bookmark(#mrargs{extra = Extra} = Args, #{rows := Rows} = Result, KeyFun) ->
StartKey = couch_util:get_value(fk, Extra),
StartId = couch_util:get_value(fid, Extra),
case {{StartId, StartKey}, first_key(KeyFun, Rows)} of
{{undefined, undefined}, {_, _}} ->
Result;
{{_, _}, {undefined, undefined}} ->
Result;
{{StartId, _}, {StartId, _}} ->
Result;
{{undefined, StartKey}, {undefined, StartKey}} ->
Result;
{{StartId, StartKey}, {EndId, EndKey}} ->
Bookmark = bookmark_encode(
Args#mrargs{
start_key = StartKey,
start_key_docid = StartId,
end_key = EndKey,
end_key_docid = EndId,
inclusive_end = false
}
),
maps:put(previous, Bookmark, Result)
end.
first_key(_KeyFun, []) ->
{undefined, undefined};
first_key(KeyFun, [First | _]) ->
KeyFun(First).
set_limit(#mrargs{page_size = PageSize, limit = Limit} = Args) when
is_integer(PageSize) andalso Limit > PageSize
->
{Limit, Args#mrargs{limit = PageSize + 1}};
set_limit(#mrargs{page_size = PageSize, limit = Limit} = Args) when
is_integer(PageSize)
->
{Limit, Args#mrargs{limit = Limit + 1}}.
check_completion(OriginalLimit, RequestedLimit, Items) when
is_integer(OriginalLimit) andalso OriginalLimit =< RequestedLimit
->
{Rows, _} = split(OriginalLimit, Items),
{Rows, nil};
check_completion(_OriginalLimit, RequestedLimit, Items) ->
split(RequestedLimit, Items).
split(Limit, Items) when length(Items) > Limit ->
case lists:split(Limit, Items) of
{Head, [NextItem | _]} ->
{Head, NextItem};
{Head, []} ->
{Head, nil}
end;
split(_Limit, Items) ->
{Items, nil}.
bookmark_encode(Args0) ->
Defaults = #mrargs{},
{RevTerms, Mask, _} = lists:foldl(
fun(Value, {Acc, Mask, Idx}) ->
case element(Idx, Defaults) of
Value ->
{Acc, Mask, Idx + 1};
_Default when Idx == #mrargs.bookmark ->
{Acc, Mask, Idx + 1};
_Default ->
% Its `(Idx - 1)` because the initial `1`
% value already accounts for one bit.
{[Value | Acc], (1 bsl (Idx - 1)) bor Mask, Idx + 1}
end
end,
{[], 0, 1},
tuple_to_list(Args0)
),
Terms = lists:reverse(RevTerms),
TermBin = term_to_binary(Terms, [compressed, {minor_version, 2}]),
MaskBin = binary:encode_unsigned(Mask),
RawBookmark = <<?BOOKMARK_VSN, MaskBin/binary, TermBin/binary>>,
couch_util:encodeBase64Url(RawBookmark).
bookmark_decode(Bookmark) ->
try
RawBin = couch_util:decodeBase64Url(Bookmark),
<<?BOOKMARK_VSN, MaskBin:4/binary, TermBin/binary>> = RawBin,
Mask = binary:decode_unsigned(MaskBin),
Index = mask_to_index(Mask, 1, []),
Terms = binary_to_term(TermBin, [safe]),
lists:foldl(
fun({Idx, Value}, Acc) ->
setelement(Idx, Acc, Value)
end,
#mrargs{},
lists:zip(Index, Terms)
)
catch
_:_ ->
throw({bad_request, <<"Invalid bookmark">>})
end.
mask_to_index(0, _Pos, Acc) ->
lists:reverse(Acc);
mask_to_index(Mask, Pos, Acc) when is_integer(Mask), Mask > 0 ->
NewAcc =
case Mask band 1 of
0 -> Acc;
1 -> [Pos | Acc]
end,
mask_to_index(Mask bsr 1, Pos + 1, NewAcc).
transform_row(#view_row{value = {[{reduce_overflow_error, Msg}]}}) ->
{row, [{key, null}, {id, error}, {value, reduce_overflow_error}, {reason, Msg}]};
transform_row(#view_row{key = Key, id = reduced, value = Value}) ->
{row, [{key, Key}, {value, Value}]};
transform_row(#view_row{key = Key, id = undefined}) ->
{row, [{key, Key}, {id, error}, {value, not_found}]};
transform_row(#view_row{key = Key, id = Id, value = Value, doc = undefined}) ->
{row, [{id, Id}, {key, Key}, {value, Value}]};
transform_row(#view_row{key = Key, id = _Id, value = _Value, doc = {error, Reason}}) ->
{row, [{id, error}, {key, Key}, {value, Reason}]};
transform_row(#view_row{key = Key, id = Id, value = Value, doc = Doc}) ->
{row, [{id, Id}, {key, Key}, {value, Value}, {doc, Doc}]}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
bookmark_encode_decode_test() ->
?assertEqual(
#mrargs{page_size = 5},
bookmark_decode(bookmark_encode(#mrargs{page_size = 5}))
),
Randomized = lists:foldl(
fun(Idx, Acc) ->
if
Idx == #mrargs.bookmark -> Acc;
true -> setelement(Idx, Acc, couch_uuids:random())
end
end,
#mrargs{},
lists:seq(1, record_info(size, mrargs))
),
?assertEqual(
Randomized,
bookmark_decode(bookmark_encode(Randomized))
).
check_completion_test() ->
?assertEqual(
{[], nil},
check_completion(100, 1, [])
),
?assertEqual(
{[1], nil},
check_completion(100, 1, [1])
),
?assertEqual(
{[1], 2},
check_completion(100, 1, [1, 2])
),
?assertEqual(
{[1], 2},
check_completion(100, 1, [1, 2, 3])
),
?assertEqual(
{[1, 2], nil},
check_completion(100, 3, [1, 2])
),
?assertEqual(
{[1, 2, 3], nil},
check_completion(100, 3, [1, 2, 3])
),
?assertEqual(
{[1, 2, 3], 4},
check_completion(100, 3, [1, 2, 3, 4])
),
?assertEqual(
{[1, 2, 3], 4},
check_completion(100, 3, [1, 2, 3, 4, 5])
),
?assertEqual(
{[1], nil},
check_completion(1, 1, [1])
),
?assertEqual(
{[1, 2], nil},
check_completion(2, 3, [1, 2])
),
?assertEqual(
{[1, 2], nil},
check_completion(2, 3, [1, 2, 3])
),
?assertEqual(
{[1, 2], nil},
check_completion(2, 3, [1, 2, 3, 4, 5])
),
ok.
-endif.