Current section
Files
Jump to
Current section
Files
lib/phiaui_design/mcp/batch_get.ex
defmodule PhiaUiDesign.Mcp.BatchGet do
@moduledoc """
Pattern-based search and batch retrieval of nodes from the scene graph.
Supports searching by type, name (regex), component name, and specific
node IDs. Results are returned with configurable depth for child expansion.
"""
alias PhiaUiDesign.Canvas.{Scene, Node}
@doc """
Execute a batch_get query against the scene.
## Query params
- `patterns` — list of pattern maps: `%{"type" => ..., "name" => ..., "component" => ...}`
- `node_ids` — list of specific node IDs to read
- `parent_id` — scope search to a subtree
- `read_depth` — how deep to expand match children (default 1; `0` = node only)
- `search_depth` — how deep to search for matches (default unlimited)
Returns `{:ok, results}` where results is a list of serialized node maps.
"""
@spec execute(reference(), map()) :: {:ok, [map()]}
def execute(scene, query) do
patterns = query["patterns"] || []
node_ids = query["node_ids"] || []
parent_id = query["parent_id"]
read_depth = query["read_depth"] || 1
_search_depth = query["search_depth"]
# Collect matched nodes from patterns
pattern_matches =
if patterns == [] do
[]
else
candidates = collect_candidates(scene, parent_id)
Enum.filter(candidates, fn node -> matches_any_pattern?(node, patterns) end)
end
# Collect nodes by ID
id_matches =
Enum.flat_map(node_ids, fn id ->
case Scene.get_node(scene, id) do
{:ok, node} -> [node]
_ -> []
end
end)
# Deduplicate by ID
all_matches =
(pattern_matches ++ id_matches)
|> Enum.uniq_by(& &1.id)
# Serialize with read_depth
results = Enum.map(all_matches, &serialize_node(scene, &1, read_depth))
{:ok, results}
end
# ---------------------------------------------------------------------------
# Candidate collection
# ---------------------------------------------------------------------------
defp collect_candidates(scene, nil) do
Scene.all_nodes(scene)
end
defp collect_candidates(scene, parent_id) do
collect_subtree(scene, parent_id)
end
defp collect_subtree(scene, node_id) do
case Scene.get_node(scene, node_id) do
{:ok, node} ->
children = Enum.flat_map(node.children, &collect_subtree(scene, &1))
[node | children]
_ ->
[]
end
end
# ---------------------------------------------------------------------------
# Pattern matching
# ---------------------------------------------------------------------------
defp matches_any_pattern?(node, patterns) do
Enum.any?(patterns, &matches_pattern?(node, &1))
end
defp matches_pattern?(node, pattern) do
type_match?(node, pattern) and
name_match?(node, pattern) and
component_match?(node, pattern)
end
defp type_match?(_node, %{"type" => nil}), do: true
defp type_match?(_node, pattern) when not is_map_key(pattern, "type"), do: true
defp type_match?(node, %{"type" => type}) do
to_string(node.type) == type
end
defp name_match?(_node, %{"name" => nil}), do: true
defp name_match?(_node, pattern) when not is_map_key(pattern, "name"), do: true
defp name_match?(node, %{"name" => name_pattern}) do
case Regex.compile(name_pattern) do
{:ok, regex} -> Regex.match?(regex, node.name || "")
_ -> node.name == name_pattern
end
end
defp component_match?(_node, %{"component" => nil}), do: true
defp component_match?(_node, pattern) when not is_map_key(pattern, "component"), do: true
defp component_match?(node, %{"component" => component}) do
to_string(node.component) == component
end
# ---------------------------------------------------------------------------
# Serialization
# ---------------------------------------------------------------------------
defp serialize_node(_scene, %Node{} = node, 0) do
base_map(node)
|> maybe_add_children_truncated(node)
end
defp serialize_node(scene, %Node{} = node, depth) do
base = base_map(node)
if node.children == [] do
base
else
children =
Enum.map(node.children, fn child_id ->
case Scene.get_node(scene, child_id) do
{:ok, child} -> serialize_node(scene, child, depth - 1)
_ -> %{"id" => child_id, "error" => "not_found"}
end
end)
Map.put(base, "children", children)
end
end
defp base_map(%Node{} = node) do
base = %{
"id" => node.id,
"type" => to_string(node.type),
"name" => node.name
}
case node.type do
:phia_component ->
base
|> Map.put("component", to_string(node.component))
|> Map.put("attrs", stringify_map(node.attrs))
|> Map.put("slots", stringify_map(node.slots))
:html_element ->
base
|> Map.put("tag", node.tag)
|> Map.put("classes", node.classes)
:text ->
Map.put(base, "text_content", node.text_content)
:frame ->
base
|> Map.put("classes", node.classes)
|> maybe_add_layout(node)
_ ->
base
end
end
defp maybe_add_children_truncated(map, %{children: []}), do: map
defp maybe_add_children_truncated(map, %{children: children}) do
Map.put(map, "children", Enum.map(children, fn id -> %{"id" => id, "_truncated" => true} end))
end
defp maybe_add_layout(map, node) do
layout_fields = [:layout, :gap, :padding, :justify_content, :align_items, :wrap, :width, :height]
Enum.reduce(layout_fields, map, fn field, acc ->
case Map.get(node, field) do
nil -> acc
false when field == :wrap -> acc
value -> Map.put(acc, to_string(field), stringify_value(value))
end
end)
end
defp stringify_map(map) when is_map(map) do
Map.new(map, fn {k, v} -> {to_string(k), stringify_value(v)} end)
end
defp stringify_map(other), do: other
defp stringify_value(v) when is_atom(v) and not is_nil(v) and not is_boolean(v),
do: ":#{v}"
defp stringify_value(v), do: v
end