lib/time_zone_info/data_persistence/file_system.ex

defmodule TimeZoneInfo.DataPersistence.FileSystem do
  @moduledoc """
  An implementation for the behaviour `TimeZoneInfo.DataPersistence` to persist
  data in the file system.
  """

  @behaviour TimeZoneInfo.DataPersistence

  alias File.Stat
  alias TimeZoneInfo.ExternalTermFormat

  @impl true
  def put(data) do
    with {:ok, path} <- fetch_env(:path),
         {:ok, data} <- ExternalTermFormat.encode(data) do
      File.write(path, data)
    end
  end

  @impl true
  def fetch do
    with {:ok, path} <- fetch_env(:path),
         {:ok, data} <- File.read(path) do
      ExternalTermFormat.decode(data)
    end
  end

  @impl true
  def checksum do
    with {:ok, path} <- fetch_env(:path),
         {:ok, data} <- File.read(path) do
      ExternalTermFormat.checksum(data)
    end
  end

  @impl true
  def fetch_last_update do
    with {:ok, path} <- fetch_env(:path),
         {:ok, %Stat{mtime: mtime}} <- File.stat(path, time: :posix) do
      {:ok, mtime}
    end
  end

  @impl true
  def put_last_update(time) do
    with {:ok, path} <- fetch_env(:path) do
      case File.exists?(path) do
        true -> File.touch(path, time)
        false -> {:error, :enoent}
      end
    end
  end

  @impl true
  def info do
    with {:ok, path} <- fetch_env(:path),
         {:ok, stat} <- File.stat(path),
         {:ok, data} <- File.read(path) do
      %{
        stat: stat,
        path: path,
        checksum: ExternalTermFormat.checksum(data)
      }
    end
  end

  @spec fetch_env(atom()) ::
          {:ok, Path.t()} | {:error, {:invalid_config, Keyword.key() | [Keyword.key()]}}
  defp fetch_env(:path) do
    with {:ok, file_system} <- fetch_env(:file_system) do
      case Keyword.fetch(file_system, :path) do
        {:ok, path} when is_binary(path) -> {:ok, path}
        {:ok, path} -> {:error, {:invalid_config, [file_system: [path: path]]}}
        :error -> {:error, {:invalid_config, [:file_system, :path]}}
      end
    end
  end

  defp fetch_env(:file_system) do
    with :error <- Application.fetch_env(:time_zone_info, :file_system) do
      {:error, {:invalid_config, :file_system}}
    end
  end
end