defmodule AshCommanded.Commanded.Transformers.GenerateDomainRouterModule do @moduledoc """ Generates a domain-specific router module based on the commands defined in a domain's resources. For each Ash.Domain containing resources with commands, this transformer will generate a domain router module that: 1. Defines dispatch functions for all commands in that domain 2. Routes commands to the appropriate aggregate This transformer should run after the command and aggregate module transformers. ## Example Given a domain with resources that have commands, this transformer will generate: ```elixir defmodule MyApp.Accounts.Router do @moduledoc "Router for commands in the Accounts domain" use Commanded.Commands.Router # Register User commands dispatch [MyApp.Accounts.Commands.RegisterUser, MyApp.Accounts.Commands.UpdateEmail], to: MyApp.Accounts.UserAggregate, identity: :id # Register Account commands dispatch [MyApp.Accounts.Commands.CreateAccount, MyApp.Accounts.Commands.CloseAccount], to: MyApp.Accounts.AccountAggregate, identity: :id end ``` """ use Spark.Dsl.Transformer alias Spark.Dsl.Transformer alias AshCommanded.Commanded.Transformers.GenerateCommandModules alias AshCommanded.Commanded.Transformers.GenerateAggregateModule @doc """ Specifies that this transformer should run after the command and aggregate module transformers. """ @impl true def after?(GenerateCommandModules), do: true def after?(GenerateAggregateModule), do: true def after?(_), do: false @doc """ Transforms the DSL state to generate a domain router module. ## Examples iex> transform(dsl_state) {:ok, updated_dsl_state} """ @impl true def transform(dsl_state) do resource_module = Transformer.get_persisted(dsl_state, :module) # Only process if this is an Ash.Resource case is_ash_resource?(resource_module) do false -> {:ok, dsl_state} true -> commands = Transformer.get_entities(dsl_state, [:commanded, :commands]) # Only proceed if there are commands defined case commands do [] -> {:ok, dsl_state} _commands -> # Get the domain module for this resource domain_module = get_domain_module(resource_module) # Skip if no domain is associated with this resource if domain_module == nil do {:ok, dsl_state} else # Get the domain router module name domain_router_module = build_domain_router_module(domain_module) # Store the command-resource-aggregate mapping in the DSL state # This will be used by the main router transformer to build the full router command_mappings = Transformer.get_persisted(dsl_state, :command_resource_mappings, []) aggregate_module = Transformer.get_persisted(dsl_state, :aggregate_module) # Get identity field for commands (default to :id if not specified) identity_field = case List.first(commands) do nil -> :id command -> command.identity_field || :id end # Update the command mappings with the new resource updated_mappings = command_mappings ++ [ %{ resource: resource_module, domain: domain_module, domain_router: domain_router_module, aggregate: aggregate_module, identity_field: identity_field, commands: commands } ] # Store the updated mappings updated_dsl_state = Transformer.persist(dsl_state, :command_resource_mappings, updated_mappings) # Also store a reference to the domain router updated_dsl_state = Transformer.persist(updated_dsl_state, :domain_router_module, domain_router_module) {:ok, updated_dsl_state} end end end end # Check if module is an Ash.Resource defp is_ash_resource?(module) do Code.ensure_loaded?(module) && function_exported?(module, :__ash_resource__, 0) end # Get the domain module for a resource defp get_domain_module(resource_module) do if function_exported?(resource_module, :__ash_domain__, 0) do resource_module.__ash_domain__() else nil end end @doc """ Builds the module name for a domain router. ## Examples iex> build_domain_router_module(MyApp.Accounts) MyApp.Accounts.Router """ def build_domain_router_module(domain_module) do Module.concat([domain_module, "Router"]) end end