├── .githooks └── pre-commit ├── .github └── workflows │ └── continous_integration.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── rebar.config ├── rebar.lock ├── src ├── mapz.app.src └── mapz.erl └── test └── mapz_tests.erl /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rebar3 fmt --check 4 | rebar3 xref 5 | rebar3 dialyzer 6 | rebar3 eunit 7 | -------------------------------------------------------------------------------- /.github/workflows/continous_integration.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - workflow_dispatch 7 | 8 | jobs: 9 | ci: 10 | runs-on: ubuntu-latest 11 | name: Erlang ${{matrix.otp}} / rebar ${{matrix.rebar3}} 12 | strategy: 13 | matrix: 14 | otp: ['25', '26', '27'] 15 | rebar3: ['3'] 16 | steps: 17 | 18 | - uses: actions/checkout@v2 19 | 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp}} 23 | rebar3-version: ${{matrix.rebar3}} 24 | 25 | - uses: actions/cache@v2 26 | env: 27 | cache-name: rebar3 28 | with: 29 | path: | 30 | ~/.cache/rebar3 31 | _build 32 | key: ci-${{runner.os}}-${{env.cache-name}}-otp_${{matrix.otp}}-rebar_${{matrix.rebar3}}-${{hashFiles('rebar.lock')}} 33 | restore-keys: | 34 | ci-${{runner.os}}-${{env.cache-name}}-otp_${{matrix.otp}}-rebar_${{matrix.rebar3}} 35 | ci-${{runner.os}}-${{env.cache-name}}-otp_${{matrix.otp}} 36 | 37 | - name: Upgrade all plugins 38 | run: rebar3 plugins upgrade --all 39 | 40 | - name: Compile 41 | run: rebar3 do clean, compile 42 | 43 | - name: Check formatting 44 | if: ${{ fromJson(matrix.otp) >= 27 }} 45 | run: rebar3 fmt --check 46 | 47 | - name: Analyze 48 | run: rebar3 do xref, dialyzer 49 | 50 | - name: Test 51 | run: rebar3 do eunit, ct 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | rebar3.crashdump 18 | 19 | doc/* 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ### Changed 12 | 13 | - **Breaking!** Changed the error `{badvalue, PartialPath}` to `{badvalue, 14 | PartialPath, Value}` 15 | 16 | ### Removed 17 | 18 | - **Breaking!** Removed official support for Erlang 23 and 24. 19 | 20 | ## [2.4.0] - 2023-11-13 21 | 22 | ### Added 23 | 24 | - New [`deep_intersect/2`][deep_intersect-2] function 25 | - New [`deep_intersect_with/3`][deep_intersect_with-3] function 26 | 27 | [deep_intersect-2]: https://hexdocs.pm/mapz/mapz.html#deep_intersect-2 28 | [deep_intersect_with-3]: https://hexdocs.pm/mapz/mapz.html#deep_intersect_with-3 29 | 30 | ### Fixed 31 | 32 | - [`deep_merge_with/3`][deep_merge_with-3] now exits with `badarg` in case the 33 | fun is not valid. 34 | 35 | ## [2.3.0] - 2022-06-08 36 | 37 | ### Added 38 | 39 | - New [`inverse/2`][inverse-2] function 40 | - New [`deep_search/2`][deep_search-2] function 41 | 42 | [inverse-2]: https://hexdocs.pm/mapz/mapz.html#inverse-2 43 | [deep_search-2]: https://hexdocs.pm/mapz/mapz.html#deep_search-2 44 | 45 | ## [2.2.0] - 2021-06-17 46 | 47 | ### Added 48 | 49 | - New [`deep_iterator/1`][deep_iterator-1] function 50 | - New [`deep_next/1`][deep_next-1] function 51 | - New [`deep_merge_with/2`][deep_merge_with-2] function 52 | - New [`deep_merge_with/3`][deep_merge_with-3] function 53 | 54 | [deep_iterator-1]: https://hexdocs.pm/mapz/mapz.html#deep_iterator-1 55 | [deep_next-1]: https://hexdocs.pm/mapz/mapz.html#deep_next-1 56 | [deep_merge_with-2]: https://hexdocs.pm/mapz/mapz.html#deep_merge_with-2 57 | [deep_merge_with-3]: https://hexdocs.pm/mapz/mapz.html#deep_merge_with-3 58 | 59 | ## [2.1.1] - 2021-06-14 60 | 61 | ### Fixed 62 | 63 | - Updating a path that had a map as value with `deep_update_with` returned 64 | `error` instead of the map value. 65 | 66 | ## [2.1.0] - 2020-07-02 67 | 68 | ### Added 69 | 70 | - New [`deep_update_with/4`][deep_update_with-4] function 71 | 72 | [deep_update_with-4]: https://hexdocs.pm/mapz/mapz.html#deep_update_with-4 73 | 74 | ## [2.0.0] - 2019-11-27 75 | 76 | ### Added 77 | 78 | - New [`deep_update/3`][deep_update-3] function 79 | - New [`deep_update_with/3`][deep_update_with-3] function 80 | 81 | [deep_update-3]: https://hexdocs.pm/mapz/mapz.html#deep_update-3 82 | [deep_update_with-3]: https://hexdocs.pm/mapz/mapz.html#deep_update_with-3 83 | 84 | ### Changed 85 | 86 | - The `{badvalue, P}` exception from `deep_put/3` now returns a path to the bad 87 | value instead of the value itself to make it coherent with the new 88 | `deep_update/3` implementation. 89 | - The `{badkey, K}` exception from `deep_get/2` has been changed to 90 | `{badvalue, P}` to make it coherent with `deep_put/3` and others. 91 | 92 | ## [1.0.0] - 2019-11-26 93 | 94 | ### Added 95 | 96 | - Clarified documentation for `deep_put/3` regarding possible exceptions. 97 | 98 | ### Changed 99 | 100 | - `deep_remove` now removes the last existing key in the path, ignoring the rest 101 | to keep it in line with the behavior of `maps:remove/1` which silently returns 102 | the map if the key doesn't exist. 103 | 104 | ## [0.3.0] - 2018-09-13 105 | 106 | Initial release 107 | 108 | [unreleased]: https://github.com/eproxus/mapz/compare/v2.4.0...HEAD 109 | [2.4.0]: https://github.com/eproxus/mapz/compare/v2.3.0...v2.4.0 110 | [2.3.0]: https://github.com/eproxus/mapz/compare/v2.2.1...v2.3.0 111 | [2.2.0]: https://github.com/eproxus/mapz/compare/v2.1.1...v2.2.0 112 | [2.1.1]: https://github.com/eproxus/mapz/compare/v2.1.0...v2.1.1 113 | [2.1.0]: https://github.com/eproxus/mapz/compare/v2.0.0...v2.1.0 114 | [2.0.0]: https://github.com/eproxus/mapz/compare/v1.0.0...v2.0.0 115 | [1.0.0]: https://github.com/eproxus/mapz/compare/v0.3.0...v1.0.0 116 | [0.3.0]: https://github.com/eproxus/mapz/releases/tag/v0.3.0 117 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2025` `Adam Lindberg` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | all: compile test 4 | 5 | compile: 6 | @rebar compile 7 | 8 | test-deps: 9 | @rebar get-deps -C test.config 10 | 11 | test-compile: test-deps 12 | @rebar compile -C test.config 13 | 14 | test-fast: 15 | @rebar eunit -C test.config 16 | 17 | test: test-compile test-fast 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

mapz

2 | 3 |

4 | 5 | continous integration 6 | 7 | 8 | hex.pm version 9 | 10 | 11 | hex.pm license 12 | 13 | 14 | erlang versions 15 | 16 |

17 | 18 |

19 | Function Reference 20 |

21 |
22 | 23 | `mapz` contains additions to the Erlang maps module, mostly functionality to 24 | nested map usage. It tries to stay as true as possible to the original `maps` 25 | API with the same or similar errors where applicable. 26 | 27 | ## Features 28 | 29 | ### Deep Access 30 | 31 | #### Getting & Setting 32 | 33 | ```erlang 34 | 1> Map = #{a => #{big => #{nested => #{map => with},some => values}, 35 | .. in => it,like => "hello"}}. 36 | #{a => 37 | #{big => #{nested => #{map => with},some => values}, 38 | in => it,like => "hello"}} 39 | 2> mapz:deep_get([a, big, nested, map], Map). 40 | with 41 | 3> mapz:deep_search([a, big, nested, list], Map). 42 | {error,[a,big,nested],#{map => with}} 43 | 4> mapz:deep_remove([a, like], Map). 44 | #{a => 45 | #{big => #{nested => #{map => with},some => values}, 46 | in => it}} 47 | ``` 48 | 49 | #### Merging 50 | 51 | ```erlang 52 | 5> NewMap = mapz:deep_merge(Map, #{a => #{big => #{nested => #{list => [1,2,3]}}}}). 53 | #{a => 54 | #{big => 55 | #{nested => #{list => [1,2,3],map => with},some => values}, 56 | in => it,like => "hello"}} 57 | 6> mapz:deep_search([a, big, nested, list], NewMap). 58 | {ok,[1,2,3]} 59 | ``` 60 | 61 | #### Inverse 62 | 63 | ```erlang 64 | 1> mapz:inverse(#{a => 1, b => 2, c => 3}). 65 | #{1 => a,2 => b,3 => c} 66 | ``` 67 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, []}. 2 | 3 | {erl_opts, [debug_info]}. 4 | 5 | {project_plugins, [ 6 | erlfmt, 7 | rebar3_ex_doc 8 | ]}. 9 | 10 | {erlfmt, [ 11 | write, 12 | {print_width, 80}, 13 | {files, [ 14 | "rebar.config", 15 | "{src,include,test}/*.{hrl,erl,app.src}" 16 | ]} 17 | ]}. 18 | 19 | {ex_doc, [ 20 | {extras, ["README.md", "LICENSE.md"]}, 21 | {main, "README.md"} 22 | ]}. 23 | {hex, [{doc, #{provider => ex_doc}}]}. 24 | 25 | {profiles, [ 26 | {test, [ 27 | {cover_enabled, true}, 28 | {cover_opts, [verbose]}, 29 | {deps, [unite]}, 30 | {eunit_opts, [no_tty, {report, {unite_compact, []}}]} 31 | ]} 32 | ]}. 33 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/mapz.app.src: -------------------------------------------------------------------------------- 1 | {application, mapz, [ 2 | {description, "Extensions to the Erlang maps module"}, 3 | {vsn, "2.4.0"}, 4 | {applications, [kernel, stdlib]}, 5 | 6 | {licenses, ["MIT"]}, 7 | {links, [ 8 | {"GitHub", "https://github.com/eproxus/mapz"} 9 | ]} 10 | ]}. 11 | -------------------------------------------------------------------------------- /src/mapz.erl: -------------------------------------------------------------------------------- 1 | -module(mapz). 2 | 3 | % Disable new documentation syntax on versions below 27 4 | -if(?OTP_RELEASE >= 27). 5 | -define(moduledoc(Docstring), -moduledoc(Docstring)). 6 | -define(doc(Docstring), -doc(Docstring)). 7 | -else. 8 | -define(moduledoc(_Docstring), -compile([])). 9 | -define(doc(_Docstring), -compile([])). 10 | -endif. 11 | 12 | ?moduledoc(""" 13 | Additions to the Erlang maps module. 14 | """). 15 | 16 | % API 17 | -export([deep_find/2]). 18 | -ignore_xref({deep_find, 2}). 19 | -export([deep_search/2]). 20 | -ignore_xref({deep_search, 2}). 21 | -export([deep_get/2]). 22 | -ignore_xref({deep_get, 2}). 23 | -export([deep_get/3]). 24 | -ignore_xref({deep_get, 3}). 25 | -export([deep_put/3]). 26 | -ignore_xref({deep_put, 3}). 27 | -export([deep_update/3]). 28 | -ignore_xref({deep_update, 3}). 29 | -export([deep_update_with/3]). 30 | -ignore_xref({deep_update_with, 3}). 31 | -export([deep_update_with/4]). 32 | -ignore_xref({deep_update_with, 4}). 33 | -export([deep_remove/2]). 34 | -ignore_xref({deep_remove, 2}). 35 | -export([deep_merge/1]). 36 | -ignore_xref({deep_merge, 1}). 37 | -export([deep_merge/2]). 38 | -ignore_xref({deep_merge, 2}). 39 | -export([deep_merge/3]). 40 | -ignore_xref({deep_merge, 3}). 41 | -export([deep_merge_with/2]). 42 | -ignore_xref({deep_merge_with, 2}). 43 | -export([deep_merge_with/3]). 44 | -ignore_xref({deep_merge_with, 3}). 45 | -export([deep_iterator/1]). 46 | -ignore_xref({deep_iterator, 1}). 47 | -export([deep_next/1]). 48 | -ignore_xref({deep_next, 1}). 49 | -export([deep_intersect/2]). 50 | -ignore_xref({deep_intersect, 2}). 51 | -export([deep_intersect_with/3]). 52 | -ignore_xref({deep_intersect_with, 3}). 53 | -export([inverse/1]). 54 | -ignore_xref({inverse, 1}). 55 | -export([inverse/2]). 56 | -ignore_xref({inverse, 2}). 57 | -export([format_error/2]). 58 | -ignore_xref({format_error, 2}). 59 | 60 | -deprecated({deep_merge, 3, "use `deep_merge_with/3` instead"}). 61 | 62 | % We must inline this so that the stack trace points to the correct function. 63 | -compile({inline, [error_info/2]}). 64 | 65 | %--- Types --------------------------------------------------------------------- 66 | 67 | -export_type([path/0]). 68 | -export_type([iterator/0]). 69 | -export_type([combiner/0]). 70 | 71 | ?doc("A list of keys that are used to iterate deeper into a map of maps."). 72 | -type path() :: [term()]. 73 | 74 | ?doc(""" 75 | An iterator representing the associations in a map with keys of type Key and 76 | values of type Value. 77 | 78 | Created using `deep_iterator/1`. 79 | 80 | Consumed by `deep_next/1`. 81 | """). 82 | -opaque iterator() :: 83 | { 84 | ?MODULE, 85 | none | maps:iterator(_, _) | {_, _, maps:iterator(_, _)}, 86 | path(), 87 | [maps:iterator(_, _)] 88 | }. 89 | 90 | ?doc(""" 91 | A combiner function that takes a path, and its two conflicting old values and 92 | returns a new value. 93 | """). 94 | -type combiner() :: 95 | fun((Path :: path(), Old :: term(), New :: term()) -> term()). 96 | 97 | %--- API ---------------------------------------------------------------------- 98 | 99 | ?doc(""" 100 | Returns a tuple `{ok,Value}`, where Value is the value associated with `Path`, 101 | or `error` if no value is associated with `Path` in `Map`. 102 | 103 | The call can raise the following exceptions: 104 | 105 | * `{badmap,Map}` if `Map` is not a map 106 | * `{badpath,Path}` if `Path` is not a path 107 | """). 108 | -spec deep_find(path(), map()) -> {ok, term()} | error. 109 | deep_find(Path, Map) -> 110 | check(Path, Map), 111 | search( 112 | Map, 113 | Path, 114 | fun(Value) -> {ok, Value} end, 115 | fun(_Existing, _Key) -> error end 116 | ). 117 | 118 | ?doc(""" 119 | Returns a tuple `{ok,Value}` where `Value` is the value associated with `Path`, 120 | or `{error, PartialPath, Value}` if no value is associated with `Path` in `Map`, 121 | where `PartialPath` represents the path to the last found element in `Map` and 122 | `Value` is the value found at that path. 123 | 124 | When no key in `Path` exists in `Map`, `{error, [], Map}` is returned. 125 | 126 | The call can raise the following exceptions: 127 | 128 | * `{badmap,Map}` if `Map` is not a map 129 | * `{badpath,Path}` if `Path` is not a path 130 | """). 131 | deep_search(Path, Map) -> 132 | check(Path, Map), 133 | search( 134 | Map, 135 | Path, 136 | fun(Value) -> {ok, Value} end, 137 | fun 138 | ({ok, Value}, LastPath) -> 139 | {error, LastPath, Value}; 140 | (error, LastPath) -> 141 | {FoundPath, _} = lists:split(length(LastPath) - 1, LastPath), 142 | {error, FoundPath, deep_get(FoundPath, Map)} 143 | end 144 | ). 145 | 146 | ?doc(""" 147 | Returns value `Value` associated with `Path` if `Map` contains `Path`. 148 | 149 | The call can raise the following exceptions: 150 | 151 | * `{badmap,Map}` if `Map` is not a map 152 | * `{badpath,Path}` if `Path` is not a path 153 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 154 | a intermediate key at the path `PartialPath` 155 | * `{badkey,Path}` if no value is associated with path `Path` 156 | """). 157 | -spec deep_get(path(), map()) -> term(). 158 | deep_get(Path, Map) -> 159 | check(Path, Map), 160 | search( 161 | Map, 162 | Path, 163 | fun(Value) -> Value end, 164 | fun 165 | ({ok, Existing}, P) -> error({badvalue, P, Existing}); 166 | (error, P) -> error({badkey, P}) 167 | end 168 | ). 169 | 170 | ?doc(""" 171 | Returns value `Value` associated with `Path` if `Map` contains `Path`. If 172 | no value is associated with `Path`, `Default` is returned. 173 | 174 | The call can raise the following exceptions: 175 | 176 | * `{badmap,Map}` if `Map` is not a map 177 | * `{badpath,Path}` if `Path` is not a path 178 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 179 | a intermediate key at the path `PartialPath` 180 | """). 181 | -spec deep_get(path(), map(), term()) -> term(). 182 | deep_get(Path, Map, Default) -> 183 | check(Path, Map), 184 | search( 185 | Map, 186 | Path, 187 | fun(Value) -> Value end, 188 | fun(_Existing, _P) -> Default end 189 | ). 190 | 191 | ?doc(""" 192 | Associates `Path` with value `Value` and inserts the association into map 193 | `Map2`. If path `Path` already exists in map `Map1`, the old associated value 194 | is replaced by value `Value`. The function returns a new map `Map2` containing 195 | the new association and the old associations in `Map1`. 196 | 197 | The call can raise the following exceptions: 198 | 199 | * `{badmap,Map}` if `Map1` is not a map 200 | * `{badpath,Path}` if `Path` is not a path 201 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 202 | a intermediate key at the path `PartialPath` 203 | """). 204 | -spec deep_put(path(), term(), map()) -> map(). 205 | deep_put(Path, Value, Map1) -> 206 | check(Path, Map1), 207 | update(Map1, Path, fun(_Existing) -> Value end, fun(P, Rest, V) -> 208 | badvalue_and_create(P, Rest, V, Value) 209 | end). 210 | 211 | ?doc(""" 212 | If `Path` exists in `Map1`, the old associated value is replaced by value 213 | `Value`. The function returns a new map `Map2` containing the new associated 214 | value. 215 | 216 | The call can raise the following exceptions: 217 | 218 | * `{badmap,Map}` if `Map1` is not a map 219 | * `{badpath,Path}` if `Path` is not a path 220 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 221 | a intermediate key at the path `PartialPath` 222 | * `{badkey,Path}` if no value is associated with path `Path` 223 | """). 224 | -spec deep_update(path(), term(), map()) -> map(). 225 | deep_update(Path, Value, Map1) -> 226 | check(Path, Map1), 227 | update(Map1, Path, fun(_Existing) -> Value end, fun badvalue_and_badkey/3). 228 | 229 | ?doc(""" 230 | Update a value in a `Map1` associated with `Path` by calling `Fun` on the old 231 | value to get a new value. 232 | 233 | The call can raise the following exceptions: 234 | 235 | * `{badmap,Map}` if `Map1` is not a map 236 | * `{badpath,Path}` if `Path` is not a path 237 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 238 | a intermediate key at the path `PartialPath` 239 | * `{badkey,Path}` if no value is associated with path `Path` 240 | * `badarg` if `Fun` is not a function of arity 1 241 | """). 242 | -spec deep_update_with(path(), fun((term()) -> term()), map()) -> map(). 243 | deep_update_with(Path, Fun, Map1) -> 244 | deep_update_with_1(Path, Fun, Map1, fun badvalue_and_badkey/3). 245 | 246 | ?doc(""" 247 | Update a value in a `Map1` associated with `Path` by calling `Fun` on the 248 | old value to get a new value. If `Path` is not present in `Map1` then `Init` 249 | will be associated with `Path`. 250 | 251 | The call can raise the following exceptions: 252 | 253 | * `{badmap,Map}` if `Map1` is not a map 254 | * `{badpath,Path}` if `Path` is not a path 255 | * `{badvalue,PartialPath,Value}` if a value `Value` that is not a map exists as 256 | a intermediate key at the path `PartialPath` 257 | * `badarg` if `Fun` is not a function of arity 1 258 | """). 259 | -spec deep_update_with(path(), fun((term()) -> term()), any(), map()) -> map(). 260 | deep_update_with(Path, Fun, Init, Map1) -> 261 | deep_update_with_1(Path, Fun, Map1, fun(P, Rest, Value) -> 262 | badvalue_and_create(P, Rest, Value, Init) 263 | end). 264 | 265 | deep_update_with_1(Path, Fun, Map1, Default) -> 266 | check(Path, Map1), 267 | check_fun(Fun, 1), 268 | update( 269 | Map1, 270 | Path, 271 | fun(Value) -> Fun(Value) end, 272 | Default 273 | ). 274 | 275 | ?doc(""" 276 | Removes the last existing key of `Path`, and its associated value from 277 | `Map1` and returns a new map `Map2` without that key. Any deeper non-existing 278 | keys are ignored. 279 | 280 | The call can raise the following exceptions: 281 | 282 | * `{badmap,Map}` if `Map` is not a map 283 | * `{badpath,Path}` if `Path` is not a path 284 | """). 285 | -spec deep_remove(path(), map()) -> map(). 286 | deep_remove(Path, Map) -> 287 | check(Path, Map), 288 | remove(Map, Path). 289 | 290 | ?doc(""" 291 | Merges a list of maps recursively into a single map. 292 | 293 | If a path exist in several maps, the value in the first nested map is superseded 294 | by the value in a following nested map. 295 | 296 | That is, `deep_merge/2` behaves as if it had been defined as follows: 297 | 298 | ```erlang 299 | deep_merge(Maps) when is_list(Maps) -> 300 | deep_merge_with(fun(_Path, _V1, V2) -> V2 end, Maps). 301 | ``` 302 | 303 | The call can raise the following exceptions: 304 | 305 | * `{badmap,Map}` exception if any of the maps is not a map 306 | """). 307 | -spec deep_merge([map()]) -> map(). 308 | deep_merge(Maps) when is_list(Maps) -> 309 | deep_merge_with(fun(_Path, _V1, V2) -> V2 end, Maps). 310 | 311 | ?doc(#{equiv => deep_merge([Map1, Map2])}). 312 | -spec deep_merge(map(), map()) -> map(). 313 | deep_merge(Map1, Map2) when is_map(Map1), is_map(Map2) -> 314 | deep_merge([Map1, Map2]). 315 | 316 | ?doc(""" 317 | Merges a list of maps `Maps` recursively into a single map `Target`. 318 | 319 | If a path exist in several maps, the function `Fun` is called with the previous 320 | and the conflicting value to resolve the conflict. The return value from the 321 | function is put into the resulting map. 322 | 323 | The call can raise the following exceptions: 324 | 325 | * `{badmap,Map}` exception if any of the maps is not a map 326 | """). 327 | -spec deep_merge( 328 | fun((Old :: term(), New :: term()) -> term()), map(), map() | [map()] 329 | ) -> map(). 330 | deep_merge(Fun, Target, Maps) -> 331 | deep_merge_with(fun(_P, V1, V2) -> Fun(V1, V2) end, Target, Maps). 332 | 333 | ?doc(""" 334 | Merges a list of maps `Maps` recursively into a single map. 335 | 336 | If a path exist in several maps, the function `Fun` is called with the path, the 337 | previous and the conflicting value to resolve the conflict. The return value 338 | from the function is put into the resulting map. 339 | 340 | The call can raise the following exceptions: 341 | 342 | * `{badmap,Map}` exception if any of the maps is not a map 343 | * `badarg` if `Fun` is not a function of arity 3 344 | """). 345 | -spec deep_merge_with(Fun :: combiner(), Maps :: [map()]) -> map(). 346 | deep_merge_with(Fun, [Target | Maps]) -> 347 | deep_merge_with1(Fun, Target, Maps, []). 348 | 349 | ?doc(""" 350 | Merges a list of maps `Maps` recursively into a single map. 351 | 352 | If a path exist in several maps, the function `Fun` is called with the path, the 353 | previous and the conflicting value to resolve the conflict. The return value 354 | from the function is put into the resulting map. 355 | 356 | The call can raise the following exceptions: 357 | 358 | * `{badmap,Map}` exception if any of the maps is not a map 359 | """). 360 | -spec deep_merge_with(Fun :: combiner(), Map1 :: map(), Map2 :: map()) -> map(). 361 | deep_merge_with(Fun, Map1, Map2) when is_map(Map1), is_map(Map2) -> 362 | deep_merge_with(Fun, [Map1, Map2]). 363 | 364 | deep_merge_with1(_Fun, Target, [], _Path) when is_map(Target) -> 365 | Target; 366 | deep_merge_with1(Fun, Target, [From | Maps], Path) -> 367 | deep_merge_with1( 368 | Fun, deep_merge_with1(Fun, Target, From, Path), Maps, Path 369 | ); 370 | deep_merge_with1(Fun, Target, Map, Path) -> 371 | check_map(Target), 372 | check_map(Map), 373 | check_fun(Fun, 3), 374 | maps:fold( 375 | fun(K, V, T) -> 376 | case maps:find(K, T) of 377 | {ok, Value} when is_map(Value), is_map(V) -> 378 | maps:put( 379 | K, deep_merge_with1(Fun, Value, [V], Path ++ [K]), T 380 | ); 381 | {ok, Value} -> 382 | maps:put(K, Fun(Path ++ [K], Value, V), T); 383 | error -> 384 | maps:put(K, V, T) 385 | end 386 | end, 387 | Target, 388 | Map 389 | ). 390 | 391 | ?doc(""" 392 | Returns a map iterator `Iterator` that can be used by `deep_next/1` to 393 | recursively traverse the path-value associations in a deep map structure. 394 | 395 | The call fails with a `{badmap,Map}` exception if Map is not a map. 396 | """). 397 | -spec deep_iterator(map()) -> iterator(). 398 | deep_iterator(Map) when is_map(Map) -> 399 | {?MODULE, maps:next(maps:iterator(Map)), [], []}; 400 | deep_iterator(Map) -> 401 | error_info({badmap, Map}, [Map]). 402 | 403 | ?doc(""" 404 | Returns the next path-value association in `Iterator` and a new iterator for the 405 | remaining associations in the iterator. 406 | 407 | If the value is anogher map the iterator will first return the map as a value 408 | with its path. Only on the next call the inner value with its path is returned. 409 | That is, first `{Path, map(), iterator()}` and then `{InnerPath, Value, 410 | iterator()}`. 411 | 412 | If there are no more associations in the iterator, `none` is returned. 413 | """). 414 | -spec deep_next(iterator()) -> {path(), term(), iterator()} | none. 415 | deep_next({?MODULE, I, Trail, Stack}) -> 416 | case {I, Stack} of 417 | {none, []} -> 418 | none; 419 | {none, [Prev | Rest]} -> 420 | deep_next({?MODULE, maps:next(Prev), lists:droplast(Trail), Rest}); 421 | {{K, V, I2}, Stack} when is_map(V) -> 422 | Path = Trail ++ [K], 423 | Next = maps:next(maps:iterator(V)), 424 | {Path, V, {?MODULE, Next, Path, [I2 | Stack]}}; 425 | {{K, V, I2}, Stack} -> 426 | Path = Trail ++ [K], 427 | {Path, V, {?MODULE, I2, Trail, Stack}} 428 | end; 429 | deep_next(Iter) -> 430 | error_info(badarg, [Iter]). 431 | 432 | ?doc(""" 433 | Intersects two maps into a single map `Map3`. 434 | 435 | If a path exists in both maps, the value in `Map1` is superseded by the value in 436 | `Map2`. 437 | 438 | That is, `deep_intersect/2` behaves as if it had been defined as follows: 439 | 440 | ```erlang 441 | deep_intersect(A, B) -> 442 | deep_intersect_with(fun(_Path, _V1, V2) -> V2 end, A, B). 443 | ``` 444 | 445 | The call can raise the following exceptions: 446 | 447 | * `{badmap,Map}` exception if any of the maps is not a map 448 | """). 449 | -spec deep_intersect(Map1 :: map(), Map2 :: map()) -> Map3 :: map(). 450 | deep_intersect(A, B) -> 451 | deep_intersect_with(fun(_Path, _V1, V2) -> V2 end, A, B). 452 | 453 | ?doc(""" 454 | Intersects two maps into a single map `Map3`. 455 | 456 | If a path exists in both maps, the value in `Map1` is combined with the value in 457 | `Map2` by the `Combiner` fun. When `Combiner` is applied the path that exists in 458 | both maps is the first parameter, the value from `Map1` is the second parameter, 459 | and the value from `Map2` is the third parameter. 460 | 461 | The call can raise the following exceptions: 462 | 463 | * `{badmap,Map}` exception if any of the maps is not a map 464 | * `badarg` if `Fun` is not a function of arity 3 465 | """). 466 | -spec deep_intersect_with(Fun :: combiner(), Map1 :: map(), Map2 :: map()) -> 467 | Map3 :: map(). 468 | deep_intersect_with(Combiner, A, B) -> 469 | check_map(B), 470 | check_fun(Combiner, 3), 471 | deep_intersect_with1({Combiner, flip_combiner(Combiner)}, A, B, []). 472 | 473 | deep_intersect_with1({Combiner, Flipped}, A, B, Path) when 474 | map_size(A) > map_size(B) 475 | -> 476 | deep_intersect_with1({Flipped, Combiner}, B, A, Path); 477 | deep_intersect_with1(Combiners, A, B, Path) -> 478 | deep_intersect1(maps:next(maps:iterator(A)), B, [], Combiners, Path). 479 | 480 | deep_intersect1(none, _B, Keep, _Combiners, _Path) -> 481 | maps:from_list(Keep); 482 | deep_intersect1({K, V1, Iter}, B, Keep, {Combiner, _} = Combiners, Path) -> 483 | NewKeep = 484 | case B of 485 | #{K := V2} when is_map(V1), is_map(V2) -> 486 | [{K, deep_intersect_with1(Combiners, V1, V2, Path)} | Keep]; 487 | #{K := V2} -> 488 | [{K, Combiner(Path ++ [K], V1, V2)} | Keep]; 489 | _ -> 490 | Keep 491 | end, 492 | deep_intersect1(maps:next(Iter), B, NewKeep, Combiners, Path). 493 | 494 | ?doc(""" 495 | Inverts a map by inserting each value as the key with its corresponding key as 496 | the value. If two keys have the same value, the value for the first key in map 497 | order will take precedence. 498 | 499 | That is, `inverse/1` behaves as if it had been defined as follows: 500 | 501 | ```erlang 502 | inverse(Map) -> inverse(Map, fun(Old, _New) -> Old end). 503 | ``` 504 | 505 | The call can raise the following exceptions: 506 | 507 | * `{badmap,Map}` if `Map` is not a map 508 | """). 509 | -spec inverse(map()) -> map(). 510 | inverse(Map) -> inverse(Map, fun(Old, _New) -> Old end). 511 | 512 | ?doc(""" 513 | Inverts a map by inserting each value as the key with its corresponding key as 514 | the value. If two keys have the same value in `Map`, `Fun` is called with the 515 | old and new key to determine the resulting value. 516 | 517 | The call can raise the following exceptions: 518 | 519 | * `{badmap,Map}` if `Map` is not a map 520 | * `badarg` if `Fun` is not a function of arity 2 521 | """). 522 | -spec inverse(map(), fun((Old :: term(), New :: term()) -> term())) -> map(). 523 | inverse(Map, Fun) when is_map(Map), is_function(Fun, 2) -> 524 | maps:fold( 525 | fun(K1, V, Acc) -> 526 | maps:update_with(V, fun(K0) -> Fun(K0, K1) end, K1, Acc) 527 | end, 528 | #{}, 529 | Map 530 | ); 531 | inverse(Map, Fun) -> 532 | check_fun(Fun, 2), 533 | error_info({badmap, Map}, [Map, Fun]). 534 | 535 | ?doc(hidden). 536 | format_error(_Reason, [{_M, F, As, _Info} | _]) -> 537 | error_args(F, As). 538 | 539 | %--- Internal Functions ------------------------------------------------------- 540 | 541 | check(Path, Map) -> 542 | check_path(Path), 543 | check_map(Map). 544 | 545 | check_path(Path) when is_list(Path) -> ok; 546 | check_path(Path) -> error_info({badpath, Path}, [Path]). 547 | 548 | check_map(Map) when is_map(Map) -> ok; 549 | check_map(Map) -> error_info({badmap, Map}, [Map]). 550 | 551 | check_fun(Fun, Arity) when is_function(Fun, Arity) -> ok; 552 | check_fun(_Fun, _Arity) -> exit(badarg). 553 | 554 | search(Map, Path, Wrap, Default) -> search(Map, Path, Wrap, Default, []). 555 | 556 | search(Element, [], Wrap, _Default, _Acc) -> 557 | Wrap(Element); 558 | search(Map, [Key | Path], Wrap, Default, Acc) when is_map(Map) -> 559 | case maps:find(Key, Map) of 560 | {ok, Value} -> search(Value, Path, Wrap, Default, [Key | Acc]); 561 | error -> Default(error, lists:reverse([Key | Acc])) 562 | end; 563 | search(Value, [_Key | _Path], _Wrap, Default, Acc) -> 564 | Default({ok, Value}, lists:reverse(Acc)). 565 | 566 | update(Map, Path, Wrap, Default) -> update(Map, Path, Wrap, Default, []). 567 | 568 | update(Map, [Key | Path], Wrap, Default, Acc) -> 569 | Hist = [Key | Acc], 570 | Value = 571 | case maps:find(Key, Map) of 572 | {ok, Existing} when is_map(Existing) -> 573 | update(Existing, Path, Wrap, Default, Hist); 574 | {ok, Existing} -> 575 | case Path of 576 | [] -> Wrap(Existing); 577 | _ -> Default(lists:reverse(Hist), Path, {ok, Existing}) 578 | end; 579 | error -> 580 | Default(lists:reverse(Hist), Path, error) 581 | end, 582 | maps:put(Key, Value, Map); 583 | update(Map, [], Wrap, _Default, _Acc) -> 584 | Wrap(Map). 585 | 586 | remove(Map, []) -> 587 | Map; 588 | remove(Map, [First]) -> 589 | maps:remove(First, Map); 590 | remove(Map, [First, Second | Path]) when is_map(Map) -> 591 | case maps:find(First, Map) of 592 | {ok, Sub} when is_map(Sub) -> 593 | case maps:find(Second, Sub) of 594 | {ok, _SubSub} -> 595 | maps:update(First, remove(Sub, [Second | Path]), Map); 596 | error -> 597 | maps:remove(First, Map) 598 | end; 599 | {ok, _Value} -> 600 | maps:remove(First, Map); 601 | error -> 602 | Map 603 | end. 604 | 605 | create(Path, Value) -> 606 | lists:foldr(fun(Key, Acc) -> #{Key => Acc} end, Value, Path). 607 | 608 | badvalue_and_badkey(P, _Rest, {ok, Existing}) -> 609 | error({badvalue, P, Existing}); 610 | badvalue_and_badkey(P, _Rest, error) -> 611 | error({badkey, P}). 612 | 613 | badvalue_and_create(P, _Rest, {ok, Existing}, _Init) -> 614 | error({badvalue, P, Existing}); 615 | badvalue_and_create(_P, Rest, error, Init) -> 616 | create(Rest, Init). 617 | 618 | -if(?OTP_RELEASE >= 24). 619 | error_info(Reason, Args) -> 620 | erlang:error(Reason, Args, [{error_info, #{module => ?MODULE}}]). 621 | -else. 622 | error_info(Reason, Args) -> 623 | erlang:error(Reason, Args). 624 | -endif. 625 | 626 | error_args(iterator, [_Map]) -> 627 | #{1 => <<"not a map">>}; 628 | error_args(deep_next, [_Iter]) -> 629 | #{1 => <<"not a valid iterator">>}. 630 | 631 | flip_combiner(Fun) -> fun(Path, V1, V2) -> Fun(Path, V2, V1) end. 632 | -------------------------------------------------------------------------------- /test/mapz_tests.erl: -------------------------------------------------------------------------------- 1 | -module(mapz_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | -define(STRUCT, #{ 6 | a => #{ 7 | a => #{ 8 | a => 1 9 | }, 10 | b => 2 11 | }, 12 | b => #{ 13 | a => 3, 14 | b => 4 15 | }, 16 | d => [], 17 | e => #{} 18 | }). 19 | -define(TO_MERGE, [ 20 | #{val => 1, a => 1}, 21 | #{val => 2, b => 2, x => #{2 => true, y => #{more => stuff}}}, 22 | #{val => 3, c => 3, x => #{3 => true}}, 23 | #{val => 4, d => 4, x => #{4 => true, y => #{extra => data}}} 24 | ]). 25 | -define(MERGED, #{ 26 | val => 4, 27 | a => 1, 28 | b => 2, 29 | c => 3, 30 | d => 4, 31 | x => #{ 32 | 2 => true, 33 | 3 => true, 34 | 4 => true, 35 | y => #{more => stuff, extra => data} 36 | } 37 | }). 38 | 39 | -import(mapz, [ 40 | deep_find/2, 41 | deep_search/2, 42 | deep_get/2, 43 | deep_get/3, 44 | deep_put/3, 45 | deep_update/3, 46 | deep_update_with/3, 47 | deep_update_with/4, 48 | deep_remove/2, 49 | deep_merge/1, 50 | deep_merge/2, 51 | deep_merge/3, 52 | deep_merge_with/2, 53 | deep_merge_with/3, 54 | deep_iterator/1, 55 | deep_next/1, 56 | deep_intersect/2, 57 | deep_intersect_with/3, 58 | inverse/1, 59 | inverse/2 60 | ]). 61 | 62 | -define(_badarg(Function), 63 | ?_assertError({badmap, foobar}, (Function)([a], foobar)), 64 | ?_assertError({badpath, foobar}, (Function)(foobar, #{a => 1})) 65 | ). 66 | 67 | -define(_badvalue(Function), 68 | ?_assertError({badvalue, [d], []}, (Function)([d, x], ?STRUCT)), 69 | ?_assertError({badvalue, [a, b], 2}, (Function)([a, b, c], ?STRUCT)) 70 | ). 71 | 72 | -define(_badkey(Function), 73 | ?_assertError({badkey, [b]}, (Function)([b], #{a => 1})), 74 | ?_assertError({badkey, [b, x]}, (Function)([b, x], ?STRUCT)) 75 | ). 76 | 77 | %--- Tests -------------------------------------------------------------------- 78 | 79 | deep_find_test_() -> 80 | {inparallel, [ 81 | ?_assertEqual({ok, 1}, deep_find([a, a, a], ?STRUCT)), 82 | ?_assertEqual(error, deep_find([a, b, a], ?STRUCT)) 83 | ]}. 84 | 85 | deep_search_test_() -> 86 | {inparallel, [ 87 | ?_assertEqual({ok, 1}, deep_search([a, a, a], ?STRUCT)), 88 | ?_assertEqual({error, [a, b], 2}, deep_search([a, b, c], ?STRUCT)), 89 | ?_assertEqual( 90 | {error, [b], #{a => 3, b => 4}}, deep_search([b, c], ?STRUCT) 91 | ), 92 | ?_assertEqual({error, [], ?STRUCT}, deep_search([x], ?STRUCT)), 93 | ?_assertEqual({ok, ?STRUCT}, deep_search([], ?STRUCT)), 94 | ?_badarg(deep_search) 95 | ]}. 96 | 97 | deep_get_test_() -> 98 | {inparallel, [ 99 | ?_assertEqual(1, deep_get([a, a, a], ?STRUCT)), 100 | ?_assertEqual(#{a => 1}, deep_get([a, a], ?STRUCT)), 101 | ?_assertEqual(d, deep_get([a, c], ?STRUCT, d)), 102 | ?_assertEqual(1, deep_get([a, a, a], ?STRUCT, default)), 103 | ?_assertEqual(default, deep_get([a, b, c], ?STRUCT, default)), 104 | ?_assertEqual(?STRUCT, deep_get([], ?STRUCT)), 105 | ?_badarg(deep_get), 106 | ?_badvalue(deep_get), 107 | ?_badkey(deep_get) 108 | ]}. 109 | 110 | deep_put_test_() -> 111 | {inparallel, [ 112 | ?_assertEqual(v, deep_put([], v, #{})), 113 | ?_assertEqual(v, deep_get([a, a, a], deep_put([a, a, a], v, ?STRUCT))), 114 | ?_assertEqual( 115 | #{a => 1, x => #{y => #{a => 3}}}, 116 | deep_get([a, a], deep_put([a, a, x, y], #{a => 3}, ?STRUCT)) 117 | ), 118 | ?_badvalue(fun(P, M) -> deep_put(P, y, M) end) 119 | ]}. 120 | 121 | deep_update_test_() -> 122 | {inparallel, [ 123 | ?_assertEqual(#{a => 2}, deep_update([a], 2, #{a => 1})), 124 | ?_assertEqual( 125 | deep_put([a, a, a], 2, ?STRUCT), 126 | deep_update([a, a, a], 2, ?STRUCT) 127 | ), 128 | ?_badarg(fun(P, M) -> deep_update(P, 2, M) end), 129 | ?_badvalue(fun(P, M) -> deep_update(P, 2, M) end), 130 | ?_badkey(fun(P, M) -> deep_update(P, 2, M) end) 131 | ]}. 132 | 133 | deep_update_with_test_() -> 134 | Incr = fun(V) -> V + 1 end, 135 | {inparallel, [ 136 | ?_assertEqual(#{a => 2}, deep_update_with([a], Incr, #{a => 1})), 137 | ?_assertEqual( 138 | deep_put([a, a, a], 2, ?STRUCT), 139 | deep_update_with([a, a, a], Incr, ?STRUCT) 140 | ), 141 | ?_assertEqual( 142 | deep_put([e], #{v => 1}, ?STRUCT), 143 | deep_update_with([e], fun(M) -> M#{v => 1} end, ?STRUCT) 144 | ), 145 | ?_assertExit(badarg, deep_update_with([a], x, ?STRUCT)), 146 | ?_assertExit( 147 | badarg, 148 | deep_update_with([a], fun() -> foo end, ?STRUCT) 149 | ), 150 | ?_badarg(fun(P, M) -> deep_update_with(P, Incr, M) end), 151 | ?_badvalue(fun(P, M) -> deep_update_with(P, Incr, M) end), 152 | ?_badkey(fun(P, M) -> deep_update_with(P, Incr, M) end) 153 | ]}. 154 | 155 | deep_update_with_init_test_() -> 156 | Incr = fun(V) -> V + 1 end, 157 | {inparallel, [ 158 | ?_assertEqual(#{a => 2}, deep_update_with([a], Incr, 0, #{a => 1})), 159 | ?_assertEqual( 160 | deep_put([a, a, a], 2, ?STRUCT), 161 | deep_update_with([a, a, a], Incr, 0, ?STRUCT) 162 | ), 163 | ?_assertEqual( 164 | deep_put([a, a, x, y], 0, ?STRUCT), 165 | deep_update_with([a, a, x, y], Incr, 0, ?STRUCT) 166 | ), 167 | ?_assertExit(badarg, deep_update_with([a], x, 0, ?STRUCT)), 168 | ?_assertExit( 169 | badarg, 170 | deep_update_with([a], fun() -> foo end, 0, ?STRUCT) 171 | ), 172 | ?_badarg(fun(P, M) -> deep_update_with(P, Incr, 0, M) end), 173 | ?_badvalue(fun(P, M) -> deep_update_with(P, Incr, 0, M) end) 174 | ]}. 175 | 176 | deep_remove_test_() -> 177 | {inparallel, [ 178 | ?_assertEqual(?STRUCT, deep_remove([], ?STRUCT)), 179 | ?_assertEqual(?STRUCT, deep_remove([y], ?STRUCT)), 180 | ?_assertEqual(?STRUCT, deep_remove([y, x], ?STRUCT)), 181 | ?_assertEqual( 182 | #{b => 2}, 183 | deep_get([a], deep_remove([a, a], ?STRUCT)) 184 | ), 185 | ?_assertEqual( 186 | #{b => 2}, 187 | deep_get([a], deep_remove([a, a, 0], ?STRUCT)) 188 | ), 189 | ?_assertEqual(#{}, deep_get([a, a], deep_remove([a, a, a], ?STRUCT))), 190 | ?_assertEqual( 191 | #{a => #{a => 1}}, 192 | deep_get([a], deep_remove([a, b, a], ?STRUCT)) 193 | ) 194 | ]}. 195 | 196 | deep_merge_test_() -> 197 | [First, Second | _] = Maps = ?TO_MERGE, 198 | Expected = ?MERGED, 199 | {inparallel, [ 200 | ?_assertEqual(?STRUCT, deep_merge([?STRUCT, ?STRUCT])), 201 | ?_assertEqual(Expected, mapz:deep_merge(Maps)), 202 | ?_assertEqual(deep_merge([First, Second]), deep_merge(First, Second)) 203 | ]}. 204 | 205 | deep_merge_fun_test_() -> 206 | First = #{a => [1, 2], b => #{c => [a]}}, 207 | Second = #{a => [3, 4], b => #{c => [b]}}, 208 | Fun = fun(A, B) -> A ++ B end, 209 | Expected = #{ 210 | a => [1, 2, 3, 4], 211 | b => #{c => [a, b]} 212 | }, 213 | {inparallel, [ 214 | ?_assertEqual(Expected, deep_merge(Fun, First, Second)) 215 | ]}. 216 | 217 | deep_merge_with_test_() -> 218 | Override = fun(_P, _A, B) -> B end, 219 | Append = fun 220 | (P, A, B) when is_list(A), is_list(B) -> 221 | io:format("~p ~p ~p~n", [P, A, B]), 222 | {P, A ++ B}; 223 | (P, A, B) -> 224 | io:format("~p ~p ~p~n", [P, A, B]), 225 | {P, {A, B}} 226 | end, 227 | {inparallel, [ 228 | ?_assertExit(badarg, deep_merge_with(foo, #{}, #{})), 229 | ?_assertExit(badarg, deep_merge_with(fun() -> ok end, #{}, #{})), 230 | ?_assertEqual(?STRUCT, deep_merge_with(Override, ?STRUCT, ?STRUCT)), 231 | ?_assertEqual(?MERGED, deep_merge_with(Override, ?TO_MERGE)), 232 | ?_assertEqual( 233 | #{ 234 | a => {[a], [1, 2]}, 235 | b => #{c => {[b, c], [3, 4]}}, 236 | d => {[d], {5, 6}}, 237 | e => 7, 238 | x => {[x], {[foo], #{y => [bar]}}} 239 | }, 240 | deep_merge_with( 241 | Append, 242 | #{a => [1], b => #{c => [3]}, d => 5, x => [foo]}, 243 | #{ 244 | a => [2], 245 | b => #{c => [4]}, 246 | d => 6, 247 | e => 7, 248 | x => #{y => [bar]} 249 | } 250 | ) 251 | ) 252 | ]}. 253 | 254 | deep_iterator_test_() -> 255 | {inparallel, [ 256 | ?_assertError({badmap, foo}, deep_iterator(foo)), 257 | ?_assertError(badarg, deep_next(foo)), 258 | ?_assertEqual(none, deep_next(deep_iterator(#{}))), 259 | ?_assertEqual( 260 | [{[a], 1}], 261 | exhaust(deep_iterator(#{a => 1})) 262 | ), 263 | ?_assertEqual( 264 | lists:sort([ 265 | {[a], #{b => 2}}, 266 | {[a, b], 2}, 267 | {[c], 3} 268 | ]), 269 | lists:sort(exhaust(deep_iterator(#{a => #{b => 2}, c => 3}))) 270 | ), 271 | ?_assertEqual( 272 | lists:sort([ 273 | {[a], deep_get([a], ?STRUCT)}, 274 | {[a, a], deep_get([a, a], ?STRUCT)}, 275 | {[a, a, a], 1}, 276 | {[a, b], 2}, 277 | {[b], deep_get([b], ?STRUCT)}, 278 | {[b, a], 3}, 279 | {[b, b], 4}, 280 | {[d], []}, 281 | {[e], #{}} 282 | ]), 283 | lists:sort(exhaust(deep_iterator(?STRUCT))) 284 | ) 285 | ]}. 286 | 287 | deep_interset_test_() -> 288 | {inparallel, [ 289 | ?_assertError({badmap, foo}, deep_intersect(foo, #{})), 290 | ?_assertError({badmap, foo}, deep_intersect(#{}, foo)), 291 | ?_assertEqual(#{}, deep_intersect(#{}, #{})), 292 | ?_assertEqual( 293 | #{ 294 | c => #{ 295 | x => #{ 296 | foo => baz 297 | } 298 | }, 299 | val => false 300 | }, 301 | deep_intersect( 302 | #{ 303 | c => #{ 304 | x => #{foo => bar} 305 | }, 306 | val => true, 307 | b => 2 308 | }, 309 | #{ 310 | c => #{ 311 | x => #{foo => baz, qux => no}, 312 | y => 4 313 | }, 314 | val => false, 315 | a => 1 316 | } 317 | ) 318 | ), 319 | ?_assertEqual( 320 | #{ 321 | a => #{ 322 | 1 => x 323 | } 324 | }, 325 | deep_intersect( 326 | #{ 327 | a => #{1 => x, 2 => x, 3 => x, 4 => x} 328 | }, 329 | #{ 330 | a => #{1 => x}, 331 | b => 1, 332 | c => 2, 333 | d => 3 334 | } 335 | ) 336 | ) 337 | ]}. 338 | 339 | deep_interset_with_test_() -> 340 | Combiner = fun 341 | (_Path, V1, V1) -> same; 342 | (_Path, V1, V2) when V1 < V2 -> bigger; 343 | (_Path, _V1, _V2) -> smaller 344 | end, 345 | {inparallel, [ 346 | ?_assertExit(badarg, deep_intersect_with(foo, #{}, #{})), 347 | ?_assertExit(badarg, deep_intersect_with(fun() -> ok end, #{}, #{})), 348 | ?_assertError({badmap, foo}, deep_intersect_with(Combiner, foo, #{})), 349 | ?_assertError({badmap, foo}, deep_intersect_with(Combiner, #{}, foo)), 350 | ?_assertEqual( 351 | #{ 352 | c => #{ 353 | x => #{ 354 | foo => bigger 355 | } 356 | }, 357 | val => smaller, 358 | d => same 359 | }, 360 | deep_intersect_with( 361 | Combiner, 362 | #{ 363 | c => #{ 364 | x => #{foo => 1} 365 | }, 366 | val => 2, 367 | b => 3, 368 | d => 9 369 | }, 370 | #{ 371 | c => #{ 372 | x => #{foo => 4, qux => 5}, 373 | y => 6 374 | }, 375 | val => -7, 376 | a => 8, 377 | d => 9 378 | } 379 | ) 380 | ) 381 | ]}. 382 | 383 | %--- Internal ------------------------------------------------------------------ 384 | 385 | exhaust(I) -> 386 | exhaust(deep_next(I), []). 387 | 388 | exhaust(none, Acc) -> 389 | lists:reverse(Acc); 390 | exhaust({K, V, I}, Acc) -> 391 | exhaust(deep_next(I), [{K, V} | Acc]). 392 | 393 | inverse_test_() -> 394 | {inparallel, [ 395 | ?_assertEqual(#{}, inverse(#{})), 396 | fun() -> 397 | Inverted = inverse(#{a => 1, b => 2, c => 2}), 398 | ?assertEqual([1, 2], lists:sort(maps:keys(Inverted))), 399 | #{1 := a, 2 := Second} = Inverted, 400 | ?assert(lists:member(Second, [b, c])) 401 | end, 402 | fun() -> 403 | Inverted = inverse( 404 | #{a => 1, b => 2, c => 2, d => 2}, 405 | fun 406 | (Old, New) when is_list(Old) -> Old ++ [New]; 407 | (Old, New) -> [Old, New] 408 | end 409 | ), 410 | ?assertEqual([1, 2], lists:sort(maps:keys(Inverted))), 411 | #{1 := a, 2 := Second} = Inverted, 412 | ?assertEqual([b, c, d], lists:sort(Second)) 413 | end, 414 | ?_assertError({badmap, foo}, inverse(foo)), 415 | ?_assertError( 416 | {badmap, foo}, 417 | inverse(foo, fun(_, _) -> error(should_not_occur) end) 418 | ), 419 | ?_assertExit(badarg, inverse(#{}, foo)), 420 | ?_assertExit(badarg, inverse(#{}, fun() -> ok end)), 421 | ?_assertExit(badarg, inverse(#{}, fun(_, _, _) -> ok end)) 422 | ]}. 423 | --------------------------------------------------------------------------------