defmodule CPSolver.ConstraintStore do @moduledoc """ Constraint store is a key-value store, where `key` is a variable id, and `value` is a implementation-dependent structure that allows to update and keep track of variables' domains. """ ################# alias CPSolver.Common alias CPSolver.Variable @type get_operation :: Common.domain_get_operation() | nil @type update_operation :: Common.domain_update_operation() @mandatory_notifications [:fixed, :fail] def default_store() do CPSolver.Store.ETS end ### Callbacks ## Tell basic constraints (a.k.a, domains) to a constraint store @callback create(variables :: Enum.t(), opts :: Keyword.t()) :: {:ok, any()} | {:error, any()} ## Get variable details @callback get(store :: any(), variable :: Variable.t(), get_operation(), [any()]) :: {:ok, any()} | {:error, any()} @callback update(store :: any(), variable :: Variable.t(), update_operation(), [any()]) :: any() @callback update_domain(store :: any(), variable :: Variable.t(), update_operation(), [any()]) :: any() @callback dispose(store :: any(), variables :: [Variable.t()]) :: :ok | :not_found @callback domain(store :: any(), variable :: Variable.t()) :: {:ok, any()} | {:error, any()} @callback on_fail(store :: any(), variable :: Variable.t()) :: any() @callback on_no_change(store :: any(), variable :: Variable.t()) :: any() @callback on_change( store :: any(), variable :: Variable.t(), change :: :fixed | :min_change | :max_change | :domain_change ) :: any() @callback get_variables(store :: any()) :: [any()] @callback subscribe( store :: any(), subscriptions :: [%{pid: pid(), variable: any(), events: [any()]}] ) :: :ok | :not_found ### API defmacro __using__(_) do quote do @behaviour CPSolver.ConstraintStore @domain_changes CPSolver.Common.domain_changes() require Logger def update(store, variable, operation, args) do update_domain(store, variable, operation, args) |> tap(fn :fail -> on_fail(store, variable) :no_change -> on_no_change(store, variable) change when change in @domain_changes -> on_change(store, variable, change) end) end defoverridable update: 4 end end def create_store(variables, store_impl \\ default_store()) do {:ok, store_instance} = store_impl.create(variables) {:ok, Enum.map(variables, fn var -> var |> Map.put(:id, var.id) |> Map.put(:name, var.name) |> Map.put(:store, store_instance) |> Map.put(:store_impl, store_impl) end), store_instance, store_impl} end def normalize_subscription(%{variable: variable, events: events} = subscription) do %{subscription | variable: variable_id(variable), events: normalize_events(events)} end def notify_subscribers(_var, :no_change, _subscriptions) do :ignore end def notify_subscribers(var_id, event, subscriptions) do Enum.each(subscriptions, fn s -> notify_subscriber(s, var_id, event) end) end defp notify_subscriber(%{pid: subscriber, events: events} = _subscription, var, event) do ## TODO: notify based on the list of events event in (@mandatory_notifications ++ events) && send(subscriber, {event, var}) end def variable_id(%Variable{id: id}) do id end def variable_id(id) do id end defp normalize_events(events) do ## :fixed and :fail are mandatory [:fixed, :fail | events] |> Enum.uniq() end end