Packages

Elixir client for the Listmonk open-source email platform API

Current section

Files

Jump to
listmonk_client lib listmonk server.ex
Raw

lib/listmonk/server.ex

defmodule Listmonk.Server do
@moduledoc """
GenServer implementation for maintaining Listmonk client state.
This module manages the client configuration and provides a process-based
interface for interacting with the Listmonk API.
## Usage
# Start with an alias (named process)
{:ok, pid} = Listmonk.new(config, :my_listmonk)
{:ok, lists} = Listmonk.get_lists(:my_listmonk)
# Start without a name (use pid)
{:ok, pid} = Listmonk.new(config)
{:ok, lists} = Listmonk.get_lists(pid)
# Update configuration at runtime
:ok = Listmonk.set_config(:my_listmonk, new_config)
# Get current configuration
config = Listmonk.get_config(:my_listmonk)
"""
use GenServer
alias Listmonk.{Config, Error}
@type server :: pid() | atom()
## Client API
@doc """
Starts a new Listmonk client process.
## Options
- `:name` - Optional name to register the process under
- `:config` - Configuration struct (required)
## Examples
# Start with a PID reference
{:ok, pid} = Listmonk.Server.start_link(config: config)
# Start with a named reference
{:ok, pid} = Listmonk.Server.start_link(config: config, name: :my_client)
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
{name, opts} = Keyword.pop(opts, :name)
config = Keyword.fetch!(opts, :config) |> normalize_config()
case Config.validate(config) do
:ok ->
if name do
GenServer.start_link(__MODULE__, config, name: name)
else
GenServer.start_link(__MODULE__, config)
end
{:error, message} ->
{:error, Error.new(message)}
end
end
@doc """
Gets the current configuration from the server process.
## Examples
iex> config = Listmonk.Server.get_config(pid)
%Listmonk.Config{...}
iex> config = Listmonk.Server.get_config(:my_listmonk)
%Listmonk.Config{...}
"""
@spec get_config(server()) :: Config.t()
def get_config(server) do
GenServer.call(server, :get_config)
end
@doc """
Updates the configuration of the server process.
## Examples
iex> new_config = %Listmonk.Config{url: "https://new.example.com", ...}
iex> Listmonk.Server.set_config(pid, new_config)
:ok
"""
@spec set_config(server(), Config.t()) :: :ok | {:error, Error.t()}
def set_config(server, new_config) do
GenServer.call(server, {:set_config, new_config})
end
@doc """
Stops the server process.
## Examples
iex> Listmonk.Server.stop(:my_listmonk)
:ok
"""
@spec stop(server()) :: :ok
def stop(server) do
GenServer.stop(server)
end
@doc """
Makes an HTTP request through the server process.
This is used internally by API modules.
"""
@spec request(server(), atom(), String.t(), keyword()) ::
{:ok, map()} | {:error, Error.t()}
def request(server, method, path, opts \\ []) do
GenServer.call(server, {:request, method, path, opts}, :infinity)
end
## GenServer Callbacks
@impl true
def init(config) do
{:ok, config}
end
@impl true
def handle_call(:get_config, _from, config) do
{:reply, config, config}
end
@impl true
def handle_call({:set_config, new_config}, _from, _config) do
case Config.validate(new_config) do
:ok ->
{:reply, :ok, new_config}
{:error, message} ->
{:reply, {:error, Error.new(message)}, new_config}
end
end
@impl true
def handle_call({:request, method, path, opts}, _from, config) do
result = do_request(method, path, config, opts)
{:reply, result, config}
end
## Private Functions
@user_agent "Listmonk-Elixir-Client/0.3.0"
defp normalize_config(%Config{} = config), do: config
defp normalize_config(opts) when is_list(opts), do: Config.new(opts)
defp do_request(method, path, config, opts) do
url = build_url(config.url, path)
req_opts =
[
method: method,
url: url,
auth: {:basic, "#{config.username}:#{config.password}"},
headers: [{"user-agent", @user_agent}],
receive_timeout: 30_000,
retry: false
]
|> Keyword.merge(opts)
case Req.request(req_opts) do
{:ok, %Req.Response{status: status} = response} when status in 200..299 ->
parse_response(response)
{:ok, %Req.Response{} = response} ->
{:error, Error.from_response(response)}
{:error, exception} ->
{:error, Error.new("Request failed: #{Exception.message(exception)}")}
end
end
defp build_url(base_url, path) do
base_url = String.trim_trailing(base_url, "/")
path = if String.starts_with?(path, "/"), do: path, else: "/#{path}"
base_url <> path
end
defp parse_response(%Req.Response{body: body}) when is_map(body) do
{:ok, body}
end
defp parse_response(%Req.Response{body: body}) when is_binary(body) do
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:ok, %{"data" => body}}
end
end
defp parse_response(%Req.Response{body: body}) do
{:ok, %{"data" => body}}
end
end