Packages

Money functions for operations on and localization of a money data type with support for ISO 4217 currencies and ISO 24165 digial tokens (crypto currencies).

Current section

Files

Jump to
ex_money lib money currency store.ex
Raw

lib/money/currency/store.ex

defmodule Money.Currency.Store do
@moduledoc """
Persistent-term-backed store for custom currencies.
Custom currencies are currencies defined with `Money.Currency.new/2`,
either at runtime or declaratively through the `:custom_currencies`
application configuration. They are stored in `:persistent_term` for
fast concurrent read access.
This module is started as part of the Money application supervision
tree. Currencies declared in `:custom_currencies` are registered each
time the store starts, so they are restored automatically after a
supervisor restart; currencies added at runtime with
`Money.Currency.new/2` are not.
"""
use GenServer
require Logger
@persistent_key {:ex_money, :custom_currencies}
# ── Client API ────────────────────────────────────────────────
@doc """
Starts the custom currency store.
"""
def start_link(options \\ []) do
GenServer.start_link(__MODULE__, options, name: __MODULE__)
end
@doc """
Returns all custom currencies as a map of `%{currency_code => currency_struct}`.
### Returns
* A map of custom currencies. Returns an empty map if no custom
currencies have been defined.
"""
@spec all() :: %{atom() => Localize.Currency.t()}
def all do
:persistent_term.get(@persistent_key, %{})
end
@doc """
Returns a list of all custom currency codes.
### Returns
* A list of atom currency codes.
"""
@spec codes() :: [atom()]
def codes do
Map.keys(all())
end
@doc """
Stores a custom currency.
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `{:ok, currency}` on success.
* `{:error, Money.CurrencyNotSavedError.t()}` if the store
is not running.
"""
@spec put(Localize.Currency.t()) ::
{:ok, Localize.Currency.t()} | {:error, Exception.t()}
def put(%Localize.Currency{} = currency) do
GenServer.call(__MODULE__, {:put, currency})
catch
:exit, _reason ->
{:error, Money.CurrencyNotSavedError.exception(currency: currency.code)}
end
@doc """
Returns the custom currency for the given code, or `nil`.
### Arguments
* `code` is a currency code atom.
### Returns
* A `t:Localize.Currency.t/0` struct or `nil`.
"""
@spec get(atom()) :: Localize.Currency.t() | nil
def get(code) when is_atom(code) do
Map.get(all(), code)
end
# ── Server callbacks ──────────────────────────────────────────
@impl true
def init(_options) do
# Currencies declared in the `:custom_currencies` configuration are
# materialised here so that they are (re-)registered every time the store
# starts, including after a supervisor restart. Runtime currencies added
# later with `Money.Currency.new/2` do not survive a restart.
currencies = configured_currencies()
:persistent_term.put(@persistent_key, currencies)
{:ok, currencies}
end
defp configured_currencies do
Money.Currency.configured_currency_specs()
|> Enum.reduce(%{}, &put_configured_currency/2)
end
defp put_configured_currency({code, options}, currencies) do
case Money.Currency.build(code, options) do
{:ok, currency} ->
Map.put(currencies, currency.code, currency)
{:error, exception} ->
Logger.warning(
"Failed to register custom currency #{inspect(code)}: " <>
Exception.message(exception)
)
currencies
end
end
defp put_configured_currency(other, currencies) do
Logger.warning(
"Ignoring invalid :custom_currencies entry #{inspect(other)}; " <>
"expected a {code, options} tuple"
)
currencies
end
@impl true
def handle_call({:put, %Localize.Currency{code: code} = currency}, _from, state) do
currencies = :persistent_term.get(@persistent_key, %{})
updated = Map.put(currencies, code, currency)
:persistent_term.put(@persistent_key, updated)
{:reply, {:ok, currency}, state}
end
end