Current section

Files

Jump to
bb lib bb dsl uniqueness_transformer.ex
Raw

lib/bb/dsl/uniqueness_transformer.ex

# SPDX-FileCopyrightText: 2025 James Harton
#
# SPDX-License-Identifier: Apache-2.0
defmodule BB.Dsl.UniquenessTransformer do
@moduledoc """
Validates that all entity names are globally unique across the robot.
This includes links, joints, sensors, actuators, controllers, and bridges - all
entities that get registered in the process registry. Commands are not included
since they're not registered processes.
"""
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
alias Spark.Error.DslError
@doc false
@impl true
def after?(BB.Dsl.DefaultNameTransformer), do: true
def after?(_), do: false
@doc false
@impl true
def before?(BB.Dsl.RobotTransformer), do: true
def before?(_), do: false
@doc false
@impl true
def transform(dsl) do
names =
[[:sensors], [:controllers], [:topology], [:parameters]]
|> Enum.reduce(%{}, fn path, names ->
dsl
|> Transformer.get_entities(path)
|> retrieve_names(path, names)
end)
dupes =
names
|> Enum.reject(fn
{_name, []} -> true
{_name, [_]} -> true
{_name, _paths} -> false
end)
if Enum.empty?(dupes) do
{:ok, dsl}
else
dupes =
dupes
|> Enum.map(fn {name, paths} ->
paths =
paths
|> Enum.reverse()
|> Enum.map_join("\n", &" - `#{inspect(&1)}`")
" - `#{inspect(name)}`:\n#{paths}\n"
end)
{:error,
DslError.exception(
module: Transformer.get_persisted(dsl, :module),
message: """
Entities with duplicate names found at the following paths:
#{dupes}
"""
)}
end
end
# `Localize.Unit` and `Decimal` structs carry a `:name` field that is not a
# DSL entity identifier — skip them so they aren't treated as named entities.
defp retrieve_names(%Localize.Unit{}, _path, names), do: names
defp retrieve_names(%Decimal{}, _path, names), do: names
# Estimator input/output entities use `:name` as a callback-side key
# (matched in the user's `handle_input/2` payload map and `{:reply, [...]}`
# output list). They are not process-registered and frequently share names
# with sensors they reference, so they are excluded from the uniqueness
# check.
defp retrieve_names(%BB.Dsl.Estimator.Input{}, _path, names), do: names
defp retrieve_names(%BB.Dsl.Estimator.Output{}, _path, names), do: names
defp retrieve_names(entity, path, names) when is_map_key(entity, :name) do
names =
names
|> Map.update(entity.name, [path], &[path | &1])
path = [entity.name | path]
entity
|> Map.drop([:name, :__struct__])
|> Enum.reduce(names, fn {key, value}, names ->
retrieve_names(value, [key | path], names)
end)
end
defp retrieve_names(entity, path, names) when is_map(entity) do
entity
|> Map.delete(:__struct__)
|> Enum.reduce(names, fn {key, value}, names ->
retrieve_names(value, [key | path], names)
end)
end
defp retrieve_names(entities, path, names) when is_list(entities) do
entities
|> Enum.reduce(names, fn value, names ->
retrieve_names(value, path, names)
end)
end
defp retrieve_names(_value, _path, names), do: names
end