Your Second Action
View SourcePrerequisites: Getting Started • Basic action creation
This tutorial builds a more sophisticated action with schema validation, error handling, and comprehensive testing.
The Task: User Registration
We'll create an action that validates user registration data, demonstrating:
- Complex schema validation
- Structured error handling
- Lifecycle hooks
- Comprehensive testing
Step 1: Define the Schema
defmodule MyApp.Actions.RegisterUser do
use Jido.Action,
name: "register_user",
description: "Validates and registers a new user",
schema: [
email: [
type: :string,
required: true,
doc: "User's email address"
],
password: [
type: :string,
required: true,
doc: "User's password (min 8 chars)"
],
age: [
type: :integer,
required: true,
doc: "User's age (minimum 13)"
],
terms_accepted: [
type: :boolean,
default: false,
doc: "Terms of service acceptance"
]
]Step 2: Add Validation Logic
def run(params, context) do
with {:ok, validated} <- validate_business_rules(params),
{:ok, user} <- create_user(validated, context) do
{:ok, %{
user_id: user.id,
email: user.email,
registered_at: DateTime.utc_now()
}}
end
end
defp validate_business_rules(params) do
cond do
not String.contains?(params.email, "@") ->
{:error, Jido.Action.Error.execution_error("Invalid email format")}
String.length(params.password) < 8 ->
{:error, Jido.Action.Error.execution_error("Password too short")}
params.age < 13 ->
{:error, Jido.Action.Error.execution_error("Must be at least 13 years old")}
not params.terms_accepted ->
{:error, Jido.Action.Error.execution_error("Terms must be accepted")}
true ->
{:ok, params}
end
end
defp create_user(params, _context) do
# Simulate user creation
user_id = "user_#{:rand.uniform(10000)}"
{:ok, %{id: user_id, email: params.email}}
endStep 3: Add Lifecycle Hooks
@impl true
def on_before_validate_params(params) do
# Normalize email before validation
normalized = Map.update(params, :email, "", &String.downcase/1)
{:ok, normalized}
end
@impl true
def on_after_run({:ok, result}) do
# Log successful registration
IO.puts("User registered: #{result.user_id}")
{:ok, result}
end
def on_after_run({:error, _} = error), do: error
@impl true
def on_error(failed_params, error, _context, _opts) do
# Log registration failure (no compensation needed)
IO.puts("Registration failed for #{failed_params[:email]}: #{error.message}")
{:ok, %{error_logged: true}}
end
endStep 4: Test Your Action
Create test/actions/register_user_test.exs:
defmodule MyApp.Actions.RegisterUserTest do
use ExUnit.Case
alias MyApp.Actions.RegisterUser
alias Jido.Exec
describe "register_user via Exec.run/4" do
test "succeeds with valid input" do
params = %{
email: "user@example.com",
password: "securepass123",
age: 25,
terms_accepted: true
}
assert {:ok, result} = Exec.run(RegisterUser, params)
assert String.starts_with?(result.user_id, "user_")
assert result.email == "user@example.com"
assert result.registered_at
end
test "normalizes email case via lifecycle hook" do
params = %{
email: "USER@EXAMPLE.COM",
password: "securepass123",
age: 25,
terms_accepted: true
}
assert {:ok, result} = Exec.run(RegisterUser, params)
assert result.email == "user@example.com"
end
test "rejects invalid email" do
params = %{
email: "invalid-email",
password: "securepass123",
age: 25,
terms_accepted: true
}
assert {:error, error} = Exec.run(RegisterUser, params)
assert error.message =~ "Invalid email format"
end
test "rejects short password" do
params = %{
email: "user@example.com",
password: "short",
age: 25,
terms_accepted: true
}
assert {:error, error} = Exec.run(RegisterUser, params)
assert error.message =~ "Password too short"
end
test "rejects underage users" do
params = %{
email: "user@example.com",
password: "securepass123",
age: 12,
terms_accepted: true
}
assert {:error, error} = Exec.run(RegisterUser, params)
assert error.message =~ "Must be at least 13 years old"
end
test "requires terms acceptance" do
params = %{
email: "user@example.com",
password: "securepass123",
age: 25,
terms_accepted: false
}
assert {:error, error} = Exec.run(RegisterUser, params)
assert error.message =~ "Terms must be accepted"
end
test "requires all mandatory fields" do
params = %{email: "user@example.com"}
assert {:error, _error} = Exec.run(RegisterUser, params)
end
end
endStep 5: Run Tests
mix test test/actions/register_user_test.exs
Expected output:
Compiling 1 file (.ex)
.......
Finished in 0.05 seconds (0.00s async, 0.05s sync)
7 tests, 0 failuresStep 6: Use with Execution Engine
# With retries and timeout
{:ok, result} = Jido.Exec.run(
MyApp.Actions.RegisterUser,
%{
email: "user@example.com",
password: "securepass123",
age: 25,
terms_accepted: true
},
%{request_id: "req_123"},
timeout: 5000,
max_retries: 2
)
# Async execution
async_ref = Jido.Exec.run_async(
MyApp.Actions.RegisterUser,
params,
%{}
)
{:ok, result} = Jido.Exec.await(async_ref, 10_000)What You've Learned
- Schema Definition - Complex validation with types, constraints, and docs
- Error Handling - Structured errors with helpful messages
- Lifecycle Hooks - Data normalization and logging
- Testing Patterns - Comprehensive test coverage using
Jido.Exec.run/4 - Production Usage - Execution engine features
Next Steps
-> Actions - Deep dive into framework architecture
-> Error Handling Guide - Advanced error patterns
-> Testing Guide - More testing strategies