lib/namor/helpers.ex

defmodule Namor.Helpers do
  @moduledoc """
  Supporting functions for dictionaries and name generation.
  """

  @doc """
  Reads word lists from a base folder and returns a parsed dictionary map.

  A dictionary folder is expected to be a directory containing three files:
  `adjectives.txt`, `nouns.txt`, and `verbs.txt`. Each file should
  have one word per line with no duplicate words.

  If the dictionary name is set to a string, it will only attempt
  to read the one file and return a single list instead of a map.
  Raises a `File.Error` exception if any of the dictionary files
  are not found.

      ┌── reserved.txt
      ├── foobar/
      │ ┌── adjectives.txt
      │ ├── nouns.txt
      │ └── verbs.txt

      iex> Namor.Helpers.get_dict("reserved.txt")
      ["foo", "bar"]

      iex> Namor.Helpers.get_dict(:foobar)
      %{adjectives: ["foo"], nouns: ["bar"], verbs: ["baz"]}

  If not provided, `base_path` will fallback to Namor's internal dictionary
  path.
  """
  @spec get_dict!(binary | atom, binary) :: Namor.Dictionary.t() | [binary]

  def get_dict!(name, base_path \\ nil)

  def get_dict!(name, base_path) when is_binary(name) do
    do_get_dict!(name, base_path)
  end

  def get_dict!(name, base_path) when is_atom(name) do
    Enum.reduce([:adjectives, :nouns, :verbs], %Namor.Dictionary{}, fn type, acc ->
      filename = Atom.to_string(name) |> Path.join("#{type}.txt")
      Map.put(acc, type, do_get_dict!(filename, base_path))
    end)
  end

  defp do_get_dict!(filename, base_path) do
    base_path = base_path || Path.expand("../../dict", __DIR__)

    base_path
    |> Path.join(filename)
    |> File.read!()
    |> String.trim()
    |> String.split("\n")
  end

  @doc """
  Returns a word pattern for adjectives, nouns, and verbs in the
  proper order.

  Only supports patterns with a size up to 4.

      iex> Namor.Helpers.get_pattern(4)
      [:adjectives, :nouns, :nouns, :verbs]
  """
  @spec get_pattern(integer) :: [atom]

  def get_pattern(size) when is_integer(size) and size > 0 and size < 5 do
    case size do
      1 -> Enum.random([[:adjectives], [:nouns], [:verbs]])
      2 -> Enum.random([[:adjectives, :nouns], [:nouns, :verbs]])
      3 -> [:adjectives, :nouns, :verbs]
      4 -> [:adjectives, :nouns, :nouns, :verbs]
    end
  end

  @doc """
  Generates a random salt from a character list that can be appended
  to a name for uniqueness.
  """
  @spec get_salt(integer, charlist) :: [binary]

  def get_salt(size, chars) when is_integer(size) and size > 0 do
    for _ <- 1..size, into: "", do: <<Enum.random(chars)>>
  end
end