/home/runner/work/klsn/klsn/_build/test/cover/eunit/klsn_obj.html

1 -module(klsn_obj).
2
3 -export([
4 get/2
5 , lookup/2
6 , find/2
7 , crud/3
8 ]).
9
10 -export_type([
11 value/0
12 , key/0
13 , nth/0
14 , obj/0
15 , cmd/0
16 , path/0
17 , find_fun/0
18 , crud_fun/0
19 ]).
20
21 %% ------------------------------------------------------------------
22 %% Exported types
23 %% ------------------------------------------------------------------
24
25 %% Any Erlang term that can live inside a nested structure tracked by this
26 %% module.
27 -type value() :: term().
28
29 %% Key used in a map.
30 -type key() :: term().
31
32 %% 1-based index into a list or tuple.
33 -type nth() :: pos_integer().
34
35 %% JSON-like recursive data structure: atoms, binaries, maps, lists or
36 %% tuples of other obj() values.
37 -type obj() :: value()
38 | lists:list(obj())
39 | tuple() % {}, {obj()}, {obj(), obj()}, ...
40 | maps:map(key(), obj())
41 .
42
43 %% A single navigation step used in a path().
44 -type cmd() :: {raw, nth() | key()}
45 | {map, key()} | {m, key()}
46 | {list, nth()} | {l, nth()}
47 | {tuple, nth()} | {t, nth()}
48 | nth() | key()
49 .
50
51 %% List of navigation commands that drills down into an obj().
52 -type path() :: [cmd()].
53
54 %% A shortened path returned by find/2 where each step is unambiguous.
55 -type short_path() :: [{m, key()} | {l|t, nth()}].
56
57 %% Predicate used by find/2. It can accept either the current value only
58 %% or the value together with its short path.
59 -type find_fun() :: fun((value())->boolean())
60 | fun((value(), short_path())->boolean())
61 .
62
63 %% Callback used by crud/3 to create, update or delete a value at the
64 %% given path.
65 -type crud_fun() :: fun((klsn:'maybe'(value()))->klsn:'maybe'(value())).
66
67
68 %% @doc
69 %% Safe navigation: returns {value, V} when the element exists, otherwise
70 %% none.
71 -spec lookup(path(), obj()) -> klsn:'maybe'(value()).
72 lookup(Path, Obj) ->
73 3 try get(Path, Obj) of
74 Value ->
75 1 {value, Value}
76 catch
77 error:not_found ->
78 2 none
79 end.
80
81 %% @doc
82 %% Navigate Obj using Path and return the value. Raises
83 %% error:not_found when any step is invalid.
84 -spec get(path(), obj()) -> value().
85 get([], Value) ->
86 30 Value;
87 get([H|T], Map) when is_map(Map) ->
88 33 Key = case H of
89 1 {raw, Key0} -> Key0;
90 1 {map, Key0} -> Key0;
91 26 {m, Key0} -> Key0;
92 1 {list, _} -> erlang:error(not_found, [[H|T], Map]);
93 1 {l, _} -> erlang:error(not_found, [[H|T], Map]);
94 1 {tuple, _} -> erlang:error(not_found, [[H|T], Map]);
95 1 {t, _} -> erlang:error(not_found, [[H|T], Map]);
96 1 Key0 -> Key0
97 end,
98 29 case maps:find(Key, Map) of
99 {ok, Value} ->
100 28 get(T, Value);
101 error ->
102 1 erlang:error(not_found, [[H|T], Map])
103 end;
104 get([H|T], List) when is_list(List) ->
105 24 Nth = case H of
106 1 {raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
107 1 {map, _} -> erlang:error(not_found, [[H|T], List]);
108 1 {m, _} -> erlang:error(not_found, [[H|T], List]);
109 2 {list, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
110 15 {l, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
111 1 {tuple, _} -> erlang:error(not_found, [[H|T], List]);
112 1 {t, _} -> erlang:error(not_found, [[H|T], List]);
113 1 Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
114 1 _ -> erlang:error(not_found, [[H|T], List])
115 end,
116 19 try lists:nth(Nth, List) of
117 Value ->
118 17 get(T, Value)
119 catch
120 error:function_clause ->
121 2 erlang:error(not_found, [[H|T], List])
122 end;
123 get([H|T], Tuple) when is_tuple(Tuple) ->
124 36 Nth = case H of
125 1 {raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
126 1 {map, _} -> erlang:error(not_found, [[H|T], Tuple]);
127 1 {m, _} -> erlang:error(not_found, [[H|T], Tuple]);
128 1 {list, _} -> erlang:error(not_found, [[H|T], Tuple]);
129 1 {l, _} -> erlang:error(not_found, [[H|T], Tuple]);
130 2 {tuple, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
131 24 {t, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
132 4 Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
133 1 _ -> erlang:error(not_found, [[H|T], Tuple])
134 end,
135 31 try element(Nth, Tuple) of
136 Value ->
137 28 get(T, Value)
138 catch
139 error:badarg ->
140 3 erlang:error(not_found, [[H|T], Tuple])
141 end;
142 get(Arg1, Arg2) ->
143 1 erlang:error(not_found, [Arg1, Arg2]).
144
145 %% @doc
146 %% Depth-first search for every occurrence that satisfies *FindFun*.
147 %% Returns a list of *short paths* to the matches.
148 -spec find(find_fun(), obj()) -> [short_path()].
149 find(FindFun, Obj) when is_function(FindFun, 1) ->
150 4 find(fun(Value, _Path) -> FindFun(Value) end, Obj);
151 find(FindFun, Obj) when is_function(FindFun, 2) ->
152 5 find_dfs(FindFun, Obj, []).
153
154 % Ignore the performance for now. Just get it to work.
155 -spec find_dfs(
156 fun((value(), short_path())->boolean())
157 , obj()
158 , path()
159 ) -> [short_path()].
160 find_dfs(FindFun, Obj, Path) ->
161 115 Acc = case FindFun(Obj, Path) of
162 true ->
163 33 [Path];
164 false ->
165 82 []
166 end,
167 115 case Obj of
168 Map when is_map(Map) ->
169 20 maps:fold(fun(Key, Elem, A)->
170 30 A ++ find_dfs(FindFun, Elem, Path ++ [{m, Key}])
171 end, Acc, Map);
172 List when is_list(List) ->
173 15 IList = lists:zip(lists:seq(1, length(List)), List),
174 15 lists:foldl(fun({I, Elem}, A)->
175 35 A ++ find_dfs(FindFun, Elem, Path ++ [{l, I}])
176 end, Acc, IList);
177 Tuple when is_tuple(Tuple) ->
178 20 List = tuple_to_list(Tuple),
179 20 IList = lists:zip(lists:seq(1, length(List)), List),
180 20 lists:foldl(fun({I, Elem}, A)->
181 45 A ++ find_dfs(FindFun, Elem, Path ++ [{t, I}])
182 end, Acc, IList);
183 _ ->
184 60 Acc
185 end.
186
187 %% @doc
188 %% "Create-Read-Update-Delete" helper. Applies CRUDFun on the existing
189 %% value (or none when the path is missing) and rebuilds the object with
190 %% the returned result.
191 -spec crud(path(), crud_fun(), obj()) -> obj().
192 crud(Path, CRUDFun, Obj) ->
193 25 {PathLeft, MaybeValue, History} = try crud_history(Path, Obj, []) catch
194 throw:{?MODULE, Args} ->
195 14 Args
196 end,
197 25 MaybeUpdatedValue = CRUDFun(MaybeValue),
198 25 case MaybeUpdatedValue of
199 {value, _} ->
200 7 ok;
201 none ->
202 17 ok;
203 Other ->
204 1 erlang:error({bad_return, #{type => {?MODULE, crud_fun, 0}, return => Other, msg => <<"Return of klsn_obj:crud_fun() must be `{value, klsn_obj:value()}` or `none`.">>}})
205 end,
206 24 crud_build(lists:reverse(PathLeft), MaybeUpdatedValue, History).
207
208
209 -spec crud_history(
210 path(), obj(), [{short_path(), obj()}]
211 ) -> {path(), klsn:'maybe'(value()), [{short_path(), obj()}]}.
212 crud_history([], Value, History) ->
213 7 {[], {value, Value}, History};
214 crud_history([H|T], Map, History) when is_map(Map) ->
215 15 Key = case H of
216 1 {raw, Key0} -> Key0;
217 1 {map, Key0} -> Key0;
218 8 {m, Key0} -> Key0;
219 1 {list, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
220 1 {l, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
221 1 {tuple, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
222 1 {t, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
223 1 Key0 -> Key0
224 end,
225 11 case maps:find(Key, Map) of
226 {ok, Value} ->
227 10 crud_history(T, Value, [{{m,Key},Map}|History]);
228 error ->
229 1 {T, none, [{{m,Key},Map}|History]}
230 end;
231 crud_history([H|T], List, History) when is_list(List) ->
232 9 Nth = case H of
233 1 {raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
234 1 {map, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
235 1 {m, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
236 1 {list, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
237 1 {l, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
238 1 {tuple, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
239 1 {t, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
240 1 Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
241 1 _ -> erlang:throw({?MODULE, {[H|T], none, History}})
242 end,
243 4 try lists:nth(Nth, List) of
244 Value ->
245 3 crud_history(T, Value, [{{l,Nth},List}|History])
246 catch
247 error:function_clause ->
248 1 {T, none, [{{l,Nth},List}|History]}
249 end;
250 crud_history([H|T], Tuple, History) when is_tuple(Tuple) ->
251 11 Nth = case H of
252 1 {raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
253 1 {map, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
254 1 {m, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
255 1 {list, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
256 1 {l, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
257 1 {tuple, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
258 3 {t, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
259 1 Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
260 1 _ -> erlang:throw({?MODULE, {[H|T], none, History}})
261 end,
262 6 try element(Nth, Tuple) of
263 Value ->
264 5 crud_history(T, Value, [{{t,Nth},Tuple}|History])
265 catch
266 error:badarg ->
267 1 {T, none, [{{t,Nth},Tuple}|History]}
268 end;
269 crud_history(Path, _Value, History) ->
270 1 {Path, none, History}.
271
272
273 -spec crud_build(
274 klsn:'maybe'(obj())
275 , [{short_path(), obj()}]
276 ) -> obj().
277 crud_build(none, []) ->
278 13 nil;
279 crud_build({value, Obj}, []) ->
280 11 Obj;
281 crud_build(none, [{{m,Key},Map}|Tail]) when is_map(Map) ->
282 1 crud_build({value, maps:remove(Key, Map)}, Tail);
283 crud_build(none, [{{l,Nth},List}|Tail]) when is_list(List) ->
284 1 crud_build({value, delete_nth(Nth, List)}, Tail);
285 crud_build(none, [{{t,Nth},Tuple}|Tail]) when is_tuple(Tuple) ->
286 2 crud_build({value, delete_nth(Nth, Tuple)}, Tail);
287 crud_build({value, Value}, [{{m,Key},Map}|Tail]) when is_map(Map) ->
288 9 crud_build({value, Map#{ Key => Value }}, Tail);
289 crud_build({value, Value}, [{{l,Nth},List}|Tail]) when is_list(List) ->
290 3 crud_build({value, replace_nth(Nth, Value, List)}, Tail);
291 crud_build({value, Value}, [{{t,Nth},Tuple}|Tail]) when is_tuple(Tuple) ->
292 4 crud_build({value, replace_nth(Nth, Value, Tuple)}, Tail).
293
294 -spec crud_build(
295 Reversed::path()
296 , klsn:'maybe'(value())
297 , [{short_path(), obj()}]
298 ) -> obj().
299 crud_build(_, none, History) ->
300 17 crud_build(none, History);
301 crud_build([], MaybeValue, History) ->
302 7 crud_build(MaybeValue, History);
303 crud_build([Cmd|Tail], {value, Value}, History) ->
304 9 ShortCmd = case Cmd of
305 1 {raw, Key0} -> {m,Key0};
306 1 {map, Key0} -> {m,Key0};
307 1 {m, Key0} -> {m,Key0};
308 1 {list, Key0} when is_integer(Key0), (Key0 > 0) -> {l,Key0};
309 2 {l, Key0} when is_integer(Key0), (Key0 > 0) -> {l,Key0};
310 1 {tuple, Key0} when is_integer(Key0), (Key0 > 0) -> {t,Key0};
311 1 {t, Key0} when is_integer(Key0), (Key0 > 0) -> {t,Key0};
312 1 Key0 -> {m,Key0}
313 end,
314 9 UpdatedValue = case ShortCmd of
315 {m,Key} ->
316 4 #{ Key => Value };
317 {l,Nth} ->
318 3 replace_nth(Nth, Value, []);
319 {t,Nth} ->
320 2 replace_nth(Nth, Value, {})
321 end,
322 9 crud_build(Tail, {value, UpdatedValue}, History).
323
324
325 replace_nth(Index, NewElement, Tuple) when is_tuple(Tuple) ->
326 6 list_to_tuple(replace_nth(Index, NewElement, tuple_to_list(Tuple)));
327 replace_nth(Index, NewElement, List) when is_integer(Index), Index >= 1, is_list(List) ->
328 12 ListLen = length(List),
329 12 case Index =< ListLen of
330 true ->
331 6 Prefix = lists:sublist(List, Index - 1),
332 6 Tail = lists:nthtail(Index, List),
333 6 Prefix ++ [NewElement] ++ Tail;
334 false ->
335 6 PadLen = Index - ListLen - 1,
336 6 NilPad = lists:duplicate(PadLen, nil),
337 6 List ++ NilPad ++ [NewElement]
338 end.
339
340
341 delete_nth(Index, Tuple) when is_tuple(Tuple) ->
342 2 list_to_tuple(delete_nth(Index, tuple_to_list(Tuple)));
343 delete_nth(Index, List) when is_integer(Index), Index >= 1, is_list(List) ->
344 3 ListLen = length(List),
345 3 case Index =< ListLen of
346 true ->
347 2 Prefix = lists:sublist(List, Index - 1),
348 2 Tail = lists:nthtail(Index, List),
349 2 Prefix ++ Tail;
350 false ->
351 1 List
352 end.
353
354
355
Line Hits Source