defmodule Vela do @moduledoc """ `Vela` is a tiny library providing easy management of validated cached state with some history. Including `use Vela` in your module would turn the module into struct, setting field accordingly to the specification, passed as a parameter. `Vela` allows the following configurable parameters per field: - `limit` — length of the series to keep (default `5`) - `validator` — validation function as either module implementing `Vela.Validator` behaviour, or a local function (default `fn _, _, _ -> true end`) - `compare_by` — comparator extraction function to extract the value, to be used for comparison, from the underlying terms (by default it returns the whole value) - `invalidator` — the function to be used to invalidate the accumulated values - `errors` — number of errors to keep (default: `5`) Also, Vela accepts `:mη` keyword parameter for the cases when the consumer needs the very custom meta to be passed to the struct. `Vela` implements `Access` behaviour. ## Usage defmodule Vela.Test do use Vela, series1: [limit: 3, errors: 1], # no validation series2: [limit: 2, validator: Vela.Test] series3: [ compare_by: &(&1.created_at), invalidator: &(DateTime.diff(DateTime.utc_now, &1) > 300) # 5 minutes ] @behaviour Vela.Validator @impl Vela.Validator def valid?(%__MODULE__{} = state, key, value) do value > 0 end end In the example above, before any structure update attempt (via `Access`,) this `valid?/3` function would be called. If it returns `true`, the value gets inserted / updated, and the series behind is truncated if needed. It it returns `false`, the state _is not updated_, and the value is put into the map under `__errors__` key of the struct. The length of errors is also configurable via `errors:` keyword parameter. """ @typedoc "Represents a key in the Vela structure" @type serie :: atom() @typedoc "Represents a value in the Vela structure" @type value :: any() @typedoc "Represents the struct created by this behaviour module" @type t :: %{ :__struct__ => atom(), :__errors__ => keyword(), :__meta__ => keyword(), optional(serie()) => [value()] } @doc false defmacro __using__(opts) do {meta, opts} = Keyword.pop(opts, :mη, []) quote bind_quoted: [meta: meta, opts: opts] do @after_compile {Vela, :implement_enumerable} @config opts @fields Keyword.keys(@config) @field_count Enum.count(@fields) fields_index = Enum.with_index(@fields) @fields_ordered Enum.sort( @fields, Keyword.get(meta, :order_by, &(fields_index[&1] <= fields_index[&2])) ) @with_initials [ {:__errors__, []}, {:__meta__, meta} | Enum.zip(@fields_ordered, Stream.cycle([[]])) ] defstruct @with_initials @spec series :: [Vela.serie()] @doc false def series, do: @fields_ordered use Vela.Access, @config @spec purge(Vela.t(), nil | (Vela.serie(), Vela.value() -> boolean())) :: Vela.t() def purge(vela, invalidator \\ nil) def purge(%__MODULE__{} = vela, nil) do purged = for {serie, list} <- vela, compare_by = Keyword.get(@config[serie], :compare_by, & &1), invalidator = Keyword.get(@config[serie], :invalidator, fn _, _ -> true end) do {serie, Enum.filter(list, &invalidator.(serie, compare_by.(&1)))} end struct(vela, purged) end def purge(%__MODULE__{} = vela, invalidator) do purged = for {serie, list} <- vela do {serie, Enum.filter(list, &invalidator.(serie, &1))} end struct(vela, purged) end end end defmacrop do_implement_enumerable(module) do quote location: :keep, bind_quoted: [module: module] do defimpl Enumerable, for: module do @moduledoc false @module module @fields @module.series() @field_count Enum.count(@fields) def count(%@module{} = vela), do: {:ok, @field_count} Enum.each(@fields, fn field -> def member?(%@module{} = vela, unquote(field)), do: {:ok, true} end) def member?(%@module{} = vela, _), do: {:ok, false} def reduce(vela, state_acc, fun, inner_acc \\ @fields) def reduce(%@module{} = vela, {:halt, acc}, _fun, _inner_acc), do: {:halted, acc} def reduce(%@module{} = vela, {:suspend, acc}, fun, inner_acc), do: {:suspended, acc, &reduce(vela, &1, fun, inner_acc)} def reduce(%@module{} = vela, {:cont, acc}, _fun, []), do: {:done, acc} Enum.each(@fields, fn field -> def reduce(%@module{unquote(field) => list} = vela, {:cont, acc}, fun, [ unquote(field) | tail ]), do: reduce(vela, fun.({unquote(field), list}, acc), fun, tail) end) def slice(%@module{}), do: {:error, @module} end end end @doc false def implement_enumerable(%Macro.Env{module: module}, _bytecode), do: do_implement_enumerable(module) @spec map(vela :: t(), ({serie(), value()} -> {serie(), value()})) :: t() def map(%_{} = vela, fun), do: struct(vela, Enum.map(vela, fun)) @spec flat_map(vela :: t(), ({serie(), value()} -> {serie(), value()})) :: list() def flat_map(%_{} = vela, fun \\ & &1) do for {serie, list} <- vela, value <- list, do: fun.({serie, value}) end end