Phoenix/Plug adapter for ARSENAL operations. Automatically convert ARSENAL operations into REST API endpoints.
- Automatic REST API Generation: Convert Arsenal operations to HTTP endpoints
- Dynamic Operation Discovery: Runtime operation registration and routing
- Parameter Validation: Request parameter validation and transformation
- Comprehensive Error Handling: HTTP-appropriate error responses
- OpenAPI Integration: Automatic API documentation generation
- URL Safety: Proper handling of encoded parameters (PIDs, etc.)
- Phoenix Integration: Seamless integration with Phoenix applications
- JSON Serialization: Automatic response formatting
Add arsenal_plug to your list of dependencies in mix.exs:
def deps do
[
{:arsenal_plug, "~> 0.0.1"}
]
endAdd the Arsenal plug to your Phoenix router:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :arsenal do
plug :accepts, ["json"]
plug ArsenalPlug
end
scope "/api/v1", MyAppWeb do
pipe_through [:api, :arsenal]
# Arsenal documentation endpoints
get "/arsenal/docs", ArsenalPlug.ArsenalController, :docs
get "/arsenal/operations", ArsenalPlug.ArsenalController, :list_operations
# Catch-all for Arsenal operations
match :*, "/*path", ArsenalPlug.ArsenalController, :operation_handler
end
endCreate operations that implement the Arsenal operation behaviour:
defmodule MyApp.Operations.GetUserInfo do
@behaviour Arsenal.Operation
@impl true
def rest_config do
%{
method: :get,
path: "/api/v1/users/:id",
summary: "Get user information",
parameters: [
%{name: "id", location: "path", type: "string", required: true, description: "User ID"}
],
responses: %{
200 => %{description: "User information retrieved successfully"},
404 => %{description: "User not found"}
}
}
end
@impl true
def validate_params(params) do
case Map.get(params, "id") do
id when is_binary(id) and id != "" ->
{:ok, %{id: id}}
_ ->
{:error, "Invalid user ID"}
end
end
@impl true
def execute(%{id: id}) do
case MyApp.Users.get_user(id) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
end
end
@impl true
def format_response(user) do
%{
data: %{
id: user.id,
name: user.name,
email: user.email,
created_at: user.inserted_at
}
}
end
endRegister operations with the Arsenal registry (typically in your application startup):
defmodule MyApp.Application do
use Application
def start(_type, _args) do
# Start Arsenal registry
{:ok, _} = Arsenal.start_link()
# Register operations
Arsenal.Registry.register_operation(MyApp.Operations.GetUserInfo)
# ... rest of your application setup
end
enddefmodule MyApp.Operations.ListUsers do
@behaviour Arsenal.Operation
@impl true
def rest_config do
%{
method: :get,
path: "/api/v1/users",
summary: "List all users",
parameters: [
%{name: "limit", location: "query", type: "integer", required: false, description: "Number of users to return"},
%{name: "offset", location: "query", type: "integer", required: false, description: "Number of users to skip"}
],
responses: %{
200 => %{description: "Users retrieved successfully"}
}
}
end
@impl true
def validate_params(params) do
limit = params["limit"] |> parse_integer(10)
offset = params["offset"] |> parse_integer(0)
{:ok, %{limit: limit, offset: offset}}
end
@impl true
def execute(%{limit: limit, offset: offset}) do
users = MyApp.Users.list_users(limit: limit, offset: offset)
{:ok, users}
end
@impl true
def format_response(users) do
%{
data: Enum.map(users, &format_user/1),
meta: %{
count: length(users)
}
}
end
defp parse_integer(nil, default), do: default
defp parse_integer(str, default) when is_binary(str) do
case Integer.parse(str) do
{int, ""} -> int
_ -> default
end
end
defp parse_integer(int, _default) when is_integer(int), do: int
defp format_user(user) do
%{
id: user.id,
name: user.name,
email: user.email
}
end
enddefmodule MyApp.Operations.CreateUser do
@behaviour Arsenal.Operation
@impl true
def rest_config do
%{
method: :post,
path: "/api/v1/users",
summary: "Create a new user",
parameters: [
%{name: "name", location: "body", type: "string", required: true, description: "User name"},
%{name: "email", location: "body", type: "string", required: true, description: "User email"}
],
responses: %{
201 => %{description: "User created successfully"},
400 => %{description: "Invalid user data"},
422 => %{description: "Validation failed"}
}
}
end
@impl true
def validate_params(params) do
with {:ok, name} <- validate_name(params["name"]),
{:ok, email} <- validate_email(params["email"]) do
{:ok, %{name: name, email: email}}
else
{:error, reason} -> {:error, reason}
end
end
@impl true
def execute(%{name: name, email: email}) do
case MyApp.Users.create_user(%{name: name, email: email}) do
{:ok, user} -> {:ok, user}
{:error, changeset} -> {:error, {:validation_error, changeset}}
end
end
@impl true
def format_response(user) do
%{
data: %{
id: user.id,
name: user.name,
email: user.email,
created_at: user.inserted_at
}
}
end
defp validate_name(name) when is_binary(name) and byte_size(name) > 0, do: {:ok, name}
defp validate_name(_), do: {:error, "Name is required"}
defp validate_email(email) when is_binary(email) do
if String.contains?(email, "@") do
{:ok, email}
else
{:error, "Invalid email format"}
end
end
defp validate_email(_), do: {:error, "Email is required"}
endAccess OpenAPI documentation at:
GET /api/v1/arsenal/docs- Full OpenAPI 3.0 specificationGET /api/v1/arsenal/operations- List of available operations
defmodule MyApp.Operations.GetUserInfoTest do
use ExUnit.Case, async: true
use MyAppWeb.ConnCase
alias MyApp.Operations.GetUserInfo
describe "GetUserInfo operation" do
test "validates parameters correctly" do
assert {:ok, %{id: "123"}} = GetUserInfo.validate_params(%{"id" => "123"})
assert {:error, _} = GetUserInfo.validate_params(%{"id" => ""})
end
test "executes operation successfully", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/v1/users/#{user.id}")
assert json_response(conn, 200)["data"]["id"] == user.id
end
end
endMIT License - see LICENSE for details.