lib/roger/key_set.ex

defmodule Roger.KeySet do
  @moduledoc """

  An opaque interface to storing keys and testing set member ship of
  keys. Like a bloom filter, but 100% probabilistic.

      iex> {:ok, pid} = Roger.KeySet.start_link
      iex> Roger.KeySet.add(pid, "bla")
      :ok
      iex> Roger.KeySet.contains?(pid, "bla")
      true
      iex> Roger.KeySet.contains?(pid, "beh")
      false

  Keys can also be removed:

      iex> {:ok, pid} = Roger.KeySet.start_link
      iex> Roger.KeySet.add(pid, "bla")
      iex> Roger.KeySet.remove(pid, "bla")
      :ok
      iex> Roger.KeySet.contains?(pid, "bla")
      false

  The state of the keyset can be retrieved in binary format. This
  state is to be treated as an opaque datastructure. We can then load
  the state into a new keyset process.

  The state can also be given as an argument when the keyset process
  is started.

      iex> {:ok, pid} = Roger.KeySet.start_link
      iex> Roger.KeySet.add(pid, "existing")
      iex> {:ok, state} = Roger.KeySet.get_state(pid)
      iex> {:ok, pid2} = Roger.KeySet.start_link(state: state)
      iex> Roger.KeySet.contains?(pid2, "existing")
      true

  Many keys can also be added at once:

      iex> {:ok, pid} = Roger.KeySet.start_link
      iex> Roger.KeySet.add_many(pid, ~w(a b c))
      :ok
      iex> Roger.KeySet.contains?(pid, "a")
      true
      iex> Roger.KeySet.contains?(pid, "b")
      true
      iex> Roger.KeySet.contains?(pid, "c")
      true

  Two keysets can also be used in set operations. These will always be
  applied to the first keyset; the second is left untouched:

      iex> {:ok, a} = Roger.KeySet.start_link
      iex> Roger.KeySet.add_many(a, ~w(a1 a2 a3))
      iex> {:ok, b} = Roger.KeySet.start_link
      iex> Roger.KeySet.add_many(b, ~w(b1 b2))
      iex> Roger.KeySet.union(a, b)
      :ok
      iex> Roger.KeySet.contains?(a, "b1")
      true
      iex> Roger.KeySet.contains?(b, "a1")
      false
      iex> Roger.KeySet.difference(a, b)
      :ok
      iex> Roger.KeySet.contains?(a, "b1")
      false

  *Note: the current implementation is a MapSet but this is an
  implementation detail and likely to change.*

  """

  use GenServer

  def start_link(opts \\ []) do
    GenServer.start(__MODULE__, [opts])
  end

  def add(pid, key) do
    GenServer.call(pid, {:add, key})
  end

  def add_many(pid, keys) when is_list(keys) do
    GenServer.call(pid, {:add_many, keys})
  end

  def remove(pid, key) do
    GenServer.call(pid, {:remove, key})
  end

  def contains?(pid, key) do
    GenServer.call(pid, {:contains, key})
  end

  def get_state(pid) do
    GenServer.call(pid, :get_state)
  end

  def put_state(pid, state) do
    GenServer.call(pid, {:put_state, state})
  end

  def union(pid, pid2) do
    GenServer.call(pid, {:set_operation, :union, pid2})
  end

  def difference(pid, pid2) do
    GenServer.call(pid, {:set_operation, :difference, pid2})
  end

  # server side

  def init([opts]) do
    Process.flag(:trap_exit, true)
    {:ok, load_state(opts[:state])}
  end

  def handle_call({:add, key}, _from, state) do
    {:reply, :ok, MapSet.put(state, key)}
  end

  def handle_call({:add_many, keys}, _from, state) do
    state = Enum.reduce(keys, state, &MapSet.put(&2, &1))
    {:reply, :ok, state}
  end

  def handle_call({:contains, key}, _from, state) do
    {:reply, MapSet.member?(state, key), state}
  end

  def handle_call({:remove, key}, _from, state) do
    {:reply, :ok, MapSet.delete(state, key)}
  end

  def handle_call(:get_state, _from, state) do
    {:reply, {:ok, save_state(state)}, state}
  end

  def handle_call({:set_operation, operation, pid2}, _from, state) do
    {:reply, :ok, set_operation(pid2, operation, state)}
  end

  defp load_state(state) when is_binary(state) do
    state
    |> :erlang.binary_to_term()
  end

  defp load_state(nil), do: MapSet.new()

  defp save_state(state) do
    :erlang.term_to_binary(state)
  end

  defp set_operation(source, operation, state) do
    {:ok, state2} = get_state(source)
    Kernel.apply(MapSet, operation, [state, load_state(state2)])
  end
end