Packages

A clean, step-based DSL library for defining actions with parameter validation, authorization, telemetry, and custom business logic. Prioritizes simplicity, explicitness, and ease of following execution flow.

Current section

Files

Jump to
axn README.md
Raw

README.md

# 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.
[![Hex.pm](https://img.shields.io/hexpm/v/axn.svg)](https://hex.pm/packages/axn)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](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