# `Athanor.Tree`
[🔗](https://github.com/Arsenalist/athanor/blob/v0.1.0-beta.7/lib/athanor/tree.ex#L1)

Pure-data tree operations over the page builder JSON shape.

See `Athanor` for the project boundary contract. This module accepts and
returns plain Elixir maps with string keys — no structs, no JSON encoding,
no Phoenix/Ecto/Amplify dependencies.

## Shape

A tree is a map with two well-known keys:

    %{
      "metadata" => map(),
      "content"  => [node, ...]
    }

A node is a map:

    %{
      "id"    => "uuid-string",
      "type"  => "component-type-string",
      "props" => map()
    }

A node MAY declare children via `node["props"]["zones"]` as a map of
`%{zone_name => [child_node, ...]}`. Tree operations walk these zones
automatically — no registry or callback required. Unknown keys at any
level (top-level, props, metadata) are preserved on round-trip.

# `find`

Locate a node by its `id`, searching the root content and recursively
through every `props["zones"]`.

Returns `{:ok, node}` or `:error` when no node has the given id.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [
    ...>   %{"id" => "a", "type" => "text", "props" => %{}}
    ...> ]}
    iex> {:ok, node} = Athanor.Tree.find(tree, "a")
    iex> node["type"]
    "text"

    iex> Athanor.Tree.find(%{"metadata" => %{}, "content" => []}, "missing")
    :error

# `from_json`

Normalize a decoded JSON value into the canonical tree shape, filling in
`metadata` and `content` defaults and preserving every other key untouched.

Accepts `nil` to mean "empty tree".

## Examples

    iex> Athanor.Tree.from_json(nil)
    %{"metadata" => %{}, "content" => []}

    iex> Athanor.Tree.from_json(%{"content" => []})
    %{"metadata" => %{}, "content" => []}

    iex> Athanor.Tree.from_json(%{"metadata" => %{"title" => "Hi"}})
    %{"metadata" => %{"title" => "Hi"}, "content" => []}

# `insert`

Insert `node` into the tree at the given target.

Targets:
- `:root` — into the top-level `content` list
- `{parent_id, zone_name}` — into a specific zone of a specific parent node

Options:
- `:at` — `:append` (default), `:prepend`, `{:index, n}`, `{:after, sibling_id}`

Returns `{:ok, new_tree}` or `{:error, reason}` where reason is one of
`:parent_not_found`, `:zone_not_found`, or `:sibling_not_found`.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [%{"id" => "a", "type" => "text", "props" => %{}}]}
    iex> n = %{"id" => "b", "type" => "text", "props" => %{}}
    iex> {:ok, t2} = Athanor.Tree.insert(tree, :root, n)
    iex> Enum.map(t2["content"], & &1["id"])
    ["a", "b"]

# `move`

Swap the node identified by `id` with its previous (`:up`) or next
(`:down`) sibling inside the same containing list (root content or a
specific zone). Moving past a boundary (first/last) is a no-op.

Returns `{:ok, new_tree}` or `{:error, :not_found}` if the id is absent.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [
    ...>   %{"id" => "a", "type" => "text", "props" => %{}},
    ...>   %{"id" => "b", "type" => "text", "props" => %{}}
    ...> ]}
    iex> {:ok, t2} = Athanor.Tree.move(tree, "b", :up)
    iex> Enum.map(t2["content"], & &1["id"])
    ["b", "a"]

# `move_to`

Move the node with `node_id` from anywhere in the tree to `target`.

`target` is `:root` (top-level content list) or `{parent_id, zone_name}`
(a specific zone of a container node). `opts` accepts the same `:at`
values as `insert/4` — `:append` (default), `:prepend`, `{:index, n}`,
`{:after, sibling_id}`.

Implemented as a find + remove + insert. Atomic: if the insert fails
(e.g. `:parent_not_found`), the original tree is returned unchanged
via the error tuple. Idempotent when the resulting position equals
the original — returns the input tree byte-equal.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [
    ...>   %{"id" => "a", "type" => "text", "props" => %{}},
    ...>   %{"id" => "b", "type" => "text", "props" => %{}}
    ...> ]}
    iex> {:ok, t2} = Athanor.Tree.move_to(tree, "a", :root, at: {:index, 1})
    iex> Enum.map(t2["content"], & &1["id"])
    ["b", "a"]

# `remove`

Remove the node with the given `id` from anywhere in the tree.

Idempotent: removing an unknown id returns `{:ok, tree}` unchanged.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [%{"id" => "a", "type" => "text", "props" => %{}}]}
    iex> {:ok, t2} = Athanor.Tree.remove(tree, "a")
    iex> t2["content"]
    []

    iex> tree = %{"metadata" => %{}, "content" => [%{"id" => "a", "type" => "text", "props" => %{}}]}
    iex> {:ok, t2} = Athanor.Tree.remove(tree, "ghost")
    iex> t2 == tree
    true

# `to_json`

Serialize a tree back to a JSON-encodable map.

Currently an identity function over the canonical shape — the tree is
already a plain map. Exists as a paired entry point so callers can
always pipe `from_json |> ... |> to_json` without thinking about
whether the intermediate ops left the shape decoded or encoded.

## Examples

    iex> Athanor.Tree.to_json(%{"metadata" => %{}, "content" => []})
    %{"metadata" => %{}, "content" => []}

# `update_props`

Update the `props` of the node identified by `id`.

The third argument may be either a map (shallowly merged into the
current props) or a function `(current_props -> new_props)`.

Returns `{:ok, new_tree}` or `{:error, :not_found}` if the id is absent.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [
    ...>   %{"id" => "a", "type" => "text", "props" => %{"text" => "old"}}
    ...> ]}
    iex> {:ok, t2} = Athanor.Tree.update_props(tree, "a", %{"text" => "new"})
    iex> {:ok, node} = Athanor.Tree.find(t2, "a")
    iex> node["props"]["text"]
    "new"

# `walk`

Walk every node in the tree, invoking `fun.(node, acc)` for each.

Visit order is pre-order: a parent node is visited before its children,
and children are visited left-to-right within their containing list. Zone
iteration order follows the underlying map's iteration order, which for
Erlang maps is insertion-stable for small maps and undefined for large
ones. In practice page builder zones are small.

Children are discovered via the convention `node["props"]["zones"]`
being a `%{zone_name => [child_node, ...]}` map. Nodes whose `zones` is
absent or not a map are treated as leaves.

## Examples

    iex> tree = %{"metadata" => %{}, "content" => [
    ...>   %{"id" => "a", "type" => "text", "props" => %{}},
    ...>   %{"id" => "b", "type" => "text", "props" => %{}}
    ...> ]}
    iex> Athanor.Tree.walk(tree, [], fn n, acc -> [n["id"] | acc] end) |> Enum.reverse()
    ["a", "b"]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
