commit
8e70b83d65
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,217 @@ |
|||||||
|
defmodule Explorer.Chain.MapCache do |
||||||
|
@moduledoc """ |
||||||
|
Behaviour for a map-like cache of elements. |
||||||
|
|
||||||
|
A macro based on `ConCache` is provided as well, at its minimum it can be used as; |
||||||
|
``` |
||||||
|
use Explorer.Chain.MapCache, |
||||||
|
name: :name, |
||||||
|
keys: [:fst, :snd] |
||||||
|
``` |
||||||
|
Note: `keys` can also be set singularly with the option `key`, e.g.: |
||||||
|
``` |
||||||
|
use Explorer.Chain.MapCache, |
||||||
|
name: :cache, |
||||||
|
key: :fst, |
||||||
|
key: :snd |
||||||
|
``` |
||||||
|
Additionally all of the options accepted by `ConCache.start_link/1` can be |
||||||
|
provided as well. By default only `ttl_check_interval:` is set (to `false`). |
||||||
|
|
||||||
|
## Named functions |
||||||
|
Apart from the functions defined in the behaviour, the macro will also create |
||||||
|
3 named function for each key, for instance for the key `:fst`: |
||||||
|
- `get_fst` |
||||||
|
- `set_fst` |
||||||
|
- `update_fst` |
||||||
|
These all work as their respective counterparts with the `t:key/0` parameter. |
||||||
|
|
||||||
|
## Callbacks |
||||||
|
Apart from the `callback` that can be set as part of the `ConCache` options, |
||||||
|
two callbacks esist and can be overridden: |
||||||
|
|
||||||
|
`c:handle_update/3` will be called whenever an update is issued. It will receive |
||||||
|
the `t:key/0` that is going to be updated, the current `t:value/0` that is |
||||||
|
stored for said key and the new `t:value/0` to evaluate. |
||||||
|
This allows to select what value to keep and do additional processing. |
||||||
|
By default this just stores the new `t:value/0`. |
||||||
|
|
||||||
|
`c:handle_fallback/1` will be called whenever a get is performed and there is no |
||||||
|
stored value for the given `t:key/0` (or when the value is `nil`). |
||||||
|
It can return 2 different tuples: |
||||||
|
- `{:update, value}` that will cause the value to be returned and the `t:key/0` |
||||||
|
to be `c:update/2`d |
||||||
|
- `{:return, value}` that will cause the value to be returned but not stored |
||||||
|
This allows to define of a default value or perform some actions. |
||||||
|
By default it will simply `{:return, nil}` |
||||||
|
""" |
||||||
|
|
||||||
|
@type key :: atom() |
||||||
|
|
||||||
|
@type value :: term() |
||||||
|
|
||||||
|
@doc """ |
||||||
|
An atom that identifies this cache |
||||||
|
""" |
||||||
|
@callback cache_name :: atom() |
||||||
|
|
||||||
|
@doc """ |
||||||
|
List of `t:key/0`s that the cache contains |
||||||
|
""" |
||||||
|
@callback cache_keys :: [key()] |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Gets everything in a map |
||||||
|
""" |
||||||
|
@callback get_all :: map() |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Gets the stored `t:value/0` for a given `t:key/0` |
||||||
|
""" |
||||||
|
@callback get(atom()) :: value() |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Stores the same `t:value/0` for every `t:key/0` |
||||||
|
""" |
||||||
|
@callback set_all(value()) :: :ok |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Stores the given `t:value/0` for the given `t:key/0` |
||||||
|
""" |
||||||
|
@callback set(key(), value()) :: :ok |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Updates every `t:key/0` with the given `t:value/0` |
||||||
|
""" |
||||||
|
@callback update_all(value()) :: :ok |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Updates the given `t:key/0` (or every `t:key/0` in a list) using the given `t:value/0` |
||||||
|
""" |
||||||
|
@callback update(key() | [key()], value()) :: :ok |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Gets called during an update for the given `t:key/0` |
||||||
|
""" |
||||||
|
@callback handle_update(key(), value(), value()) :: {:ok, value()} | {:error, term()} |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Gets called when a `c:get/1` finds no `t:value/0` |
||||||
|
""" |
||||||
|
@callback handle_fallback(key()) :: {:update, value()} | {:return, value()} |
||||||
|
|
||||||
|
# credo:disable-for-next-line /Complexity/ |
||||||
|
defmacro __using__(opts) when is_list(opts) do |
||||||
|
# name is necessary |
||||||
|
name = Keyword.fetch!(opts, :name) |
||||||
|
keys = Keyword.get(opts, :keys) || Keyword.get_values(opts, :key) |
||||||
|
|
||||||
|
concache_params = |
||||||
|
opts |
||||||
|
|> Keyword.drop([:keys, :key]) |
||||||
|
|> Keyword.put_new(:ttl_check_interval, false) |
||||||
|
|
||||||
|
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks |
||||||
|
quote do |
||||||
|
alias Explorer.Chain.MapCache |
||||||
|
|
||||||
|
@behaviour MapCache |
||||||
|
|
||||||
|
@dialyzer {:nowarn_function, handle_fallback: 1} |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def cache_name, do: unquote(name) |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def cache_keys, do: unquote(keys) |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def get_all do |
||||||
|
Map.new(cache_keys(), fn key -> {key, get(key)} end) |
||||||
|
end |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def get(key) do |
||||||
|
case ConCache.get(cache_name(), key) do |
||||||
|
nil -> |
||||||
|
case handle_fallback(key) do |
||||||
|
{:update, new_value} -> |
||||||
|
update(key, new_value) |
||||||
|
new_value |
||||||
|
|
||||||
|
{:return, new_value} -> |
||||||
|
new_value |
||||||
|
end |
||||||
|
|
||||||
|
value -> |
||||||
|
value |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def set_all(value) do |
||||||
|
Enum.each(cache_keys(), &set(&1, value)) |
||||||
|
end |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def set(key, value) do |
||||||
|
ConCache.put(cache_name(), key, value) |
||||||
|
end |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def update_all(value), do: update(cache_keys(), value) |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def update(keys, value) when is_list(keys) do |
||||||
|
Enum.each(keys, &update(&1, value)) |
||||||
|
end |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def update(key, value) do |
||||||
|
ConCache.update(cache_name(), key, fn old_val -> handle_update(key, old_val, value) end) |
||||||
|
end |
||||||
|
|
||||||
|
### Autogenerated named functions |
||||||
|
|
||||||
|
unquote(Enum.map(keys, &named_functions(&1))) |
||||||
|
|
||||||
|
### Overridable callback functions |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def handle_update(_key, _old_value, new_value), do: {:ok, new_value} |
||||||
|
|
||||||
|
@impl MapCache |
||||||
|
def handle_fallback(_key), do: {:return, nil} |
||||||
|
|
||||||
|
defoverridable handle_update: 3, handle_fallback: 1 |
||||||
|
|
||||||
|
### Supervisor's child specification |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The child specification for a Supervisor. Note that all the `params` |
||||||
|
provided to this function will override the ones set by using the macro |
||||||
|
""" |
||||||
|
def child_spec(params \\ []) do |
||||||
|
params = Keyword.merge(unquote(concache_params), params) |
||||||
|
|
||||||
|
Supervisor.child_spec({ConCache, params}, id: child_id()) |
||||||
|
end |
||||||
|
|
||||||
|
def child_id, do: {ConCache, cache_name()} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# sobelow_skip ["DOS"] |
||||||
|
defp named_functions(key) do |
||||||
|
quote do |
||||||
|
# sobelow_skip ["DOS"] |
||||||
|
def unquote(:"get_#{key}")(), do: get(unquote(key)) |
||||||
|
|
||||||
|
# sobelow_skip ["DOS"] |
||||||
|
def unquote(:"set_#{key}")(value), do: set(unquote(key), value) |
||||||
|
|
||||||
|
# sobelow_skip ["DOS"] |
||||||
|
def unquote(:"update_#{key}")(value), do: update(unquote(key), value) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -1,6 +1,6 @@ |
|||||||
defmodule Explorer.Chain.Supply.CoinMarketCap do |
defmodule Explorer.Chain.Supply.ExchangeRate do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Defines the supply API for calculating supply for coins from coinmarketcap. |
Defines the supply API for calculating supply for coins from exchange_rate.. |
||||||
""" |
""" |
||||||
|
|
||||||
use Explorer.Chain.Supply |
use Explorer.Chain.Supply |
@ -1,52 +0,0 @@ |
|||||||
defmodule Explorer.ExchangeRates.Source.CoinMarketCap do |
|
||||||
@moduledoc """ |
|
||||||
Adapter for fetching exchange rates from https://coinmarketcap.com. |
|
||||||
""" |
|
||||||
|
|
||||||
alias Explorer.ExchangeRates.{Source, Token} |
|
||||||
|
|
||||||
import Source, only: [decode_json: 1, to_decimal: 1] |
|
||||||
|
|
||||||
@behaviour Source |
|
||||||
|
|
||||||
@impl Source |
|
||||||
def format_data(data) do |
|
||||||
for item <- decode_json(data), not is_nil(item["last_updated"]) do |
|
||||||
{last_updated_as_unix, _} = Integer.parse(item["last_updated"]) |
|
||||||
last_updated = DateTime.from_unix!(last_updated_as_unix) |
|
||||||
|
|
||||||
%Token{ |
|
||||||
available_supply: to_decimal(item["available_supply"]), |
|
||||||
total_supply: to_decimal(item["total_supply"]), |
|
||||||
btc_value: to_decimal(item["price_btc"]), |
|
||||||
id: item["id"], |
|
||||||
last_updated: last_updated, |
|
||||||
market_cap_usd: to_decimal(item["market_cap_usd"]), |
|
||||||
name: item["name"], |
|
||||||
symbol: item["symbol"], |
|
||||||
usd_value: to_decimal(item["price_usd"]), |
|
||||||
volume_24h_usd: to_decimal(item["24h_volume_usd"]) |
|
||||||
} |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
@impl Source |
|
||||||
def source_url do |
|
||||||
source_url(1) |
|
||||||
end |
|
||||||
|
|
||||||
def source_url(page) do |
|
||||||
"#{base_url()}/v1/ticker/?start=#{page - 1}00" |
|
||||||
end |
|
||||||
|
|
||||||
def max_page_number, do: config(:pages) |
|
||||||
|
|
||||||
defp base_url do |
|
||||||
config(:base_url) || "https://api.coinmarketcap.com" |
|
||||||
end |
|
||||||
|
|
||||||
@spec config(atom()) :: term |
|
||||||
defp config(key) do |
|
||||||
Application.get_env(:explorer, __MODULE__, [])[key] |
|
||||||
end |
|
||||||
end |
|
@ -1,59 +0,0 @@ |
|||||||
defmodule Explorer.ExchangeRates.Source.CoinMarketCapTest do |
|
||||||
use ExUnit.Case |
|
||||||
|
|
||||||
alias Explorer.ExchangeRates.Token |
|
||||||
alias Explorer.ExchangeRates.Source.CoinMarketCap |
|
||||||
|
|
||||||
@json """ |
|
||||||
[ |
|
||||||
{ |
|
||||||
"id": "poa-network", |
|
||||||
"name": "POA Network", |
|
||||||
"symbol": "POA", |
|
||||||
"rank": "103", |
|
||||||
"price_usd": "0.485053", |
|
||||||
"price_btc": "0.00007032", |
|
||||||
"24h_volume_usd": "20185000.0", |
|
||||||
"market_cap_usd": "98941986.0", |
|
||||||
"available_supply": "203981804.0", |
|
||||||
"total_supply": "254473964.0", |
|
||||||
"max_supply": null, |
|
||||||
"percent_change_1h": "-0.66", |
|
||||||
"percent_change_24h": "12.34", |
|
||||||
"percent_change_7d": "49.15", |
|
||||||
"last_updated": "1523473200" |
|
||||||
} |
|
||||||
] |
|
||||||
""" |
|
||||||
|
|
||||||
describe "format_data/1" do |
|
||||||
test "returns valid tokens with valid data" do |
|
||||||
expected_date = ~N[2018-04-11 19:00:00] |> DateTime.from_naive!("Etc/UTC") |
|
||||||
|
|
||||||
expected = [ |
|
||||||
%Token{ |
|
||||||
available_supply: Decimal.new("203981804.0"), |
|
||||||
total_supply: Decimal.new("254473964.0"), |
|
||||||
btc_value: Decimal.new("0.00007032"), |
|
||||||
id: "poa-network", |
|
||||||
last_updated: expected_date, |
|
||||||
market_cap_usd: Decimal.new("98941986.0"), |
|
||||||
name: "POA Network", |
|
||||||
symbol: "POA", |
|
||||||
usd_value: Decimal.new("0.485053"), |
|
||||||
volume_24h_usd: Decimal.new("20185000.0") |
|
||||||
} |
|
||||||
] |
|
||||||
|
|
||||||
assert expected == CoinMarketCap.format_data(@json) |
|
||||||
end |
|
||||||
|
|
||||||
test "returns nothing when given bad data" do |
|
||||||
bad_data = """ |
|
||||||
[{"id": "poa-network"}] |
|
||||||
""" |
|
||||||
|
|
||||||
assert [] = CoinMarketCap.format_data(bad_data) |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue