Mooncore.Action (mooncore v0.2.4)

Copy Markdown

Action dispatcher framework.

use Mooncore.Action in your app's action module to get the standard dispatcher pattern with role checking, deep merge, and command fallback.

Usage

defmodule MyApp.Action do
  @actions %{
    "task.create" => %{handler: {MyApp.Action.Task, :create}, roles: ~w(user)},
    "task.list"   => %{handler: {MyApp.Action.Task, :list}, roles: ~w(user)},
    "echo"        => %{handler: {MyApp.Action.Echo, :echo}},
  }

  use Mooncore.Action
end

Important: @actions must be defined before use Mooncore.Action. The macro captures @actions at compile time — if it's defined after, actions_map/0 will return nil and nothing will dispatch.

Actions are defined as maps with the following keys:

  • :handler{Module, :function} tuple (required)
  • :roles — list of role strings. Omit or set [] for public (no auth needed).
  • :overrides — map deep-merged into the request before calling the handler, overriding any incoming params with the same keys. Use this to force server-controlled values (e.g. %{format: "pdf"}) regardless of what the caller sends.
  • :validate — a Mooncore.Validate schema (keyword list of field: [rules]) applied to params before the handler is called. If validation fails, an error is returned immediately without reaching the handler.

Overrides Example

Both routes call the same handler but force different config:

@actions %{
  "report.pdf"     => %{handler: {MyApp.Action.Report, :generate}, roles: ~w(admin), overrides: %{format: "pdf"}},
  "report.preview" => %{handler: {MyApp.Action.Report, :generate}, roles: ~w(user),  overrides: %{format: "html"}},
}

The handler reads req[:format] — callers cannot override it.

Validate Example

@actions %{
  "task.create" => %{
    handler:  {MyApp.Action.Task, :create},
    roles:    ~w(user),
    validate: [
      {"title",    [:required, :string, {:min_length, 2}]},
      {"priority", [:integer, {:in, [1, 2, 3]}]}
    ]
  }
}

On failure the caller receives %{error: "validation_failed", errors: %{"title" => ["is required"]}}. Use string keys to match HTTP/WebSocket params; atom keys for internal Elixir calls; list paths (["address", "city"]) for nested maps.

Request Map Structure

The request map passed to handlers looks like:

%{
  auth: %{"user" => "alice", "app" => "myapp", "roles" => ["user"], ...},
  params: %{                        # the FULL request body / message
    "action" => "task.create",     # action name lives here too
    "title" => "My Task",          # user data is at the top level
    "rayid" => "abc-123"           # (WebSocket only) correlation id
  }
}

req[:params] is the entire parsed request body (HTTP) or the full WebSocket message. User-supplied fields sit alongside "action".

Calling Actions

# Via the pipeline (runs before/after middlewares):
Mooncore.Action.execute("task.create", request_map)

# Direct (no middlewares):
MyApp.Action.run("task.create", request_map)

Summary

Functions

Check if user has any of the required roles.

Execute an action through the full pipeline (before hooks → dispatch → after hooks). Called by transport adapters (HTTP, WebSocket).

Format action responses for transport.

Functions

check_roles(user_roles, allowed_roles)

Check if user has any of the required roles.

execute(action, request)

Execute an action through the full pipeline (before hooks → dispatch → after hooks). Called by transport adapters (HTTP, WebSocket).

When params["mooncore_log"] is truthy, logs the entire lifecycle with timestamps.

format_response(response)

Format action responses for transport.