lib/factory_ex/schema_counter.ex

defmodule FactoryEx.SchemaCounter do
  @moduledoc """
  This is a counter module that wraps :counters

  ### Setting Up
  We can use this in our application by calling `FactoryEx.SchemaCounter.start()`
  inside our `test/test_helper.exs` file.

  ### Using SchemaCounter
  We can then use this module to add unique integers to each field, each value
  will only be used once provided we continue to call `FactoryEx.SchemaCounter.next`

  ```elixir
  FactoryEx.SchemaCounter.next("my_schema_field")
  ```
  """

  # This range is more than enough for schemas
  # max range is 4_294_967_296
  # If you have 500k fields you have a problem
  @phash_range 500_000
  @term_key :factory_ex_counter_ref

  @doc """
  This starts the SchemaCounter, you need to call this before calling any of the other
  functions in this module. Most commonly this will go in your `test/test_helper.exs` file
  """
  @spec start :: :ok
  def start do
    :persistent_term.put(@term_key, :counters.new(@phash_range, []))
  end

  @doc """
  Increments the counter for a specific key

  ### Example

      iex> key = Enum.random(1..10_000_000)
      iex> FactoryEx.SchemaCounter.increment(key)
      :ok
      iex> FactoryEx.SchemaCounter.get(key)
      1
  """
  @spec increment(key :: any) :: :ok
  def increment(key) do
    :counters.add(counter_ref(), key_hash(key), 1)
  end

  @doc """
  Puts the counter for a specific key

  ### Example

      iex> key = Enum.random(1..10_000_000)
      iex> FactoryEx.SchemaCounter.increment(key)
      :ok
      iex> FactoryEx.SchemaCounter.put(key, 1234)
      :ok
      iex> FactoryEx.SchemaCounter.get(key)
      1234
  """
  @spec put(key :: any, value :: integer) :: :ok
  def put(key, value) do
    :counters.put(counter_ref(), key_hash(key), value)
  end

  @doc """
  Gets the counter for a specific key

  ### Example

      iex> key = Enum.random(1..10_000_000)
      iex> FactoryEx.SchemaCounter.increment(key)
      :ok
      iex> FactoryEx.SchemaCounter.get(key)
      1
  """
  @spec get(key :: any) :: integer
  def get(key) do
    :counters.get(counter_ref(), key_hash(key))
  end

  @doc """
  Increments and gets the current value for a specific key

  ### Example

      iex> key = Enum.random(1..10_000_000)
      iex> FactoryEx.SchemaCounter.next(key)
      0
      iex> FactoryEx.SchemaCounter.next(key)
      1
      iex> FactoryEx.SchemaCounter.next(key)
      2
  """
  @spec next(key :: any) :: integer
  def next(key) do
    next_integer = get(key)

    increment(key)

    next_integer
  end

  defp counter_ref, do: :persistent_term.get(@term_key)
  defp key_hash(key), do: :erlang.phash2(key, @phash_range)
end