Current section

Files

Jump to
closure_table lib cte.ex
Raw

lib/cte.ex

defmodule CTE do
@moduledoc """
The Closure Table for Elixir strategy, CTE for short, is a simple and elegant way of storing and working with hierarchies. It involves storing all paths through a tree, not just those with a direct parent-child relationship. You may want to chose this model, over the [Nested Sets model](https://en.wikipedia.org/wiki/Nested_set_model), should you need referential integrity and to assign nodes to multiple trees.
With CTE you can navigate through hierarchies using a simple [API](CTE.Adapter.html#functions). You can find the ascendants and descendants of a node, you can insert and delete nodes, move entire sub-trees or print them as digraph (.dot) files.
### Quick example.
For this example we're using the (in) [Memory Adapter](CTE.Adapter.Memory.html#content) of CTE, useful when we want to prototype of we have data structures that can fit in memory and their persistence is already taken care of. For more involved use cases, CTE integrates with Ecto using a simple API.
When used from a module, the CTE expects the :otp_app and :adapter as option. The :otp_app should point to an OTP application that might provide additional configuration. Equally important are the `:nodes` and the `:paths` options. The `:nodes`, in the case of the [Memory Adapter](CTE.Adapter.Memory.html#content), is a Map defining your nodes and the `:paths` a list containing the tree path between the nodes. For example:
defmodule CTM do
use CTE,
otp_app: :ct_empty,
adapter: CTE.Adapter.Memory,
nodes: %{
1 => %{id: 1, author: "Olie", comment: "Is Closure Table better than the Nested Sets?"},
2 => %{id: 2, author: "Rolie", comment: "It depends. Do you need referential integrity?"},
3 => %{id: 3, author: "Polie", comment: "Yeah."}
},
paths: [[1, 1], [1, 2], [1, 3], [2, 2], [2, 3], [3, 3]]
end
With the configuration above, the `:nodes` attribute is a map containing the comments our interlocutors made; these are "nodes", in CTE's parlance. When using the `CTE.Adapter.Ecto` implementation, the `:nodes` attribute will be a Schema (or a table name! In our initial implementation, the nodes definitions must have at least the `:id`, as one of their properties. This caveat will be lifted in a later implementation. The `:paths` attribute represents the parent-child relationship between the comments.
Add the `CTM` module to your main supervision tree:
defmodule CTM.Application do
@moduledoc false
use Application
def start(_type, _args) do
opts = [strategy: :one_for_one, name: CTM.Supervisor]
Supervisor.start_link([CTM], opts)
end
end
Using `iex -S mix`, for quickly experimenting with the CTE API:
- find the descendants of comment #1
```elixir
iex» CTM.descendants(1)
{:ok, [3, 2]}
```
- find the ancestors
```elixir
iex» CTM.ancestors(2)
{:ok, [1]}
iex» CTM.ancestors(3)
{:ok, [1]}
```
- find the ancestors, with information about the node:
```elixir
iex» CTM.ancestors(2, nodes: true)
{:ok,
[
%{
author: "Olie",
comment: "Is Closure Table better than the Nested Sets?",
id: 1
}
]}
```
- move leafs/subtrees around. From being a child of comment #1, to becoming a
child of comment #2, in the following example:
```elixir
iex» CTM.move(3, 2, limit: 1)
:ok
iex» CTM.descendants(2)
{:ok, [3]}
```
Please check the docs, the tests, and the examples folder, for more details.
"""
@type config :: Keyword.t()
@type table :: String.t() | atom
@type nodes :: map() | table
@type paths :: [list()] | table
@type repo :: Ecto.Repo
@type t :: %__MODULE__{
adapter: any() | nil,
nodes: nodes | nil,
paths: paths | nil,
repo: repo | nil
}
defstruct [:nodes, :paths, :adapter, :repo]
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@default_adapter CTE.Adapter.Memory
@default_config [nodes: [], paths: [], adapter: @default_adapter, repo: nil]
@default_dynamic_supervisor opts[:default_dynamic_supervisor] || __MODULE__
@otp_app Keyword.fetch!(opts, :otp_app)
@adapter Keyword.fetch!(opts, :adapter)
@opts opts
@doc false
def config(), do: parse_config(@opts)
@doc false
def __adapter__ do
{{:repo, _repo}, %{pid: adapter}} = CTE.Registry.lookup(get_dynamic_supervisor())
adapter
end
@doc false
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :supervisor
}
end
@doc false
def start_link(opts \\ []) do
CTE.Supervisor.start_link(__MODULE__, @otp_app, @adapter, config())
end
@compile {:inline, get_dynamic_supervisor: 0}
def get_dynamic_supervisor() do
Process.get({__MODULE__, :dynamic_supervisor}, @default_dynamic_supervisor)
end
def put_dynamic_supervisor(dynamic) when is_atom(dynamic) or is_pid(dynamic) do
Process.put({__MODULE__, :dynamic_supervisor}, dynamic) || @default_dynamic_supervisor
end
def insert(leaf, ancestor, opts \\ [])
def insert(leaf, ancestor, opts), do: @adapter.insert(__adapter__(), leaf, ancestor, opts)
def tree(leaf, opts \\ [])
def tree(leaf, opts), do: @adapter.tree(__adapter__(), leaf, opts)
def ancestors(descendant, opts \\ [])
def ancestors(descendant, opts), do: @adapter.ancestors(__adapter__(), descendant, opts)
def descendants(ancestor, opts \\ [])
def descendants(ancestor, opts), do: @adapter.descendants(__adapter__(), ancestor, opts)
@doc """
when limit: 1, the default value, then delete only the leafs, else the entire subtree
"""
def delete(leaf, ops \\ [])
def delete(leaf, opts), do: @adapter.delete(__adapter__(), leaf, opts)
def move(leaf, ancestor, opts \\ [])
def move(leaf, ancestor, opts), do: @adapter.move(__adapter__(), leaf, ancestor, opts)
defp parse_config(config), do: CTE.parse_config(@otp_app, __MODULE__, @opts, config)
end
end
@doc false
def parse_config(otp_app, adapter, adapter_config, dynamic_config) do
conf =
Application.get_env(otp_app, adapter, [])
|> Keyword.merge(adapter_config)
|> Keyword.merge(dynamic_config)
|> CTE.interpolate_env_vars()
%CTE{
nodes: Keyword.get(conf, :nodes, []),
paths: Keyword.get(conf, :paths, []),
repo: Keyword.get(conf, :repo, nil),
adapter: Keyword.get(conf, :adapter)
}
end
@doc false
def interpolate_env_vars(config) do
Enum.map(config, fn
{key, {:system, env_var}} -> {key, System.get_env(env_var)}
{key, value} -> {key, value}
end)
end
end