# Axn - Unified Action DSL for Phoenix
A clean, step-based DSL library for defining actions that work seamlessly across Phoenix Controllers and LiveViews. Axn provides a unified interface for parameter validation, authorization, telemetry, and business logic where Plugs cannot be used.
**Why Axn?** Plugs only work with `Plug.Conn` but not `Phoenix.LiveView.Socket`. Axn bridges this gap, letting you write action logic once and use it in both contexts.
[](https://hex.pm/packages/axn)
[](https://hexdocs.pm/axn)
## Installation
```elixir
def deps do
[
{:axn, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
defmodule MyApp.UserActions do
use Axn
action :create_user do
step :validate_params
step :require_admin
step :create_user
def validate_params(ctx) do
# Simple validation - details in docs
{:cont, ctx}
end
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
end
def create_user(ctx) do
case Users.create(ctx.params) do
{:ok, user} -> {:halt, {:ok, user}}
{:error, reason} -> {:halt, {:error, reason}}
end
end
defp admin?(user), do: user && user.role == "admin"
end
end
# Use in Phoenix Controller
def create(conn, params) do
case MyApp.UserActions.run(:create_user, params, conn) do
{:ok, user} -> json(conn, %{user: user})
{:error, reason} -> json(conn, %{error: reason})
end
end
# Use in Phoenix LiveView
def handle_event("create_user", params, socket) do
case MyApp.UserActions.run(:create_user, params, socket) do
{:ok, user} -> {:noreply, assign(socket, :user, user)}
{:error, reason} -> {:noreply, put_flash(socket, :error, "Error: #{reason}")}
end
end
```
## Core Concepts
### Actions
Actions are named units of work that execute steps in order:
```elixir
action :action_name do
step :step_name
step :step_name, option: value
step {ExternalModule, :external_step}
end
```
### Steps
Steps take a context and either continue or halt the pipeline:
```elixir
def my_step(ctx) do
{:cont, updated_ctx} # Continue to next step
# OR
{:halt, {:ok, result}} # Stop with success
# OR
{:halt, {:error, reason}} # Stop with error
end
```
### Context
The `Axn.Context` struct flows through steps, carrying data:
```elixir
%Axn.Context{
action: :create_user,
assigns: %{current_user: user}, # Phoenix-style assigns
params: %{email: "...", name: "..."}, # Request parameters
private: %{}, # Internal state
result: nil # Final result
}
```
## Built-in Steps
### Parameter Validation
```elixir
step :cast_validate_params, schema: %{
email!: :string, # Required
name: :string, # Optional
age: [field: :integer, default: 18] # With default
}
# With custom validation
step :cast_validate_params,
schema: %{phone!: :string},
validate: &validate_phone/1
```
## Authorization
Create simple authorization steps:
```elixir
step :require_admin
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
end
```
## Telemetry
Axn automatically emits telemetry events:
- `[:axn, :action, :start]` - Action starts
- `[:axn, :action, :stop]` - Action completes
- `[:axn, :action, :exception]` - Action fails
### Custom Metadata
```elixir
defmodule MyApp.UserActions do
use Axn, metadata: &__MODULE__.telemetry_metadata/1
def telemetry_metadata(ctx) do
%{
user_id: ctx.assigns.current_user && ctx.assigns.current_user.id,
tenant: ctx.assigns.tenant && ctx.assigns.tenant.slug
}
end
end
```
## Unified Phoenix Integration
**The Problem:** Plugs work with Controllers but not LiveViews, creating code duplication.
**The Solution:** Axn works with both contexts seamlessly.
```elixir
# Same action works in both:
MyApp.UserActions.run(:create_user, params, conn) # Controller
MyApp.UserActions.run(:create_user, params, socket) # LiveView
```
The action automatically extracts assigns from either `conn` or `socket`, eliminating the need to duplicate authorization, validation, and business logic.
## Testing
```elixir
test "create_user succeeds with valid input" do
assigns = %{current_user: %User{role: "admin"}}
params = %{"email" => "test@example.com", "name" => "John"}
assert {:ok, user} = MyApp.UserActions.run(:create_user, params, assigns)
assert user.email == "test@example.com"
end
```
## Error Handling
Axn provides consistent error handling:
```elixir
# Parameter errors
{:error, %{reason: :invalid_params, changeset: changeset}}
# Authorization errors
{:error, :unauthorized}
# Custom errors
{:error, :custom_reason}
```
## External Steps
Use steps from other modules:
```elixir
action :complex_operation do
step :validate_params
step {MySteps, :enrich_context}, fields: [:preferences]
step :handle_operation
end
```
## Performance
- Minimal overhead when telemetry is disabled
- Efficient pipeline using `Enum.reduce_while/3`
- Steps are pure functions, easy to optimize
## Comparison
### vs. Phoenix Plugs
- **Plugs**: Work only with Controllers (`Plug.Conn`)
- **Axn**: Works with both Controllers and LiveViews
### vs. Phoenix Contexts
- **Contexts**: Business logic modules, manual integration
- **Axn**: Built-in Phoenix integration with parameter validation and telemetry
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Add tests for your changes
4. Ensure all tests pass (`mix test`)
5. Run static analysis (`mix credo`)
6. Commit your changes (`git commit -am 'Add some feature'`)
7. Push to the branch (`git push origin my-new-feature`)
8. Create a new Pull Request
## License
MIT