lib/exvcr/task/runner.ex

defmodule ExVCR.Task.Runner do
  @moduledoc """
  Provides task processing logics, which will be invoked by custom mix tasks.
  """

  @print_header_format   "  ~-40s ~-30s\n"
  @check_header_format   "  ~-40s ~-20s ~-20s\n"
  @check_content_format  "  ~-40s ~-20w ~-20w\n"
  @date_format   "~4..0B/~2..0B/~2..0B ~2..0B:~2..0B:~2..0B"
  @json_file_pattern ~r/\.json$/

  @doc """
  Use specified path to show the list of vcr cassettes.
  """
  def show_vcr_cassettes(path_list) do
    Enum.each(path_list, fn(path) ->
      if File.exists?(path) do
        read_cassettes(path) |> print_cassettes(path)
        IO.puts ""
      end
    end)
  end

  defp read_cassettes(path) do
    file_names = find_json_files(path)
    date_times = Enum.map(file_names, &(extract_last_modified_time(path, &1)))
    Enum.zip(file_names, date_times)
  end

  defp find_json_files(path) do
    if File.exists?(path) do
      File.ls!(path)
      |> Enum.filter(&(&1 =~ @json_file_pattern))
      |> Enum.sort
    else
      raise ExVCR.PathNotFoundError, message: "Specified path '#{path}' for reading cassettes was not found."
    end
  end

  defp extract_last_modified_time(path, file_name) do
    {{year, month, day}, {hour, min, sec}} = File.stat!(Path.join(path, file_name)).mtime
    sprintf(@date_format, [year, month, day, hour, min, sec])
  end

  defp print_cassettes(items, path) do
    IO.puts "Showing list of cassettes in [#{path}]"
    printf(@print_header_format, ["[File Name]", "[Last Update]"])
    Enum.each(items, fn({name, date}) -> printf(@print_header_format, [name, date]) end)
  end


  @doc """
  Use specified path to delete cassettes.
  """
  def delete_cassettes(path, file_patterns, is_interactive \\ false) do
    path |> find_json_files
         |> Enum.filter(&(&1 =~ file_patterns))
         |> Enum.each(&(delete_and_print_name(path, &1, is_interactive)))
  end

  defp delete_and_print_name(path, file_name, true) do
    line = IO.gets("delete #{file_name}? ")
    if String.upcase(line) == "Y\n" do
      delete_and_print_name(path, file_name, false)
    end
  end

  defp delete_and_print_name(path, file_name, false) do
    case Path.expand(file_name, path) |> File.rm do
      :ok    -> IO.puts "Deleted #{file_name}."
      :error -> IO.puts "Failed to delete #{file_name}"
    end
  end

  @doc """
  Check and show which cassettes are used by the test execution.
  """
  def check_cassettes(record) do
    count_hash = create_count_hash(record.files, %{})
    Enum.each(record.dirs, fn(dir) ->
      IO.puts "Showing hit counts of cassettes in [#{dir}]"
      if File.exists?(dir) do
        cassettes = read_cassettes(dir)
        print_check_cassettes(cassettes, count_hash)
      end
      IO.puts ""
    end)
  end

  defp create_count_hash([], acc), do: acc
  defp create_count_hash([{type, path}|tail], acc) do
    file = Path.basename(path)
    counts = Map.get(acc, file, %ExVCR.Checker.Counts{})
    hash = case type do
      :cache  -> Map.put(acc, file, %{counts | cache: counts.cache + 1})
      :server -> Map.put(acc, file, %{counts | server: counts.server + 1})
    end
    create_count_hash(tail, hash)
  end

  defp print_check_cassettes(items, counts_hash) do
    printf(@check_header_format, ["[File Name]", "[Cassette Counts]", "[Server Counts]"])
    Enum.each(items, fn({name, _date}) ->
      counts = Map.get(counts_hash, name, %ExVCR.Checker.Counts{})
      printf(@check_content_format, [name, counts.cache, counts.server])
    end)
  end

  defp printf(format, params) do
    IO.write sprintf(format, params)
  end

  defp sprintf(format, params) do
    char_list = :io_lib.format(format, params)
    List.to_string(char_list)
  end
end