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), such as: finding the ascendants and descendants of a node, inserting and deleting nodes, moving entire sub-trees or print them as a digraph (.dot) file.
### Quick example.
For this example we're using the in-[Memory Adapter](CTE.Adapter.Memory.html#content). This `Adapter` is useful for prototyping or with data structures that can easily fit in memory; their persistence being taken care of by other components. 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` attributes, to be defined. The `:otp_app` should point to an OTP application that might provide additional configuration. Equally so are the: `:nodes` and the `:paths` attributes. The `:nodes` attribute, in the case of the [Memory Adapter](CTE.Adapter.Memory.html#content), is a Map defining your nodes while the: `:paths` attribute, is a list containing the tree path between the nodes - a list of lists. 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
When using the `CTE.Adapter.Ecto`, the: `:nodes` attribute, will be a Schema i.e. `Post`, `TreePath`, etc! 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.
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 | map()
@type name :: String.t() | atom
@type t :: %__MODULE__{
adapter: any() | nil,
nodes: nodes | nil,
paths: paths | nil,
repo: repo | nil,
name: name | nil
}
defstruct [:nodes, :paths, :adapter, :repo, :name]
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] || opts[:name] || __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),
name: Keyword.get(conf, :name)
}
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