Commit 34b8d1b4 authored by Mikko Ahlroth's avatar Mikko Ahlroth
Browse files

Add typed struct implementation to simplify structs

parent 20515c03
Pipeline #183591936 failed with stages
in 4 minutes and 45 seconds
......@@ -3,38 +3,35 @@ defmodule CodeStats.User.Cache do
User cache is data stored in the user's cache JSON field.
"""
@doc """
Struct for storing user cache data when processing it. Data is read from DB in `db_t` form and
must be passed through `CacheUtils.unformat_cache_from_db/1` to get it in struct form.
"""
defstruct languages: %{},
machines: %{},
dates: %{},
hours: %{},
caching_duration: 0.0,
total_caching_duration: 0.0
import CodeStats.Utils.TypedStruct
@type languages_t :: %{optional(integer) => integer}
@type machines_t :: %{optional(integer) => integer}
@type dates_t :: %{optional(Date.t()) => integer}
@type hours_t :: %{optional(integer) => integer}
@type t :: %__MODULE__{
# Map of language total XPs, key is language ID
languages: languages_t,
# Map of machine total XPs, key is machine ID
machines: machines_t,
# Map of date total XPs, key is date (Date)
dates: dates_t,
# Map of hour total XPs, key is hour (integer)
hours: hours_t,
# How long in seconds it took to update cache partially
caching_duration: float,
# How long in seconds it took to update cache totally
total_caching_duration: float
}
@typedoc """
Struct for storing user cache data when processing it. Data is read from DB in `db_t` form and
must be passed through `CacheUtils.unformat_cache_from_db/1` to get it in struct form.
"""
deftypedstruct(%{
# Map of language total XPs, key is language ID
languages: {languages_t(), %{}},
# Map of machine total XPs, key is machine ID
machines: {machines_t(), %{}},
# Map of date total XPs, key is date (Date)
dates: {dates_t(), %{}},
# Map of hour total XPs, key is hour (integer)
hours: {hours_t(), %{}},
# How long in seconds it took to update cache partially
caching_duration: {float(), 0.0},
# How long in seconds it took to update cache totally
total_caching_duration: {float(), 0.0}
})
# Cache data as read from DB, with string keys
@typedoc """
Cache data as read from DB, with string keys
"""
@type db_t :: %{
optional(String.t()) => integer | float
}
......
......@@ -12,4 +12,87 @@ defmodule CodeStats.Utils do
def get_conf(key) do
Application.get_env(:code_stats, key)
end
defmodule TypedStruct do
@moduledoc """
A typed struct implementation to make them less painful.
"""
@type typespec :: any()
@type enforced :: {typespec(), :enforced}
@type has_default :: {typespec(), any()}
@type field_spec :: typespec() | enforced() | has_default()
@type field_map :: %{optional(atom()) => field_spec()}
@spec deftypedstruct(field_map()) :: term()
@doc """
Create typed struct with a type, default values, and enforced keys.
Input should be a map where the key names are names of the struct keys and values are the
field information. The value can be a typespec, in which case the field will be enforced, or
a 2-tuple of `{typespec, default_value}`, making the field unenforced.
To prevent ambiguity, a value of `{typespec, :ts_enforced}` will be interpreted as enforced,
this will allow you to typespec a 2-tuple.
NOTE: Due to the ambiguity removal technique above, `:ts_enforced` is not allowed as a default
value.
Example:
```elixir
deftypedstruct(%{
# Enforced with simple type
foo: integer(),
# Enforced 2-tuple typed field, written like this to remove ambiguity
bar: {{String.t(), integer()}, :ts_enforced},
# Non-enforced field with default value
baz: {any(), ""}
})
```
"""
defmacro deftypedstruct(fields) do
fields_list =
case fields do
{:%{}, _, flist} -> flist
_ -> raise ArgumentError, "Fields must be a map!"
end
enforced_list =
fields_list
|> Enum.filter(fn
{_, {_, :ts_enforced}} -> true
{_, {_, _}} -> false
{_, _} -> true
end)
|> Enum.map(&elem(&1, 0))
field_specs =
Enum.map(fields_list, fn
{field, {typespec, :ts_enforced}} ->
{field, typespec}
{field, {typespec, _}} ->
{field, typespec}
{field, typespec} ->
{field, typespec}
end)
field_vals =
Enum.map(fields_list, fn
{field, {_, :ts_enforced}} -> field
{field, {_, default}} -> {field, default}
{field, _} -> field
end)
quote do
@type t :: %__MODULE__{unquote_splicing(field_specs)}
@enforce_keys unquote(enforced_list)
defstruct unquote(field_vals)
end
end
end
end
......@@ -13,6 +13,7 @@ defmodule CodeStatsWeb.Gravatar.Proxy do
require Logger
import Ex2ms, only: [fun: 1]
import CodeStats.Utils.TypedStruct
alias CodeStatsWeb.Gravatar.Utils
......@@ -29,28 +30,23 @@ defmodule CodeStatsWeb.Gravatar.Proxy do
@type response :: {:ok, String.t(), binary()} | :error
defmodule Options do
@type t :: %__MODULE__{
name: GenServer.name()
}
@enforce_keys [:name]
defstruct [:name]
deftypedstruct(%{
name: GenServer.name()
})
end
defmodule State do
defmodule FetchData do
@type t :: %__MODULE__{
task: Task.t(),
listeners: [GenServer.from()]
}
@enforce_keys [:task]
defstruct [:task, listeners: []]
deftypedstruct(%{
task: Task.t(),
listeners: {[GenServer.from()], []}
})
end
@type t :: %__MODULE__{
fetches: %{optional(Utils.hash()) => FetchData.t()},
refs: %{optional(reference()) => Utils.hash()}
}
defstruct fetches: %{}, refs: %{}
deftypedstruct(%{
fetches: {%{optional(Utils.hash()) => FetchData.t()}, %{}},
refs: {%{optional(reference()) => Utils.hash()}, %{}}
})
end
### SERVER INTERFACE ###
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment