| % 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(mango_doc). |
| |
| |
| -export([ |
| from_bson/1, |
| |
| apply_update/2, |
| update_as_insert/1, |
| has_operators/1, |
| |
| get_field/2, |
| get_field/3, |
| rem_field/2, |
| set_field/3 |
| ]). |
| |
| |
| -include_lib("couch/include/couch_db.hrl"). |
| -include("mango.hrl"). |
| |
| |
| from_bson({Props}) -> |
| DocProps = case lists:keytake(<<"_id">>, 1, Props) of |
| {value, {<<"_id">>, DocId0}, RestProps} -> |
| DocId = case DocId0 of |
| {[{<<"$id">>, Id}]} -> |
| Id; |
| Else -> |
| Else |
| end, |
| [{<<"_id">>, DocId} | RestProps]; |
| false -> |
| Props |
| end, |
| Doc = couch_doc:from_json_obj({DocProps}), |
| case Doc#doc.id of |
| <<"">> -> |
| Doc#doc{id=couch_uuids:new(), revs={0, []}}; |
| _ -> |
| Doc |
| end. |
| |
| |
| apply_update(#doc{body={Props}}=Doc, Update) -> |
| NewProps = apply_update(Props, Update), |
| Doc#doc{body={NewProps}}; |
| apply_update({Props}, {Update}) -> |
| Result = do_update({Props}, Update), |
| case has_operators(Result) of |
| true -> |
| ?MANGO_ERROR(update_leaves_operators); |
| false -> |
| ok |
| end, |
| Result. |
| |
| |
| update_as_insert({Update}) -> |
| NewProps = do_update_to_insert(Update, {[]}), |
| apply_update(NewProps, {Update}). |
| |
| |
| has_operators(#doc{body=Body}) -> |
| has_operators(Body); |
| has_operators({Props}) when is_list(Props) -> |
| has_operators_obj(Props); |
| has_operators(Arr) when is_list(Arr) -> |
| has_operators_arr(Arr); |
| has_operators(Val) when is_atom(Val) -> |
| false; |
| has_operators(Val) when is_number(Val) -> |
| false; |
| has_operators(Val) when is_binary(Val) -> |
| false. |
| |
| |
| has_operators_obj([]) -> |
| false; |
| has_operators_obj([{K, V} | Rest]) -> |
| case K of |
| <<"$", _/binary>> -> |
| true; |
| _ -> |
| case has_operators(V) of |
| true -> |
| true; |
| false -> |
| has_operators_obj(Rest) |
| end |
| end. |
| |
| |
| has_operators_arr([]) -> |
| false; |
| has_operators_arr([V | Rest]) -> |
| case has_operators(V) of |
| true -> |
| true; |
| false -> |
| has_operators_arr(Rest) |
| end. |
| |
| |
| do_update(Props, []) -> |
| Props; |
| do_update(Props, [{Op, Value} | Rest]) -> |
| UpdateFun = update_operator_fun(Op), |
| NewProps = case UpdateFun of |
| undefined -> |
| lists:keystore(Op, 1, Props, {Op, Value}); |
| Fun when is_function(Fun, 2) -> |
| case Value of |
| {ValueProps} -> |
| Fun(Props, ValueProps); |
| _ -> |
| ?MANGO_ERROR({invalid_operand, Op, Value}) |
| end |
| end, |
| do_update(NewProps, Rest). |
| |
| |
| update_operator_fun(<<"$", _/binary>> = Op) -> |
| OperatorFuns = [ |
| % Object operators |
| {<<"$inc">>, fun do_update_inc/2}, |
| {<<"$rename">>, fun do_update_rename/2}, |
| {<<"$setOnInsert">>, fun do_update_set_on_insert/2}, |
| {<<"$set">>, fun do_update_set/2}, |
| {<<"$unset">>, fun do_update_unset/2}, |
| |
| % Array opereators |
| {<<"$addToSet">>, fun do_update_add_to_set/2}, |
| {<<"$pop">>, fun do_update_pop/2}, |
| {<<"$pullAll">>, fun do_update_pull_all/2}, |
| {<<"$pull">>, fun do_update_pull/2}, |
| {<<"$pushAll">>, fun do_update_push_all/2}, |
| {<<"$push">>, fun do_update_push/2}, |
| |
| % Bitwise Operators |
| {<<"$bit">>, fun do_update_bitwise/2} |
| ], |
| case lists:keyfind(Op, 1, OperatorFuns) of |
| {Op, Fun} -> |
| Fun; |
| false -> |
| ?MANGO_ERROR({update_operator_not_supported, Op}) |
| end; |
| update_operator_fun(_) -> |
| undefined. |
| |
| |
| do_update_inc(Props, []) -> |
| Props; |
| do_update_inc(Props, [{Field, Incr} | Rest]) -> |
| if is_number(Incr) -> ok; true -> |
| ?MANGO_ERROR({invalid_increment, Incr}) |
| end, |
| NewProps = case get_field(Props, Field, fun is_number/1) of |
| Value when is_number(Value) -> |
| set_field(Props, Field, Value + Incr); |
| not_found -> |
| set_field(Props, Field, Incr); |
| _ -> |
| Props |
| end, |
| do_update_inc(NewProps, Rest). |
| |
| |
| do_update_rename(Props, []) -> |
| Props; |
| do_update_rename(Props, [{OldField, NewField} | Rest]) -> |
| NewProps = case rem_field(Props, OldField) of |
| {RemProps, OldValue} -> |
| set_field(RemProps, NewField, OldValue); |
| _ -> |
| Props |
| end, |
| do_update_rename(NewProps, Rest). |
| |
| |
| do_update_set_on_insert(Props, _) -> |
| % This is only called during calls to apply_update/2 |
| % which means this isn't an insert, so drop it on |
| % the floor. |
| Props. |
| |
| |
| do_update_set(Props, []) -> |
| Props; |
| do_update_set(Props, [{Field, Value} | Rest]) -> |
| NewProps = set_field(Props, Field, Value), |
| do_update_set(NewProps, Rest). |
| |
| |
| do_update_unset(Props, []) -> |
| Props; |
| do_update_unset(Props, [{Field, _} | Rest]) -> |
| NewProps = case rem_field(Props, Field) of |
| {RemProps, _} -> |
| RemProps; |
| _ -> |
| Props |
| end, |
| do_update_unset(NewProps, Rest). |
| |
| |
| do_update_add_to_set(Props, []) -> |
| Props; |
| do_update_add_to_set(Props, [{Field, NewValue} | Rest]) -> |
| ToAdd = case NewValue of |
| {[{<<"$each">>, NewValues}]} when is_list(NewValues) -> |
| NewValues; |
| {[{<<"$each">>, NewValue}]} -> |
| [NewValue]; |
| Else -> |
| [Else] |
| end, |
| NewProps = case get_field(Props, Field) of |
| OldValues when is_list(OldValues) -> |
| FinalValues = lists:foldl(fun(V, Acc) -> |
| lists:append(Acc, [V]) |
| end, OldValues, ToAdd), |
| set_field(Props, Field, FinalValues); |
| _ -> |
| Props |
| end, |
| do_update_add_to_set(NewProps, Rest). |
| |
| |
| do_update_pop(Props, []) -> |
| Props; |
| do_update_pop(Props, [{Field, Pos} | Rest]) -> |
| NewProps = case get_field(Props, Field) of |
| OldValues when is_list(OldValues) -> |
| NewValues = case Pos > 0 of |
| true -> |
| lists:sublist(OldValues, 1, length(OldValues) - 1); |
| false -> |
| lists:sublist(OldValues, 2, length(OldValues) - 1) |
| end, |
| set_field(Props, Field, NewValues); |
| _ -> |
| Props |
| end, |
| do_update_pop(NewProps, Rest). |
| |
| |
| do_update_pull_all(Props, []) -> |
| Props; |
| do_update_pull_all(Props, [{Field, Values} | Rest]) -> |
| ToRem = case is_list(Values) of |
| true -> Values; |
| false -> [Values] |
| end, |
| NewProps = case get_field(Props, Field) of |
| OldValues when is_list(OldValues) -> |
| NewValues = lists:foldl(fun(ValToRem, Acc) -> |
| % The logic in these filter functions is a bit |
| % subtle. The way to think of this is that we |
| % return true for all elements we want to keep. |
| FilterFun = case has_operators(ValToRem) of |
| true -> |
| fun(A) -> |
| Sel = mango_selector:normalize(ValToRem), |
| not mango_selector:match(A, Sel) |
| end; |
| false -> |
| fun(A) -> A /= ValToRem end |
| end, |
| lists:filter(FilterFun, Acc) |
| end, OldValues, ToRem), |
| set_field(Props, Field, NewValues); |
| _ -> |
| Props |
| end, |
| do_update_add_to_set(NewProps, Rest). |
| |
| |
| do_update_pull(Props, []) -> |
| Props; |
| do_update_pull(Props, [{Field, Value} | Rest]) -> |
| ToRem = case Value of |
| {[{<<"$each">>, Values}]} when is_list(Values) -> |
| Values; |
| {[{<<"$each">>, Value}]} -> |
| [Value]; |
| Else -> |
| [Else] |
| end, |
| NewProps = do_update_pull_all(Props, [{Field, ToRem}]), |
| do_update_pull(NewProps, Rest). |
| |
| |
| do_update_push_all(_, []) -> |
| []; |
| do_update_push_all(Props, [{Field, Values} | Rest]) -> |
| ToAdd = case is_list(Values) of |
| true -> Values; |
| false -> [Values] |
| end, |
| NewProps = case get_field(Props, Field) of |
| OldValues when is_list(OldValues) -> |
| NewValues = OldValues ++ ToAdd, |
| set_field(Props, Field, NewValues); |
| _ -> |
| Props |
| end, |
| do_update_push_all(NewProps, Rest). |
| |
| |
| do_update_push(Props, []) -> |
| Props; |
| do_update_push(Props, [{Field, Value} | Rest]) -> |
| ToAdd = case Value of |
| {[{<<"$each">>, Values}]} when is_list(Values) -> |
| Values; |
| {[{<<"$each">>, Value}]} -> |
| [Value]; |
| Else -> |
| [Else] |
| end, |
| NewProps = do_update_push_all(Props, [{Field, ToAdd}]), |
| do_update_push(NewProps, Rest). |
| |
| |
| |
| do_update_bitwise(Props, []) -> |
| Props; |
| do_update_bitwise(Props, [{Field, Value} | Rest]) -> |
| DoOp = case Value of |
| {[{<<"and">>, Val}]} when is_integer(Val) -> |
| fun(V) -> V band Val end; |
| {[{<<"or">>, Val}]} when is_integer(Val) -> |
| fun(V) -> V bor Val end; |
| _ -> |
| fun(V) -> V end |
| end, |
| NewProps = case get_field(Props, Field, fun is_number/1) of |
| Value when is_number(Value) -> |
| NewValue = DoOp(Value), |
| set_field(Props, Field, NewValue); |
| _ -> |
| Props |
| end, |
| do_update_bitwise(NewProps, Rest). |
| |
| |
| do_update_to_insert([], Doc) -> |
| Doc; |
| do_update_to_insert([{<<"$setOnInsert">>, {FieldProps}}], Doc) -> |
| lists:foldl(fun({Field, Value}, DocAcc) -> |
| set_field(DocAcc, Field, Value) |
| end, Doc, FieldProps); |
| do_update_to_insert([{_, _} | Rest], Doc) -> |
| do_update_to_insert(Rest, Doc). |
| |
| |
| get_field(Props, Field) -> |
| get_field(Props, Field, no_validation). |
| |
| |
| get_field(Props, Field, Validator) when is_binary(Field) -> |
| {ok, Path} = mango_util:parse_field(Field), |
| get_field(Props, Path, Validator); |
| get_field(Props, [], no_validation) -> |
| Props; |
| get_field(Props, [], Validator) -> |
| case (catch Validator(Props)) of |
| true -> |
| Props; |
| _ -> |
| invalid_value |
| end; |
| get_field({Props}, [Name | Rest], Validator) -> |
| case lists:keyfind(Name, 1, Props) of |
| {Name, Value} -> |
| get_field(Value, Rest, Validator); |
| false -> |
| not_found |
| end; |
| get_field(Values, [Name | Rest], Validator) when is_list(Values) -> |
| % Name might be an integer index into an array |
| try |
| Pos = list_to_integer(binary_to_list(Name)), |
| case Pos >= 0 andalso Pos < length(Values) of |
| true -> |
| % +1 because Erlang uses 1 based list indices |
| Value = lists:nth(Pos + 1, Values), |
| get_field(Value, Rest, Validator); |
| false -> |
| bad_path |
| end |
| catch error:badarg -> |
| bad_path |
| end; |
| get_field(_, [_|_], _) -> |
| bad_path. |
| |
| |
| rem_field(Props, Field) when is_binary(Field) -> |
| {ok, Path} = mango_util:parse_field(Field), |
| rem_field(Props, Path); |
| rem_field({Props}, [Name]) -> |
| case lists:keytake(Name, 1, Props) of |
| {value, Value, NewProps} -> |
| {NewProps, Value}; |
| false -> |
| not_found |
| end; |
| rem_field({Props}, [Name | Rest]) -> |
| case lists:keyfind(Name, 1, Props) of |
| {Name, Value} -> |
| case rem_field(Value, Rest) of |
| {NewValue, Ret} -> |
| NewObj = {lists:keystore(Name, 1, Props, {Name, NewValue})}, |
| {NewObj, Ret}; |
| Else -> |
| Else |
| end; |
| false -> |
| not_found |
| end; |
| rem_field(Values, [Name]) when is_list(Values) -> |
| % Name might be an integer index into an array |
| try |
| Pos = list_to_integer(binary_to_list(Name)), |
| case Pos >= 0 andalso Pos < length(Values) of |
| true -> |
| % +1 because Erlang uses 1 based list indices |
| rem_elem(Pos + 1, Values); |
| false -> |
| bad_path |
| end |
| catch error:badarg -> |
| bad_path |
| end; |
| rem_field(Values, [Name | Rest]) when is_list(Values) -> |
| % Name might be an integer index into an array |
| try |
| Pos = list_to_integer(binary_to_list(Name)), |
| case Pos >= 0 andalso Pos < length(Values) of |
| true -> |
| % +1 because Erlang uses 1 based list indices |
| Value = lists:nth(Pos + 1, Values), |
| case rem_field(Value, Rest) of |
| {NewValue, Ret} -> |
| {set_elem(Pos + 1, Values, NewValue), Ret}; |
| Else -> |
| Else |
| end; |
| false -> |
| bad_path |
| end |
| catch error:badarg -> |
| bad_path |
| end; |
| rem_field(_, [_|_]) -> |
| bad_path. |
| |
| |
| set_field(Props, Field, Value) when is_binary(Field) -> |
| {ok, Path} = mango_util:parse_field(Field), |
| set_field(Props, Path, Value); |
| set_field({Props}, [Name], Value) -> |
| {lists:keystore(Name, 1, Props, {Name, Value})}; |
| set_field({Props}, [Name | Rest], Value) -> |
| case lists:keyfind(Name, 1, Props) of |
| {Name, Elem} -> |
| Result = set_field(Elem, Rest, Value), |
| {lists:keystore(Name, 1, Props, {Name, Result})}; |
| false -> |
| Nested = make_nested(Rest, Value), |
| {lists:keystore(Name, 1, Props, {Name, Nested})} |
| end; |
| set_field(Values, [Name], Value) when is_list(Values) -> |
| % Name might be an integer index into an array |
| try |
| Pos = list_to_integer(binary_to_list(Name)), |
| case Pos >= 0 andalso Pos < length(Values) of |
| true -> |
| % +1 because Erlang uses 1 based list indices |
| set_elem(Pos, Values, Value); |
| false -> |
| Values |
| end |
| catch error:badarg -> |
| Values |
| end; |
| set_field(Values, [Name | Rest], Value) when is_list(Values) -> |
| % Name might be an integer index into an array |
| try |
| Pos = list_to_integer(binary_to_list(Name)), |
| case Pos >= 0 andalso Pos < length(Values) of |
| true -> |
| % +1 because Erlang uses 1 based list indices |
| Elem = lists:nth(Pos + 1, Values), |
| Result = set_field(Elem, Rest, Value), |
| set_elem(Pos, Values, Result); |
| false -> |
| Values |
| end |
| catch error:badarg -> |
| Values |
| end; |
| set_field(Value, [_|_], _) -> |
| Value. |
| |
| |
| make_nested([], Value) -> |
| Value; |
| make_nested([Name | Rest], Value) -> |
| {[{Name, make_nested(Rest, Value)}]}. |
| |
| |
| rem_elem(1, [Value | Rest]) -> |
| {Rest, Value}; |
| rem_elem(I, [Item | Rest]) when I > 1 -> |
| {Tail, Value} = rem_elem(I+1, Rest), |
| {[Item | Tail], Value}. |
| |
| |
| set_elem(1, [_ | Rest], Value) -> |
| [Value | Rest]; |
| set_elem(I, [Item | Rest], Value) when I > 1 -> |
| [Item | set_elem(I-1, Rest, Value)]. |