├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------