lib/git_hub_actions/config.ex

defmodule GitHubActions.Config do
  @moduledoc """
  A simple keyword-based configuration API.

  ## Examples

  This module is used to define the configuration for `GitHubActions`.

  ```elixir
  import GitHubActions.Config

  config :linux,
    name: "Ubuntu",
    runs_on: "ubuntu-latest"

  config key: "value"
  """

  alias GitHubActions.Access

  @config_key __MODULE__

  @type key :: atom()
  @type keys :: [atom()]
  @type value :: any()
  @type config :: keyword()

  @doc """
  Reads the configuration from the given `path`.
  """
  @spec read(Path.t()) :: :ok | {:error, :enonet}
  def read(path) do
    path |> File.read!() |> Code.eval_string()
    :ok
  end

  @doc """
  Returns the configuration.
  """
  @spec config :: config()
  def config, do: Process.get(@config_key) || []

  @doc """
  Adds the given `value` to the configuration under the given `key`.

  Returns the configuration that was previously stored.
  """
  @spec config(key(), value()) :: config() | nil
  def config(key, value) when is_atom(key), do: add([{key, value}])

  @doc """
  Adds the given data to the configuration.

  Returns the configuration that was previously stored.
  """
  @spec config(config()) :: config() | nil
  def config(data) when is_list(data) do
    unless Keyword.keyword?(data) do
      raise ArgumentError, "config/1 expected a keyword list, got: #{inspect(data)}"
    end

    add(data)
  end

  @doc """
  Returns the value for `key` or `keys`.

  If the configuration parameter does not exist, the function returns the
  default value.

  ## Examples

      iex> Config.get(:jobs)
      [:linux]

      iex> Config.get(:foo, :bar)
      :bar

      iex> Config.get([:linux, :runs_on])
      "ubuntu-latest"

      iex> Config.get(:foo)
      nil
  """
  @spec get(key() | keys(), value()) :: value()
  def get(keys, default \\ nil), do: Access.get(config!(), keys, default)

  @doc """
  Returns the value for `key` or `keys` in a tuple.

  If the configuration parameter does not exist, the function returns `error`.

  ## Examples

      iex> Config.fetch(:jobs)
      {:ok, [:linux]}

      iex> Config.fetch(:foo)
      :error

      iex> Config.fetch([:linux, :name])
      {:ok, "Ubuntu"}
  """
  @spec fetch(key() | keys()) :: value()
  def fetch(keys), do: Access.fetch(config!(), keys)

  @doc """
  Returns the value for `key` or `keys`.

  ## Examples

      iex> Config.fetch!(:jobs)
      [:linux]

      iex> Config.fetch!([:linux, :runs_on])
      "ubuntu-latest"

      iex> Config.fetch!([:linux, :foo])
      ** (KeyError) key :foo not found in: [name: \"Ubuntu\", runs_on: \"ubuntu-latest\"]
  """
  @spec fetch!(key() | keys()) :: value()
  def fetch!(keys), do: Access.fetch!(config!(), keys)

  defp add([{key, value}] = data) when is_atom(key) and is_list(value) do
    case Keyword.keyword?(value) do
      true ->
        key |> merge(config(), value) |> put()

      false ->
        config() |> Keyword.merge(data) |> put()
    end
  end

  defp add(data), do: config() |> Keyword.merge(data) |> put()

  defp merge(key, config, data) do
    new = config |> Keyword.get(key, []) |> Keyword.merge(data)
    Keyword.put(config, key, new)
  end

  defp put(value), do: Process.put(@config_key, value)

  defp config!, do: Process.get(@config_key) || raise("No configuration available")
end