Current section
Files
Jump to
Current section
Files
lib/alpa/resource.ex
defmodule Alpa.Resource do
@moduledoc """
A macro that generates standard CRUD functions for Alpaca API resources.
This reduces boilerplate for simple endpoints while preserving the typed
return pattern used throughout the SDK. All generated functions return
`{:ok, Model.t()}` or `{:error, Alpa.Error.t()}` tuples.
## Usage
defmodule Alpa.Trading.Assets do
use Alpa.Resource,
base_path: "/v2/assets",
model: Alpa.Models.Asset,
operations: [:list, :get]
end
## Options
* `:base_path` - The API path prefix (e.g., `"/v2/assets"`). Required.
* `:model` - The model module that implements `from_map/1`. Required.
* `:operations` - List of operations to generate. Required.
Supported: `:list`, `:get`, `:create`, `:update`, `:delete`.
* `:id_field` - Name for the ID parameter in generated docs and
function signatures. Default: `:id`.
* `:client` - Which API base URL to use: `:trading` or `:data`.
Default: `:trading`.
## Generated functions
Depending on the operations specified:
* `list/0`, `list/1` - List resources. Returns `{:ok, [Model.t()]} | {:error, Alpa.Error.t()}`.
* `get/1`, `get/2` - Get a single resource by ID. Returns `{:ok, Model.t()} | {:error, Alpa.Error.t()}`.
* `create/1` - Create a resource. Returns `{:ok, Model.t()} | {:error, Alpa.Error.t()}`.
* `update/2` - Update a resource by ID. Returns `{:ok, Model.t()} | {:error, Alpa.Error.t()}`.
* `delete/1`, `delete/2` - Delete a resource by ID. Returns `{:ok, :deleted} | {:error, Alpa.Error.t()}`.
This macro is **optional** and intended for new endpoints or simple CRUD
resources. Existing modules do not need to be changed.
"""
defmacro __using__(opts) do
base_path = Keyword.fetch!(opts, :base_path)
model = Keyword.fetch!(opts, :model)
operations = Keyword.fetch!(opts, :operations)
id_field = Keyword.get(opts, :id_field, :id)
client = Keyword.get(opts, :client, :trading)
# Validate operations at compile time
valid_ops = [:list, :get, :create, :update, :delete]
invalid = operations -- valid_ops
if invalid != [] do
raise ArgumentError,
"Invalid operations: #{inspect(invalid)}. Must be one of #{inspect(valid_ops)}"
end
get_fn =
case client do
:data -> :get_data
:trading -> :get
end
quote do
alias Alpa.Client
alias Alpa.Error
unquote(if :list in operations, do: gen_list(base_path, model, get_fn))
unquote(if :get in operations, do: gen_get(base_path, model, id_field, get_fn))
unquote(if :create in operations, do: gen_create(base_path, model))
unquote(if :update in operations, do: gen_update(base_path, model, id_field))
unquote(if :delete in operations, do: gen_delete(base_path, id_field))
end
end
defp gen_list(base_path, model, get_fn) do
quote do
@doc """
List all resources.
Accepts keyword options for query parameters and client configuration.
"""
@spec list(keyword()) :: {:ok, [unquote(model).t()]} | {:error, Error.t()}
def list(opts \\ []) do
{params, rest} = Keyword.pop(opts, :params, %{})
call_opts =
if params == %{}, do: rest, else: Keyword.put(rest, :params, params)
case Client.unquote(get_fn)(unquote(base_path), call_opts) do
{:ok, data} when is_list(data) ->
{:ok, Enum.map(data, &unquote(model).from_map/1)}
{:ok, unexpected} ->
{:error, Error.invalid_response(unexpected)}
{:error, _} = error ->
error
end
end
end
end
defp gen_get(base_path, model, id_field, get_fn) do
quote do
@doc """
Get a single resource by #{unquote(id_field)}.
"""
@spec get(String.t(), keyword()) :: {:ok, unquote(model).t()} | {:error, Error.t()}
def get(unquote(Macro.var(id_field, nil)), opts \\ []) do
path =
unquote(base_path) <>
"/" <> URI.encode_www_form(to_string(unquote(Macro.var(id_field, nil))))
case Client.unquote(get_fn)(path, opts) do
{:ok, data} -> {:ok, unquote(model).from_map(data)}
{:error, _} = error -> error
end
end
end
end
defp gen_create(base_path, model) do
quote do
@doc """
Create a new resource.
Accepts a keyword list or map of attributes.
"""
@spec create(keyword() | map()) :: {:ok, unquote(model).t()} | {:error, Error.t()}
def create(params) when is_list(params) do
create(Map.new(params))
end
def create(params) when is_map(params) do
case Client.post(unquote(base_path), params) do
{:ok, data} -> {:ok, unquote(model).from_map(data)}
{:error, _} = error -> error
end
end
end
end
defp gen_update(base_path, model, id_field) do
quote do
@doc """
Update a resource by #{unquote(id_field)}.
Accepts a keyword list or map of attributes to update.
"""
@spec update(String.t(), keyword() | map()) ::
{:ok, unquote(model).t()} | {:error, Error.t()}
def update(unquote(Macro.var(id_field, nil)), params) when is_list(params) do
update(unquote(Macro.var(id_field, nil)), Map.new(params))
end
def update(unquote(Macro.var(id_field, nil)), params) when is_map(params) do
path =
unquote(base_path) <>
"/" <> URI.encode_www_form(to_string(unquote(Macro.var(id_field, nil))))
case Client.put(path, params) do
{:ok, data} -> {:ok, unquote(model).from_map(data)}
{:error, _} = error -> error
end
end
end
end
defp gen_delete(base_path, id_field) do
quote do
@doc """
Delete a resource by #{unquote(id_field)}.
"""
@spec delete(String.t(), keyword()) :: {:ok, :deleted} | {:error, Error.t()}
def delete(unquote(Macro.var(id_field, nil)), opts \\ []) do
path =
unquote(base_path) <>
"/" <> URI.encode_www_form(to_string(unquote(Macro.var(id_field, nil))))
Client.delete(path, opts)
end
end
end
end