Packages

Elixir SDK for the SW4RM multi-agent coordination protocol

Current section

Files

Jump to
sw4rm_sdk lib sw4rm persistence.ex
Raw

lib/sw4rm/persistence.ex

defmodule Sw4rm.Persistence do
@moduledoc """
Persistence behaviour with pluggable backends.
"""
@callback save_records(backend :: term(), records :: list(), opts :: keyword()) ::
{:ok, non_neg_integer()} | {:error, term()}
@callback load_records(backend :: term(), opts :: keyword()) :: {:ok, list()} | {:error, term()}
@callback clear_records(backend :: term(), opts :: keyword()) ::
{:ok, non_neg_integer()} | {:error, term()}
end
defmodule Sw4rm.Persistence.InMemory do
@moduledoc "In-memory persistence backend (for testing). Uses Agent for state."
@behaviour Sw4rm.Persistence
use Agent
def start_link(opts \\ []) do
name = Keyword.get(opts, :name)
Agent.start_link(fn -> %{} end, name: name)
end
@impl true
def save_records(agent, records, opts \\ []) do
ns = Keyword.get(opts, :namespace, "default")
Agent.update(agent, &Map.put(&1, ns, records))
{:ok, length(records)}
end
@impl true
def load_records(agent, opts \\ []) do
ns = Keyword.get(opts, :namespace, "default")
{:ok, Agent.get(agent, &Map.get(&1, ns, []))}
end
@impl true
def clear_records(agent, opts \\ []) do
ns = Keyword.get(opts, :namespace, "default")
count =
Agent.get_and_update(agent, fn storage ->
records = Map.get(storage, ns, [])
{length(records), Map.delete(storage, ns)}
end)
{:ok, count}
end
def list_namespaces(agent) do
Agent.get(agent, &Map.keys/1)
end
end
defmodule Sw4rm.Persistence.JsonFile do
@moduledoc "JSON file-based persistence backend. GenServer for thread safety."
@behaviour Sw4rm.Persistence
use GenServer
def start_link(opts \\ []) do
name = Keyword.get(opts, :name)
base_dir = Keyword.get(opts, :base_directory, Path.expand("~/.sw4rm/"))
GenServer.start_link(__MODULE__, base_dir, name: name)
end
@impl Sw4rm.Persistence
def save_records(server, records, opts \\ []) do
GenServer.call(server, {:save, records, opts})
end
@impl Sw4rm.Persistence
def load_records(server, opts \\ []) do
GenServer.call(server, {:load, opts})
end
@impl Sw4rm.Persistence
def clear_records(server, opts \\ []) do
GenServer.call(server, {:clear, opts})
end
def list_namespaces(server), do: GenServer.call(server, :list_namespaces)
# -- Callbacks --
@impl true
def init(base_dir) do
File.mkdir_p!(base_dir)
{:ok, %{base_dir: base_dir}}
end
@impl true
def handle_call({:save, records, opts}, _from, s) do
path = ns_path(s.base_dir, opts)
File.mkdir_p!(Path.dirname(path))
case Jason.encode(records, pretty: true) do
{:ok, json} ->
File.write!(path, json)
{:reply, {:ok, length(records)}, s}
{:error, reason} ->
{:reply, {:error, reason}, s}
end
end
def handle_call({:load, opts}, _from, s) do
path = ns_path(s.base_dir, opts)
result =
case File.read(path) do
{:ok, content} -> Jason.decode(content)
{:error, :enoent} -> {:ok, []}
{:error, reason} -> {:error, reason}
end
{:reply, result, s}
end
def handle_call({:clear, opts}, _from, s) do
path = ns_path(s.base_dir, opts)
count =
case File.read(path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, records} -> length(records)
_ -> 0
end
_ ->
0
end
File.rm(path)
{:reply, {:ok, count}, s}
end
def handle_call(:list_namespaces, _from, s) do
namespaces =
case File.ls(s.base_dir) do
{:ok, files} ->
files
|> Enum.filter(&String.ends_with?(&1, ".json"))
|> Enum.map(&String.trim_trailing(&1, ".json"))
_ ->
[]
end
{:reply, namespaces, s}
end
defp ns_path(base_dir, opts) do
ns = Keyword.get(opts, :namespace, "default")
Path.join(base_dir, "#{ns}.json")
end
end