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 @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