/home/runner/work/klsn/klsn/_build/test/cover/ct/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
:-(
try get(Path, Obj) of
74 Value ->
75
:-(
{value, Value}
76 catch
77 error:not_found ->
78
:-(
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
:-(
Value;
87 get([H|T], Map) when is_map(Map) ->
88
:-(
Key = case H of
89
:-(
{raw, Key0} -> Key0;
90
:-(
{map, Key0} -> Key0;
91
:-(
{m, Key0} -> Key0;
92
:-(
{list, _} -> erlang:error(not_found, [[H|T], Map]);
93
:-(
{l, _} -> erlang:error(not_found, [[H|T], Map]);
94
:-(
{tuple, _} -> erlang:error(not_found, [[H|T], Map]);
95
:-(
{t, _} -> erlang:error(not_found, [[H|T], Map]);
96
:-(
Key0 -> Key0
97 end,
98
:-(
case maps:find(Key, Map) of
99 {ok, Value} ->
100
:-(
get(T, Value);
101 error ->
102
:-(
erlang:error(not_found, [[H|T], Map])
103 end;
104 get([H|T], List) when is_list(List) ->
105
:-(
Nth = case H of
106
:-(
{raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
107
:-(
{map, _} -> erlang:error(not_found, [[H|T], List]);
108
:-(
{m, _} -> erlang:error(not_found, [[H|T], List]);
109
:-(
{list, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
110
:-(
{l, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
111
:-(
{tuple, _} -> erlang:error(not_found, [[H|T], List]);
112
:-(
{t, _} -> erlang:error(not_found, [[H|T], List]);
113
:-(
Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
114
:-(
_ -> erlang:error(not_found, [[H|T], List])
115 end,
116
:-(
try lists:nth(Nth, List) of
117 Value ->
118
:-(
get(T, Value)
119 catch
120 error:function_clause ->
121
:-(
erlang:error(not_found, [[H|T], List])
122 end;
123 get([H|T], Tuple) when is_tuple(Tuple) ->
124
:-(
Nth = case H of
125
:-(
{raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
126
:-(
{map, _} -> erlang:error(not_found, [[H|T], Tuple]);
127
:-(
{m, _} -> erlang:error(not_found, [[H|T], Tuple]);
128
:-(
{list, _} -> erlang:error(not_found, [[H|T], Tuple]);
129
:-(
{l, _} -> erlang:error(not_found, [[H|T], Tuple]);
130
:-(
{tuple, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
131
:-(
{t, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
132
:-(
Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
133
:-(
_ -> erlang:error(not_found, [[H|T], Tuple])
134 end,
135
:-(
try element(Nth, Tuple) of
136 Value ->
137
:-(
get(T, Value)
138 catch
139 error:badarg ->
140
:-(
erlang:error(not_found, [[H|T], Tuple])
141 end;
142 get(Arg1, Arg2) ->
143
:-(
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
:-(
find(fun(Value, _Path) -> FindFun(Value) end, Obj);
151 find(FindFun, Obj) when is_function(FindFun, 2) ->
152
:-(
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
:-(
Acc = case FindFun(Obj, Path) of
162 true ->
163
:-(
[Path];
164 false ->
165
:-(
[]
166 end,
167
:-(
case Obj of
168 Map when is_map(Map) ->
169
:-(
maps:fold(fun(Key, Elem, A)->
170
:-(
A ++ find_dfs(FindFun, Elem, Path ++ [{m, Key}])
171 end, Acc, Map);
172 List when is_list(List) ->
173
:-(
IList = lists:zip(lists:seq(1, length(List)), List),
174
:-(
lists:foldl(fun({I, Elem}, A)->
175
:-(
A ++ find_dfs(FindFun, Elem, Path ++ [{l, I}])
176 end, Acc, IList);
177 Tuple when is_tuple(Tuple) ->
178
:-(
List = tuple_to_list(Tuple),
179
:-(
IList = lists:zip(lists:seq(1, length(List)), List),
180
:-(
lists:foldl(fun({I, Elem}, A)->
181
:-(
A ++ find_dfs(FindFun, Elem, Path ++ [{t, I}])
182 end, Acc, IList);
183 _ ->
184
:-(
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
:-(
{PathLeft, MaybeValue, History} = try crud_history(Path, Obj, []) catch
194 throw:{?MODULE, Args} ->
195
:-(
Args
196 end,
197
:-(
MaybeUpdatedValue = CRUDFun(MaybeValue),
198
:-(
case MaybeUpdatedValue of
199 {value, _} ->
200
:-(
ok;
201 none ->
202
:-(
ok;
203 Other ->
204
:-(
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
:-(
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
:-(
{[], {value, Value}, History};
214 crud_history([H|T], Map, History) when is_map(Map) ->
215
:-(
Key = case H of
216
:-(
{raw, Key0} -> Key0;
217
:-(
{map, Key0} -> Key0;
218
:-(
{m, Key0} -> Key0;
219
:-(
{list, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
220
:-(
{l, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
221
:-(
{tuple, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
222
:-(
{t, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
223
:-(
Key0 -> Key0
224 end,
225
:-(
case maps:find(Key, Map) of
226 {ok, Value} ->
227
:-(
crud_history(T, Value, [{{m,Key},Map}|History]);
228 error ->
229
:-(
{T, none, [{{m,Key},Map}|History]}
230 end;
231 crud_history([H|T], List, History) when is_list(List) ->
232
:-(
Nth = case H of
233
:-(
{raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
234
:-(
{map, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
235
:-(
{m, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
236
:-(
{list, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
237
:-(
{l, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
238
:-(
{tuple, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
239
:-(
{t, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
240
:-(
Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
241
:-(
_ -> erlang:throw({?MODULE, {[H|T], none, History}})
242 end,
243
:-(
try lists:nth(Nth, List) of
244 Value ->
245
:-(
crud_history(T, Value, [{{l,Nth},List}|History])
246 catch
247 error:function_clause ->
248
:-(
{T, none, [{{l,Nth},List}|History]}
249 end;
250 crud_history([H|T], Tuple, History) when is_tuple(Tuple) ->
251
:-(
Nth = case H of
252
:-(
{raw, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
253
:-(
{map, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
254
:-(
{m, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
255
:-(
{list, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
256
:-(
{l, _} -> erlang:throw({?MODULE, {[H|T], none, History}});
257
:-(
{tuple, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
258
:-(
{t, Key0} when is_integer(Key0), (Key0 > 0) -> Key0;
259
:-(
Key0 when is_integer(Key0), (Key0 > 0) -> Key0;
260
:-(
_ -> erlang:throw({?MODULE, {[H|T], none, History}})
261 end,
262
:-(
try element(Nth, Tuple) of
263 Value ->
264
:-(
crud_history(T, Value, [{{t,Nth},Tuple}|History])
265 catch
266 error:badarg ->
267
:-(
{T, none, [{{t,Nth},Tuple}|History]}
268 end;
269 crud_history(Path, _Value, History) ->
270
:-(
{Path, none, History}.
271
272
273 -spec crud_build(
274 klsn:'maybe'(obj())
275 , [{short_path(), obj()}]
276 ) -> obj().
277 crud_build(none, []) ->
278
:-(
nil;
279 crud_build({value, Obj}, []) ->
280
:-(
Obj;
281 crud_build(none, [{{m,Key},Map}|Tail]) when is_map(Map) ->
282
:-(
crud_build({value, maps:remove(Key, Map)}, Tail);
283 crud_build(none, [{{l,Nth},List}|Tail]) when is_list(List) ->
284
:-(
crud_build({value, delete_nth(Nth, List)}, Tail);
285 crud_build(none, [{{t,Nth},Tuple}|Tail]) when is_tuple(Tuple) ->
286
:-(
crud_build({value, delete_nth(Nth, Tuple)}, Tail);
287 crud_build({value, Value}, [{{m,Key},Map}|Tail]) when is_map(Map) ->
288
:-(
crud_build({value, Map#{ Key => Value }}, Tail);
289 crud_build({value, Value}, [{{l,Nth},List}|Tail]) when is_list(List) ->
290
:-(
crud_build({value, replace_nth(Nth, Value, List)}, Tail);
291 crud_build({value, Value}, [{{t,Nth},Tuple}|Tail]) when is_tuple(Tuple) ->
292
:-(
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
:-(
crud_build(none, History);
301 crud_build([], MaybeValue, History) ->
302
:-(
crud_build(MaybeValue, History);
303 crud_build([Cmd|Tail], {value, Value}, History) ->
304
:-(
ShortCmd = case Cmd of
305
:-(
{raw, Key0} -> {m,Key0};
306
:-(
{map, Key0} -> {m,Key0};
307
:-(
{m, Key0} -> {m,Key0};
308
:-(
{list, Key0} when is_integer(Key0), (Key0 > 0) -> {l,Key0};
309
:-(
{l, Key0} when is_integer(Key0), (Key0 > 0) -> {l,Key0};
310
:-(
{tuple, Key0} when is_integer(Key0), (Key0 > 0) -> {t,Key0};
311
:-(
{t, Key0} when is_integer(Key0), (Key0 > 0) -> {t,Key0};
312
:-(
Key0 -> {m,Key0}
313 end,
314
:-(
UpdatedValue = case ShortCmd of
315 {m,Key} ->
316
:-(
#{ Key => Value };
317 {l,Nth} ->
318
:-(
replace_nth(Nth, Value, []);
319 {t,Nth} ->
320
:-(
replace_nth(Nth, Value, {})
321 end,
322
:-(
crud_build(Tail, {value, UpdatedValue}, History).
323
324
325 replace_nth(Index, NewElement, Tuple) when is_tuple(Tuple) ->
326
:-(
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
:-(
ListLen = length(List),
329
:-(
case Index =< ListLen of
330 true ->
331
:-(
Prefix = lists:sublist(List, Index - 1),
332
:-(
Tail = lists:nthtail(Index, List),
333
:-(
Prefix ++ [NewElement] ++ Tail;
334 false ->
335
:-(
PadLen = Index - ListLen - 1,
336
:-(
NilPad = lists:duplicate(PadLen, nil),
337
:-(
List ++ NilPad ++ [NewElement]
338 end.
339
340
341 delete_nth(Index, Tuple) when is_tuple(Tuple) ->
342
:-(
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
:-(
ListLen = length(List),
345
:-(
case Index =< ListLen of
346 true ->
347
:-(
Prefix = lists:sublist(List, Index - 1),
348
:-(
Tail = lists:nthtail(Index, List),
349
:-(
Prefix ++ Tail;
350 false ->
351
:-(
List
352 end.
353
354
355
Line Hits Source