defmodule AshCommanded.Commanded.Transformers.GenerateEventModules do @moduledoc """ Generates event modules based on the events defined in the DSL. For each event defined in a resource, this transformer will generate a corresponding module with the event name as a struct with the specified fields. This transformer should run after command modules have been generated. ## Example Given a resource with the following event: ```elixir event :user_registered do fields [:id, :email, :name] end ``` This transformer will generate a module like: ```elixir defmodule MyApp.Events.UserRegistered do @moduledoc "Event representing when a user was registered" @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.GenerateCommandModules @doc """ Specifies that this transformer should run after the command module transformer. """ @impl true def after?(GenerateCommandModules), do: true def after?(_), do: false @doc """ Transforms the DSL state to generate event modules. ## Examples iex> transform(dsl_state) {:ok, updated_dsl_state} """ @impl true def transform(dsl_state) do resource_module = Transformer.get_persisted(dsl_state, :module) with events when is_list(events) and events != [] <- Transformer.get_entities(dsl_state, [:commanded, :events]) do resource_name = BaseTransformer.get_resource_name(resource_module) app_prefix = BaseTransformer.get_module_prefix(resource_module) final_state = Enum.reduce(events, dsl_state, fn event, acc_dsl_state -> event_module = build_event_module(event, app_prefix) BaseTransformer.create_module(event_module, build_event_module_ast(event, resource_name), __ENV__) # Store the generated module in DSL state for potential use by other transformers event_modules = Transformer.get_persisted(acc_dsl_state, :event_modules, []) updated_dsl_state = Transformer.persist(acc_dsl_state, :event_modules, [ {event.name, event_module} | event_modules ]) updated_dsl_state end) {:ok, final_state} else _ -> {:ok, dsl_state} end end @doc """ Builds the module name for an event. ## Examples iex> build_event_module(%Event{name: :user_registered}, MyApp) MyApp.Events.UserRegistered iex> build_event_module(%Event{name: :user_registered, event_name: :new_user}, MyApp) MyApp.Events.NewUser """ def build_event_module(event, app_prefix) do event_name = event.event_name || event.name event_name_str = BaseTransformer.camelize_atom(event_name) Module.concat([app_prefix, "Events", event_name_str]) end @doc """ Builds the AST (Abstract Syntax Tree) for an event module. ## Examples iex> build_event_module_ast(%Event{name: :user_registered, fields: [:id]}, "User") {:__block__, [], [{:@, [...], [{:moduledoc, [...], [...]}]}, ...]} """ def build_event_module_ast(event, resource_name) do fields = event.fields event_name = event.event_name || event.name # Create a more descriptive moduledoc event_description = humanize_event(event_name) moduledoc = "Event representing when a #{resource_name} #{event_description}" 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_event(name) do name |> to_string() |> String.replace("_", " ") end end