lib/config_server.ex

defmodule ConfigServer do
  @moduledoc """
  This module implements a GenServer that monitors a git repository.
  The content of the repository is parsed and changes are handed to a callback.
  """
  use GenServer

  alias ConfigServer.{Parser, Git}

  defstruct [
    :repo_url,
    :repo_path,
    :config,
    :commit_hash,
    :pull_interval_ms,
    :state_change_fun
  ]

  # API
  @doc """
  Retrieve the current config.
  """
  def config() do
    GenServer.call(__MODULE__, :config)
  end

  # Server functions
  @spec start_link([]) :: {:ok, pid()}
  def start_link([]) do
    get_application_env() |> start_link()
  end

  @spec start_link(%{
      repo_url: String.t(),
      repo_path: String.t(),
      pull_interval_ms: integer(),
      state_change_fun: nil | fun()
    }) :: {:ok, pid()}
  def start_link(%{repo_url: repo_url, repo_path: repo_path, pull_interval_ms: pull_interval_ms, state_change_fun: state_change_fun} = args)
    when is_binary(repo_url) and is_binary(repo_path) and
         is_integer(pull_interval_ms) and pull_interval_ms > 0 and
         (is_nil(state_change_fun) or is_function(state_change_fun)) do
    {:ok, _} = GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  @impl true
  def init(%{repo_url: repo_url, repo_path: repo_path, pull_interval_ms: pull_interval_ms, state_change_fun: state_change_fun})
    when is_binary(repo_url) and is_binary(repo_path) and
         is_integer(pull_interval_ms) and pull_interval_ms > 0 and
         (is_nil(state_change_fun) or is_function(state_change_fun)) do
    initial_state = %ConfigServer{
      repo_url: repo_url,
      repo_path: repo_path,
      pull_interval_ms: pull_interval_ms,
      state_change_fun: state_change_fun
    }
    schedule_next_pull(initial_state)
    pull_configs_from_repo(initial_state)
    {:ok, parse_configs_from_filesystem(initial_state)}
  end

  @impl true
  def handle_call(:config, _from, %{config: config} = state) do
    {:reply, config, state}
  end

  @impl true
  def handle_info(:pull_configs, state) do
    schedule_next_pull(state)
    spawn(fn() -> pull_configs_from_repo(state) end)
    {:noreply, state}
  end

  @impl true
  def handle_cast(:parse_configs, state) do
    {:noreply, parse_configs_from_filesystem(state)}
  end

  defp parse_configs_from_filesystem(%ConfigServer{repo_path: repo_path, state_change_fun: state_change_fun} = state) do
    next_config = Parser.parse_directory(repo_path)
    next_commit_hash = Git.commit_hash(repo_path)

    next_state =
      %ConfigServer{state | 
        config:      next_config, 
        commit_hash: next_commit_hash 
      }

    if is_function(state_change_fun) && state.commit_hash != next_state.commit_hash do
      spawn(fn() -> state_change_fun.(state.config, next_state.config) end)
    end

    next_state
  end

  defp pull_configs_from_repo(%ConfigServer{repo_path: repo_path, repo_url: repo_url}) do
    Git.refresh(repo_url, repo_path)
    GenServer.cast(__MODULE__, :parse_configs)
  end

  defp schedule_next_pull(%ConfigServer{pull_interval_ms: pull_interval_ms}) do
    Process.send_after(self(), :pull_configs, pull_interval_ms, [])
  end

  def get_application_env() do
    %{
      repo_url: Application.get_env(:config_server, :repo_url), 
      repo_path: Application.get_env(:config_server, :repo_path),
      pull_interval_ms: Application.get_env(:config_server, :pull_interval_ms), 
      state_change_fun: Application.get_env(:config_server, :state_change_fun)
    }
  end

end