Skip to content

nshkrdotcom/arsenal_plug

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ArsenalPlug

Phoenix/Plug adapter for ARSENAL operations. Automatically convert ARSENAL operations into REST API endpoints.

Features

  • 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

Installation

Add arsenal_plug to your list of dependencies in mix.exs:

def deps do
  [
    {:arsenal_plug, "~> 0.0.1"}
  ]
end

Quick Start

1. Add to Router

Add 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
end

2. Create Arsenal Operations

Create 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
end

3. Register Operations

Register 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
end

Usage Examples

Basic GET Operation

defmodule 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
end

POST Operation with Body Parameters

defmodule 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"}
end

API Documentation

Auto-Generated Documentation

Access OpenAPI documentation at:

  • GET /api/v1/arsenal/docs - Full OpenAPI 3.0 specification
  • GET /api/v1/arsenal/operations - List of available operations

Testing

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
end

License

MIT License - see LICENSE for details.

Releases

No releases published

Packages

No packages published

Languages