defmodule Estructura.LazyMap do
@moduledoc """
The implementation of lazy map implementing lazy `Access` for its keys.
`Estructura.LazyMap` is backed by the “raw” object and a key-value pairs
where values might be instances of `Estructura.Lazy`. If this is a case,
they will be accessed through `Lazy` implementation.
Values might be also raw values, which makes `LazyMap` a drop-in replacement
of standard _Elixir_ maps, assuming they are accessed through `Access`
only (e. g. `map[:key]` and not `map.key`.)
"""
@type t :: %{
__struct__: __MODULE__,
__lazy_data__: term(),
data: map()
}
defstruct data: %{}, __lazy_data__: nil
alias Estructura.Lazy
@behaviour Access
@impl Access
def fetch(lazy, key)
def fetch(%__MODULE__{data: %{} = data} = this, key) when is_map_key(data, key) do
case Map.get(data, key) do
%Lazy{} = value ->
case Lazy.apply(value, this, key) do
%Lazy{value: {:ok, value}} -> {:ok, value}
_ -> :error
end
value ->
{:ok, value}
end
end
def fetch(%__MODULE__{}, _), do: :error
@impl Access
def pop(lazy, key)
def pop(%__MODULE__{data: %{} = data} = this, key) when is_map_key(data, key) do
case Map.get(data, key) do
%Lazy{} = value ->
case Lazy.apply(value, this, key) do
%Lazy{value: {:ok, value}} ->
{value, %__MODULE__{this | data: Map.delete(data, key)}}
_ ->
{nil, this}
end
value ->
{value, %__MODULE__{this | data: Map.delete(data, key)}}
end
end
def pop(%__MODULE__{data: %{}} = this, _), do: {nil, this}
@impl Access
def get_and_update(lazy, key, fun \\ &{&1, &1})
def get_and_update(%__MODULE__{data: %{} = data} = this, key, fun) do
case Map.get(data, key) do
%Lazy{} = value ->
case Lazy.apply(value, this, key) do
%Lazy{value: {:ok, value}} = result ->
case fun.(value) do
:pop ->
pop(this, key)
{current_value, new_value} ->
{current_value,
%__MODULE__{this | data: Map.put(data, key, Lazy.put(result, new_value))}}
end
_ ->
{nil, data}
end
_ ->
{value, data} = Map.get_and_update(data, key, fun)
{value, %__MODULE__{this | data: data}}
end
end
@spec new(keyword() | map()) :: t()
@doc """
Creates new instance of `LazyMap` with a second parameter being a backed up object,
which would be used for lazy retrieving data for values, when value is an instance
of `Estructura.Lazy`.
## Examples
iex> lm = Estructura.LazyMap.new(
...> [int: Estructura.Lazy.new(&Estructura.LazyInst.parse_int/1)], "42")
...> get_in lm, [:int]
42
"""
def new(initial \\ %{}, lazy_data \\ nil)
def new(kw, lazy_data) when is_list(kw), do: kw |> Map.new() |> new(lazy_data)
def new(%{} = map, lazy_data), do: %__MODULE__{data: map, __lazy_data__: lazy_data}
@spec keys(t()) :: [Map.key()]
@doc "Returns all the keys of the underlying map"
@doc since: "0.4.1"
def keys(%__MODULE__{data: data}), do: Map.keys(data)
@spec fetch_all(t()) :: t()
@doc "Eagerly instantiates the data"
@doc since: "0.4.1"
def fetch_all(%__MODULE__{} = lazy) do
lazy
|> keys()
|> Enum.reduce({%{}, lazy}, fn key, {result, lazy} ->
{value, lazy} = get_and_update(lazy, key, &{&1, &1})
{Map.put(result, key, value), lazy}
end)
end
defimpl Inspect do
@moduledoc false
import Inspect.Algebra
alias Estructura.LazyMap
def inspect(%LazyMap{data: %{} = data}, opts) do
{_, data} = Map.pop(data, :__lazy_data__)
if Keyword.get(opts.custom_options, :lazy_marker, false),
do: concat(["%‹", to_doc(data, opts), "›"]),
else: to_doc(data, opts)
end
end
end