Packages

Protocol-based dependency-injection solution for Elixir

Current section

Files

Jump to
capsulable lib capsulable.ex
Raw

lib/capsulable.ex

defprotocol Capsulable do
@fallback_to_any true
@spec put(t(), any(), any()) :: t()
def put(capsulable, key, value)
@doc """
Capsulable.fetch(capsulable, key)
- Returns {:ok, value} if key-value has been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.put(:a, 1)
iex> |> Capsulable.fetch(:a)
{:ok, 1}
```
- Returns :error if key-value has NOT been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.fetch(:a)
:error
```
"""
@spec fetch(t(), any()) :: {:ok, any()} | :error
def fetch(capsulable, key)
@doc """
Capsulable.get(capsulable, key, default)
- Returns value if key-value has been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.put(:a, 1)
iex> |> Capsulable.get(:a, :default_value)
1
```
- Returns value generated from default_fn if key-value has NOT been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.put(:a, 1)
iex> |> Capsulable.get(:b, :default_value)
:default_value
```
"""
Kernel.def get(capsulable, key, default) do
case fetch(capsulable, key) do
{:ok, value} -> value
:error -> default
end
end
@doc """
Capsulable.get_lazy(capsulable, key, default_fn)
- Returns value if key-value has been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.put(:a, 1)
iex> |> Capsulable.get_lazy(:a, fn -> raise("default_fn should not be called") end)
1
```
- Returns value generated from default_fn if key-value has NOT been set:
```
iex> TestCapsulable.new()
iex> |> Capsulable.get_lazy(:a, fn -> 2 end)
2
```
"""
Kernel.def get_lazy(capsulable, key, default_fn) do
case fetch(capsulable, key) do
{:ok, value} -> value
:error -> default_fn.()
end
end
end
defimpl Capsulable, for: Any do
if Code.ensure_loaded?(Oban.Job) && Code.ensure_loaded?(Ecto.Changeset) do
def put(%Ecto.Changeset{data: %Oban.Job{}} = oban_job_changeset, key, value) do
old_meta = Ecto.Changeset.get_field(oban_job_changeset, :meta, %{})
new_capsulable =
old_meta
|> Map.get("__capsulable__", %{})
|> put_in_capsulable(key, serialize(value))
new_meta = Map.put(old_meta, "__capsulable__", new_capsulable)
Ecto.Changeset.put_change(oban_job_changeset, :meta, new_meta)
end
end
if Code.ensure_loaded?(Plug.Conn) do
def put(%Plug.Conn{} = conn, key, value) do
new_capsulable =
conn.private
|> Map.get(:__capsulable__, %{})
|> put_in_capsulable(key, value)
Plug.Conn.put_private(conn, :__capsulable__, new_capsulable)
end
end
def put(any, _key, _value) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: any,
description:
"Capsulable.put/3 by default only supports %Plug.Conn{} and %Ecto.Changeset{data: %Oban.Job{}}"
end
if Code.ensure_loaded?(Plug.Conn) do
def fetch(%Plug.Conn{} = conn, key) do
conn.private
|> Map.get(:__capsulable__, %{})
|> fetch_from_capsulable(key)
end
end
if Code.ensure_loaded?(Oban.Job) do
def fetch(%Oban.Job{} = oban_job, key) do
with {:ok, value} <-
oban_job.meta
|> Map.get("__capsulable__", %{})
|> fetch_from_capsulable(key) do
{:ok, deserialize(value)}
end
end
end
def fetch(any, _key) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: any,
description: "Capsulable.fetch/2 by default only supports %Plug.Conn{} and %Oban.Job{}"
end
defp put_in_capsulable(capsulable, key, value) do
Map.merge(capsulable, %{key => value})
end
defp fetch_from_capsulable(capsulable, key) do
case capsulable do
%{^key => value} -> {:ok, value}
_ -> :error
end
end
defp serialize(term) do
term
|> :erlang.term_to_binary()
|> Base.encode64()
end
defp deserialize(binary) do
binary
|> Base.decode64!()
|> :erlang.binary_to_term()
end
end