Current section
Files
Jump to
Current section
Files
src/nm.erl
-module(nm).
-moduledoc """
`nm` is tool to help handling deeply nested structure.
It works by unnesting your data into a ets table, allowing you to handle transformations in a simple
and easy way.
Once you finished manipulating your data in the way you want, it allows you to nest the data back
with all the applied changes.
It provides simple operations to get, update and delete values in a meaninful way.
To keep it as simple as possible, initially we're considering only maps and simple lists as
nesteable structures.
When unnesting, everytime `nm` finds a nesteable structure, it gonna replace it by `t:nm_ref/0`
and also unnest it recursevely.
So for exemple a list of maps like `[#{}, #{}]`, would be unnested to
`[{'$nm_ref', Id1}, {'$nm_ref', Id2}]`.
This can help handling deeply nested structures because when transforming some data referencing
it by it's `t:nested_id/0`, you don't need to update all the parent structures for this change
to take efffect.
After all transformations you can nest the structure again and use it externaly to your system.
Keep in mind that proplists and tuples won't be unnested, so if `nm` finds a tuple while unnesting
it gonna consider it a literal value for a thing.
For exemlo, now let's think of a list with a map and a tuple `[#{}, {1, 2 , 3}]`, after unnesting it
you gonna have `[{'$nm_ref', Id}, {1, 2, 3}]`.
""".
-define(is_nested(D), is_map(D); is_list(D)).
-export([
delete/2,
fast_delete/2,
fast_update/3,
get_node/2,
get_value/2,
nest/1,
partial_nest/2,
unnest/1,
update/3
]).
-export_type([
nested_table/0,
nested_structure/0,
unnested_structure/0,
nm_node/0,
nm_ref/0,
nested_id/0
]).
-doc "An ETS table configured as `set` and `protected` with the unnested structure.".
-type nested_table() :: ets:table().
-doc "A nested structure of `maps` and `lists`.".
-type nested_structure() :: map() | list().
-doc """
A unnested strucutre, any value that was another nested_structure is replaced by `t:nm_ref/0`.
""".
-type unnested_structure() :: map() | list().
-doc """
A map with the value of an `t:unnested_structure/0` and it's metadata.
- `ref`: the `t:nested_id/0` of the structure.
- `parent`: the `t:nested_id/0` of the parent structure.
- `value`: the actual `t:unnested_structure/0` value.
""".
-type nm_node() :: #{ref => nested_id(), parent => nested_id(), value => unnested_structure()}.
-doc "A tag to reference a value that was unnested".
-type nm_ref() :: {'$nm_ref', nested_id()}.
-doc """
Identifiers for nested structure.
`root` is the begining of the nested structure, all other inner structures are binaries.
`null` is used only as parent for `root`.
""".
-type nested_id() :: root | null | binary().
-doc """
Removes the node identified by the id.
Also remove the reference to the node in the parent and all the subtrees of children of the node.
""".
-spec delete(nested_table(), nested_id()) -> ok | {error, atom()}.
delete(NestedTable, Id) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:delete(NestedTable, Id);
Error -> Error
end.
-doc """
""".
-spec fast_delete(nested_table(), nested_id()) -> ok | {error, atom()}.
fast_delete(NestedTable, Id) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:fast_delete(NestedTable, Id);
Error -> Error
end.
-doc """
Update a node of the table with a new value.
This operation will discard all children of the node and recursevely unnest all it's values.
Can be used in conjunction with `nm:partial_nest/2` to change entire subtrees.
In case you're changing just literals and not adding or removing a nested data structure,
you might want to sue `nm:fast_update/3`.
""".
-spec update(nested_table(), nested_id(), nested_structure()) -> ok | {error, atom()}.
update(NestedTable, Id, Value) when ?is_nested(Value) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:update(NestedTable, Id, Value);
Error -> Error
end.
-doc """
Update a node without removing children nodes or unnesting all it's values.
Useful when you want to do simple updates that doesn't change relation with children nodes.
For example, adding a new field to a map that is a literal or changing the order of a list.
""".
-spec fast_update(nested_table(), nested_id(), unnested_structure()) -> ok | {error, atom()}.
fast_update(NestedTable, Id, Value) when ?is_nested(Value) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:fast_update(NestedTable, Id, Value);
Error -> Error
end.
-doc """
Returns a node of the table.
The node contains the actual value and metadata as the current node id and the parent node id.
""".
-spec get_node(nested_table(), nested_id()) -> {ok, nm_node()} | {error, atom()}.
get_node(NestedTable, Id) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:get_node(NestedTable, Id);
Error -> Error
end.
-doc """
Get the actual value of the node.
""".
-spec get_value(nested_table(), nested_id()) -> {ok, unnested_structure()} | {error, atom()}.
get_value(NestedTable, Id) ->
case validate_args(NestedTable, Id) of
ok -> nm_node:get_value(NestedTable, Id);
Error -> Error
end.
-doc """
It nests the entire table back into a nested structure.
Re-nesting the table won't remove anything from the table.
""".
-spec nest(nested_table()) -> {ok, nested_structure()} | {error, atom()}.
nest(NestedTable) ->
case is_table(NestedTable) of
true -> {ok, nm_nesting:nest(NestedTable)};
false -> {error, not_a_table}
end.
-doc """
It nests just a subtree of the table, starting from the id provided.
Just as `nm:nest/1`, re-nesting the information won't remove it from the table.
This is useful when you want to modify an entire subtreee of the data.
""".
-spec partial_nest(nested_table(), nested_id()) -> {ok, nested_structure()} | {error, atom()}.
partial_nest(NestedTable, Id) ->
case validate_args(NestedTable, Id) of
ok ->
case nm_node:get_value(NestedTable, Id) of
{ok, Data} -> {ok, nm_nesting:nest(NestedTable, Data)};
Error -> Error
end;
Error ->
Error
end.
-doc """
Take a nested data structure and unnest it into a ets table.
""".
-spec unnest(nested_structure()) -> {ok, nested_table()} | {error, atom()}.
unnest(Data) when ?is_nested(Data) ->
{ok, nm_nesting:unnest(Data)};
unnest(_Data) ->
{error, invalid_data}.
is_table(Table) ->
try ets:info(Table, name) of
'$nm_table' -> true;
_ -> false
catch
_:_ -> false
end.
is_id(Id) ->
case Id of
<<"$nm", _Rest/binary>> -> true;
root -> true;
_ -> false
end.
validate_args(Table, Id) ->
case is_table(Table) of
true ->
case is_id(Id) of
true -> ok;
false -> {error, not_an_id}
end;
false ->
{error, not_a_table}
end.