lib/ex_teal/metric/value.ex

defmodule ExTeal.Metric.Value do
  @moduledoc """
  Value Metrics display a single value and it's change compared to a previous
  interval of time.  For example, a value metric might display the total number
  of blog posts created in the last thirty days, versus the previous thirty days.

  Once generated, a value metric module has two functions that can be customized,
  a `calculate/1` and a `ranges/0` function.

  Additionally there are modifier functions that change the behavior and display
  of the metric.
  """

  @type result :: %{current: term(), previous: term()}

  @type multi_result :: [%{label: String.t(), data: result()}]

  @type valid_result :: result() | [multi_result()]

  @callback calculate(ExTeal.Metric.Request.t()) :: valid_result()

  defmacro __using__(_opts) do
    quote do
      @behaviour ExTeal.Metric.Value
      use ExTeal.Metric

      alias ExTeal.Metric.{Request, Result, Value}

      @impl true
      def component, do: "value-metric"

      @doc """
      Performs a count query against the specified schema for the requested
      range.
      """
      @spec count(Request.t(), Ecto.Queryable.t(), atom()) :: Value.result()
      def count(request, query, field \\ :id) do
        Value.aggregate(__MODULE__, request, query, :count, field)
      end

      @doc """
      Performs an average query against the specified schema for the requested
      range specified field
      """
      @spec average(Request.t(), Ecto.Queryable.t(), atom()) :: Value.result()
      def average(request, query, field) do
        Value.aggregate(__MODULE__, request, query, :avg, field)
      end

      @doc """
      Performs a max query against the specified schema for the requested
      range specified field
      """
      @spec maximum(Request.t(), Ecto.Queryable.t(), atom()) :: Value.result()
      def maximum(request, query, field) do
        Value.aggregate(__MODULE__, request, query, :max, field)
      end

      @doc """
      Performs a minimum query against the specified schema for the requested
      range specified field
      """
      @spec minimum(Request.t(), Ecto.Queryable.t(), atom()) :: Value.result()
      def minimum(request, query, field) do
        Value.aggregate(__MODULE__, request, query, :min, field)
      end

      @doc """
      Performs a sum query against the specified schema for the requested
      range specified field
      """
      @spec sum(Request.t(), Ecto.Queryable.t(), atom()) :: Value.result()
      def sum(request, query, field) do
        Value.aggregate(__MODULE__, request, query, :sum, field)
      end

      @impl true
      def format, do: "(0[.]00a)"

      defoverridable format: 0
    end
  end

  use Timex
  alias ExTeal.Metric.Request
  import ExTeal.Metric.QueryHelpers
  import ExTeal.Metric.Ranges

  @spec aggregate(module(), Request.t(), Ecto.Queryable.t(), atom(), atom()) :: map()
  def aggregate(metric_module, request, queryable, aggregate_type, field) do
    %{
      previous: query_for(:previous, metric_module, request, queryable, aggregate_type, field),
      current: query_for(:current, metric_module, request, queryable, aggregate_type, field)
    }
  end

  @spec query_for(atom(), module(), Request.t(), Ecto.Queryable.t(), atom(), atom()) ::
          integer() | float()
  def query_for(current, metric, request, queryable, aggregate_type, field) do
    {start_dt, end_dt} = get_aggregate_datetimes(request)

    queryable
    |> between_current(current, start_dt: start_dt, end_dt: end_dt, metric: metric)
    |> metric.repo().aggregate(aggregate_type, field)
    |> parse()
  end

  def between_current(query, :current, options) do
    between(query, options)
  end

  def between_current(query, :previous, start_dt: start_dt, end_dt: end_dt, metric: metric) do
    duration =
      Interval.new(from: start_dt, until: end_dt)
      |> Interval.duration(:duration)

    between(query, start_dt: Timex.subtract(start_dt, duration), end_dt: start_dt, metric: metric)
  end
end