| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- %%--------------------------------------------------------------------
- %% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
- %%
- %% 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(emqx_utils_maps).
- -export([
- best_effort_recursive_sum/3,
- binary_key_map/1,
- binary_string/1,
- deep_convert/3,
- deep_find/2,
- deep_force_put/3,
- deep_get/2,
- deep_get/3,
- deep_merge/2,
- deep_put/3,
- deep_remove/2,
- diff_maps/2,
- if_only_to_toggle_enable/2,
- indent/3,
- jsonable_map/1,
- jsonable_map/2,
- key_comparer/1,
- put_if/4,
- rename/3,
- safe_atom_key_map/1,
- unindent/2,
- unsafe_atom_key_map/1,
- update_if_present/3
- ]).
- -export_type([config_key/0, config_key_path/0]).
- -type config_key() :: atom() | binary() | [byte()].
- -type config_key_path() :: [config_key()].
- -type convert_fun() :: fun((...) -> {K1 :: any(), V1 :: any()} | drop).
- -define(CONFIG_NOT_FOUND_MAGIC, '$0tFound').
- %%-----------------------------------------------------------------
- -spec deep_get(config_key_path(), map()) -> term().
- deep_get(ConfKeyPath, Map) ->
- case deep_get(ConfKeyPath, Map, ?CONFIG_NOT_FOUND_MAGIC) of
- ?CONFIG_NOT_FOUND_MAGIC -> error({config_not_found, ConfKeyPath});
- Res -> Res
- end.
- -spec deep_get(config_key_path(), map(), term()) -> term().
- deep_get(ConfKeyPath, Map, Default) ->
- case deep_find(ConfKeyPath, Map) of
- {not_found, _KeyPath, _Data} -> Default;
- {ok, Data} -> Data
- end.
- -spec deep_find(config_key_path(), map()) ->
- {ok, term()} | {not_found, config_key_path(), term()}.
- deep_find([], Map) ->
- {ok, Map};
- deep_find([Key | KeyPath] = Path, Map) when is_map(Map) ->
- case maps:find(Key, Map) of
- {ok, SubMap} -> deep_find(KeyPath, SubMap);
- error -> {not_found, Path, Map}
- end;
- deep_find(KeyPath, Data) ->
- {not_found, KeyPath, Data}.
- -spec deep_put(config_key_path(), map(), term()) -> map().
- deep_put([], _Map, Data) ->
- Data;
- deep_put([Key | KeyPath], Map, Data) ->
- SubMap = maps:get(Key, Map, #{}),
- Map#{Key => deep_put(KeyPath, SubMap, Data)}.
- %% Like deep_put, but ensures that the key path is present.
- %% If key path is not present in map, creates the keys, until it's present
- %% deep_force_put([x, y, z], #{a => 1}, 0) -> #{a => 1, x => #{y => #{z => 0}}}
- -spec deep_force_put(config_key_path(), map(), term()) -> map().
- deep_force_put([], _Map, Data) ->
- Data;
- deep_force_put([Key | KeyPath] = FullPath, Map, Data) ->
- case Map of
- #{Key := InnerValue} ->
- Map#{Key => deep_force_put(KeyPath, InnerValue, Data)};
- #{} ->
- maps:put(Key, path_to_map(KeyPath, Data), Map);
- _ ->
- path_to_map(FullPath, Data)
- end.
- -spec path_to_map(config_key_path(), term()) -> map().
- path_to_map([], Data) -> Data;
- path_to_map([Key | Tail], Data) -> #{Key => path_to_map(Tail, Data)}.
- -spec deep_remove(config_key_path(), map()) -> map().
- deep_remove([], Map) ->
- Map;
- deep_remove([Key], Map) ->
- maps:remove(Key, Map);
- deep_remove([Key | KeyPath], Map) ->
- case maps:find(Key, Map) of
- {ok, SubMap} when is_map(SubMap) ->
- Map#{Key => deep_remove(KeyPath, SubMap)};
- {ok, _Val} ->
- Map;
- error ->
- Map
- end.
- %% #{a => #{b => 3, c => 2}, d => 4}
- %% = deep_merge(#{a => #{b => 1, c => 2}, d => 4}, #{a => #{b => 3}}).
- -spec deep_merge(map(), map()) -> map().
- deep_merge(BaseMap, NewMap) ->
- NewKeys = maps:keys(NewMap) -- maps:keys(BaseMap),
- MergedBase = maps:fold(
- fun(K, V, Acc) ->
- case maps:find(K, NewMap) of
- error ->
- Acc#{K => V};
- {ok, NewV} when is_map(V), is_map(NewV) ->
- Acc#{K => deep_merge(V, NewV)};
- {ok, NewV} ->
- Acc#{K => NewV}
- end
- end,
- #{},
- BaseMap
- ),
- maps:merge(MergedBase, maps:with(NewKeys, NewMap)).
- -spec deep_convert(any(), convert_fun(), Args :: list()) -> any().
- deep_convert(Map, ConvFun, Args) when is_map(Map) ->
- maps:fold(
- fun(K, V, Acc) ->
- case apply(ConvFun, [K, deep_convert(V, ConvFun, Args) | Args]) of
- drop -> Acc;
- {K1, V1} -> Acc#{K1 => V1}
- end
- end,
- #{},
- Map
- );
- deep_convert(ListV, ConvFun, Args) when is_list(ListV) ->
- [deep_convert(V, ConvFun, Args) || V <- ListV];
- deep_convert(Val, _, _Args) ->
- Val.
- -spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}.
- unsafe_atom_key_map(Map) ->
- convert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end).
- -spec binary_key_map
- (map()) -> map();
- (list()) -> list().
- binary_key_map(Map) ->
- deep_convert(
- Map,
- fun
- (K, V) when is_atom(K) -> {atom_to_binary(K, utf8), V};
- (K, V) when is_binary(K) -> {K, V}
- end,
- []
- ).
- -spec safe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}.
- safe_atom_key_map(Map) ->
- convert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end).
- -spec jsonable_map(map() | list()) -> map() | list().
- jsonable_map(Map) ->
- jsonable_map(Map, fun(K, V) -> {K, V} end).
- jsonable_map(Map, JsonableFun) ->
- deep_convert(Map, fun binary_string_kv/3, [JsonableFun]).
- -spec diff_maps(map(), map()) ->
- #{
- added := map(),
- identical := map(),
- removed := map(),
- changed := #{any() => {OldValue :: any(), NewValue :: any()}}
- }.
- diff_maps(NewMap, OldMap) ->
- InitR = #{identical => #{}, changed => #{}, removed => #{}},
- {Result, RemInNew} =
- lists:foldl(
- fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, RemNewMap}) ->
- Result1 =
- case maps:find(OldK, NewMap) of
- error ->
- Result0#{removed => D#{OldK => OldV}};
- {ok, NewV} when NewV == OldV ->
- Result0#{identical => I#{OldK => OldV}};
- {ok, NewV} ->
- Result0#{changed => U#{OldK => {OldV, NewV}}}
- end,
- {Result1, maps:remove(OldK, RemNewMap)}
- end,
- {InitR, NewMap},
- maps:to_list(OldMap)
- ),
- Result#{added => RemInNew}.
- binary_string_kv(K, V, JsonableFun) ->
- case JsonableFun(K, V) of
- drop -> drop;
- {K1, V1} -> {binary_string(K1), V1}
- end.
- %% [FIXME] this doesn't belong here
- binary_string([]) ->
- [];
- binary_string(Val) when is_list(Val) ->
- case io_lib:printable_unicode_list(Val) of
- true -> unicode:characters_to_binary(Val);
- false -> [binary_string(V) || V <- Val]
- end;
- binary_string(Val) ->
- Val.
- %%---------------------------------------------------------------------------
- convert_keys_to_atom(BinKeyMap, Conv) ->
- deep_convert(
- BinKeyMap,
- fun
- (K, V) when is_atom(K) -> {K, V};
- (K, V) when is_binary(K) -> {Conv(K), V}
- end,
- []
- ).
- %% @doc Sum-merge map values.
- %% For bad merges, ErrorLogger is called to log the key, and value in M2 is ignored.
- best_effort_recursive_sum(M10, M20, ErrorLogger) ->
- FilterF = fun(K, V) ->
- case erlang:is_number(V) of
- true ->
- true;
- false ->
- ErrorLogger(#{failed_to_merge => K, bad_value => V}),
- false
- end
- end,
- M1 = deep_filter(M10, FilterF),
- M2 = deep_filter(M20, FilterF),
- do_best_effort_recursive_sum(M1, M2, ErrorLogger).
- do_best_effort_recursive_sum(M1, M2, ErrorLogger) ->
- F =
- fun(Key, V1, V2) ->
- case {erlang:is_map(V1), erlang:is_map(V2)} of
- {true, true} ->
- do_best_effort_recursive_sum(V1, V2, ErrorLogger);
- {true, false} ->
- ErrorLogger(#{failed_to_merge => Key, bad_value => V2}),
- do_best_effort_recursive_sum(V1, #{}, ErrorLogger);
- {false, true} ->
- ErrorLogger(#{failed_to_merge => Key, bad_value => V1}),
- do_best_effort_recursive_sum(V2, #{}, ErrorLogger);
- {false, false} ->
- true = is_number(V1),
- true = is_number(V2),
- V1 + V2
- end
- end,
- maps:merge_with(F, M1, M2).
- deep_filter(M, F) when is_map(M) ->
- %% maps:filtermap is not available before OTP 24
- maps:from_list(
- lists:filtermap(
- fun
- ({K, V}) when is_map(V) ->
- {true, {K, deep_filter(V, F)}};
- ({K, V}) ->
- F(K, V) andalso {true, {K, V}}
- end,
- maps:to_list(M)
- )
- ).
- if_only_to_toggle_enable(OldConf, Conf) ->
- #{added := Added, removed := Removed, changed := Updated} =
- emqx_utils_maps:diff_maps(OldConf, Conf),
- case {Added, Removed, Updated} of
- {Added, Removed, #{enable := _} = Updated} when
- map_size(Added) =:= 0,
- map_size(Removed) =:= 0,
- map_size(Updated) =:= 1
- ->
- true;
- {_, _, _} ->
- false
- end.
- %% Like `maps:update_with', but does nothing if key does not exist.
- update_if_present(Key, Fun, Map) ->
- case Map of
- #{Key := Val} ->
- Map#{Key := Fun(Val)};
- _ ->
- Map
- end.
- put_if(Acc, K, V, true) ->
- Acc#{K => V};
- put_if(Acc, _K, _V, false) ->
- Acc.
- rename(OldKey, NewKey, Map) ->
- case maps:find(OldKey, Map) of
- {ok, Value} ->
- maps:put(NewKey, Value, maps:remove(OldKey, Map));
- error ->
- Map
- end.
- -spec key_comparer(K) -> fun((M, M) -> boolean()) when M :: #{K => _V}.
- key_comparer(K) ->
- fun
- (#{K := V1}, #{K := V2}) ->
- V1 < V2;
- (#{K := _}, _) ->
- false;
- (_, #{K := _}) ->
- true;
- (M1, M2) ->
- M1 < M2
- end.
- -spec indent(term(), [term()], map()) -> map().
- indent(IndentKey, PickKeys, Map) ->
- maps:put(
- IndentKey,
- maps:with(PickKeys, Map),
- maps:without(PickKeys, Map)
- ).
- -spec unindent(term(), map()) -> map().
- unindent(Key, Map) ->
- deep_merge(
- maps:remove(Key, Map),
- maps:get(Key, Map, #{})
- ).
|