defmodule AshCommanded.Commanded.Transformers.GenerateCommandModules do @moduledoc """ Generates command modules based on the commands defined in the DSL. For each command defined in a resource, this transformer will generate a corresponding module with the command name as a struct with the specified fields. This transformer should run before any other code generation transformers. ## Example Given a resource with the following command: ```elixir command :register_user do fields [:id, :email, :name] identity_field :id end ``` This transformer will generate a module like: ```elixir defmodule MyApp.Commands.RegisterUser do @moduledoc "Command for registering a user" @type t :: %__MODULE__{ id: String.t(), email: String.t(), name: String.t() } defstruct [:id, :email, :name] end ``` """ use Spark.Dsl.Transformer alias Spark.Dsl.Transformer alias AshCommanded.Commanded.Transformers.BaseTransformer alias AshCommanded.Commanded.Transformers.GenerateEventModules @doc """ Specifies that this transformer should run before the event module transformer. """ @impl true def before?(GenerateEventModules), do: true def before?(_), do: false @doc """ Transforms the DSL state to generate command modules. ## Examples iex> transform(dsl_state) updated_dsl_state """ @impl true def transform(dsl_state) do resource_module = Transformer.get_persisted(dsl_state, :module) with commands when is_list(commands) and commands != [] <- Transformer.get_entities(dsl_state, [:commanded, :commands]) do resource_name = BaseTransformer.get_resource_name(resource_module) app_prefix = BaseTransformer.get_module_prefix(resource_module) final_state = Enum.reduce(commands, dsl_state, fn command, acc_dsl_state -> command_module = build_command_module(command, app_prefix) BaseTransformer.create_module(command_module, build_command_module_ast(command, resource_name), __ENV__) # Store the generated module in DSL state for potential use by other transformers command_modules = Transformer.get_persisted(acc_dsl_state, :command_modules, []) updated_dsl_state = Transformer.persist(acc_dsl_state, :command_modules, [ {command.name, command_module} | command_modules ]) updated_dsl_state end) {:ok, final_state} else _ -> {:ok, dsl_state} end end defp build_command_module(command, app_prefix) do command_name = command.command_name || command.name command_name_str = BaseTransformer.camelize_atom(command_name) Module.concat([app_prefix, "Commands", command_name_str]) end defp build_command_module_ast(command, resource_name) do fields = command.fields command_name = command.command_name || command.name # Create a more descriptive moduledoc verb = humanize_verb(command_name) moduledoc = "Command for #{verb} a #{resource_name}" quote do @moduledoc unquote(moduledoc) @type t :: %__MODULE__{unquote_splicing(build_type_fields(fields))} defstruct unquote(fields) end end defp build_type_fields(fields) do Enum.map(fields, fn field -> {field, quote do: any()} end) end defp humanize_verb(name) do name |> to_string() |> String.replace("_", " ") end end