Remove incorrect arities in test descriptions (#10194)

These tests actually use get_and_update_in/3 and pop_in/2.

I removed the arities, rather than correcting them, since it seems
like unnecessary detail here.

Code.require_file("test_helper.exs", __DIR__)

defmodule AccessTest do
  use ExUnit.Case, async: true

  doctest Access

  # Test nil at compilation time does not fail
  # and that @config[:foo] has proper precedence.
  @config nil
  nil = @config[:foo]

  @config [foo: :bar]
  :bar = @config[:foo]

  @mod :lists
  [1, 2, 3] = @mod.flatten([1, [2], 3])

  @mod -13
  -13 = @mod

  test "for nil" do
    assert nil[:foo] == nil
    assert Access.fetch(nil, :foo) == :error
    assert Access.get(nil, :foo) == nil

    assert_raise ArgumentError, "could not put/update key :foo on a nil value", fn ->
      Access.get_and_update(nil, :foo, fn nil -> {:ok, :bar} end)
    end
  end

  test "for keywords" do
    assert [foo: :bar][:foo] == :bar
    assert [foo: [bar: :baz]][:foo][:bar] == :baz
    assert [foo: [bar: :baz]][:fuu][:bar] == nil

    assert Access.fetch([foo: :bar], :foo) == {:ok, :bar}
    assert Access.fetch([foo: :bar], :bar) == :error

    msg = ~r/the Access calls for keywords expect the key to be an atom/

    assert_raise ArgumentError, msg, fn ->
      Access.fetch([], "foo")
    end

    assert Access.get([foo: :bar], :foo) == :bar
    assert Access.get_and_update([], :foo, fn nil -> {:ok, :baz} end) == {:ok, [foo: :baz]}

    assert Access.get_and_update([foo: :bar], :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, [foo: :baz]}

    assert Access.pop([foo: :bar], :foo) == {:bar, []}
    assert Access.pop([], :foo) == {nil, []}
  end

  test "for maps" do
    assert %{foo: :bar}[:foo] == :bar
    assert %{1 => 1}[1] == 1
    assert %{1.0 => 1.0}[1.0] == 1.0
    assert %{1 => 1}[1.0] == nil

    assert Access.fetch(%{foo: :bar}, :foo) == {:ok, :bar}
    assert Access.fetch(%{foo: :bar}, :bar) == :error

    assert Access.get(%{foo: :bar}, :foo) == :bar
    assert Access.get_and_update(%{}, :foo, fn nil -> {:ok, :baz} end) == {:ok, %{foo: :baz}}

    assert Access.get_and_update(%{foo: :bar}, :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, %{foo: :baz}}

    assert Access.pop(%{foo: :bar}, :foo) == {:bar, %{}}
    assert Access.pop(%{}, :foo) == {nil, %{}}
  end

  test "for struct" do
    defmodule Sample do
      defstruct [:name]
    end

    message =
      "function AccessTest.Sample.fetch/2 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.fetch(struct(Sample, []), :name)
    end

    message =
      "function AccessTest.Sample.get_and_update/3 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.get_and_update(struct(Sample, []), :name, fn nil -> {:ok, :baz} end)
    end

    message =
      "function AccessTest.Sample.pop/2 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.pop(struct(Sample, []), :name)
    end
  end

  describe "fetch!/2" do
    assert Access.fetch!(%{foo: :bar}, :foo) == :bar

    assert_raise ArgumentError,
                 ~r/the Access calls for keywords expect the key to be an atom/,
                 fn -> Access.fetch!([], "foo") end

    assert_raise KeyError,
                 ~r/key \"foo\" not found/,
                 fn -> Access.fetch!(nil, "foo") end
  end

  describe "filter/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "filters in get_in" do
      assert get_in(@test_list, [Access.filter(&(&1 > 3))]) == [4, 5, 6]
    end

    test "retains order in get_and_update_in" do
      assert get_and_update_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))], &{&1 * 2, &1}) ==
               {[4, 6], [1, 2, 3, 4, 5, 6]}
    end

    test "retains order in pop_in" do
      assert pop_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))]) == {[2, 3], [1, 4, 5, 6]}
    end

    test "chains with other access functions" do
      mixed_map_and_list = %{foo: Enum.map(@test_list, &%{value: &1})}

      assert get_in(mixed_map_and_list, [:foo, Access.filter(&(&1.value <= 3)), :value]) ==
               [1, 2, 3]
    end
  end

  describe "at/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "returns element from the end if index is negative" do
      assert get_in(@test_list, [Access.at(-2)]) == 5
    end

    test "returns nil if index is out of bounds counting from the end" do
      assert get_in(@test_list, [Access.at(-10)]) == nil
    end

    test "updates the element counting from the end if index is negative" do
      assert get_and_update_in(@test_list, [Access.at(-2)], fn prev ->
               {prev, :foo}
             end) == {5, [1, 2, 3, 4, :foo, 6]}
    end

    test "returns nil and does not update if index is out of bounds" do
      assert get_and_update_in(@test_list, [Access.at(-10)], fn prev ->
               {prev, :foo}
             end) == {nil, [1, 2, 3, 4, 5, 6]}
    end
  end
end

Code.require_file("test_helper.exs", __DIR__)

defmodule AccessTest do
  use ExUnit.Case, async: true

  doctest Access

  # Test nil at compilation time does not fail
  # and that @config[:foo] has proper precedence.
  @config nil
  nil = @config[:foo]

  @config [foo: :bar]
  :bar = @config[:foo]

  @mod :lists
  [1, 2, 3] = @mod.flatten([1, [2], 3])

  @mod -13
  -13 = @mod

  test "for nil" do
    assert nil[:foo] == nil
    assert Access.fetch(nil, :foo) == :error
    assert Access.get(nil, :foo) == nil

    assert_raise ArgumentError, "could not put/update key :foo on a nil value", fn ->
      Access.get_and_update(nil, :foo, fn nil -> {:ok, :bar} end)
    end
  end

  test "for keywords" do
    assert [foo: :bar][:foo] == :bar
    assert [foo: [bar: :baz]][:foo][:bar] == :baz
    assert [foo: [bar: :baz]][:fuu][:bar] == nil

    assert Access.fetch([foo: :bar], :foo) == {:ok, :bar}
    assert Access.fetch([foo: :bar], :bar) == :error

    msg = ~r/the Access calls for keywords expect the key to be an atom/

    assert_raise ArgumentError, msg, fn ->
      Access.fetch([], "foo")
    end

    assert Access.get([foo: :bar], :foo) == :bar
    assert Access.get_and_update([], :foo, fn nil -> {:ok, :baz} end) == {:ok, [foo: :baz]}

    assert Access.get_and_update([foo: :bar], :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, [foo: :baz]}

    assert Access.pop([foo: :bar], :foo) == {:bar, []}
    assert Access.pop([], :foo) == {nil, []}
  end

  test "for maps" do
    assert %{foo: :bar}[:foo] == :bar
    assert %{1 => 1}[1] == 1
    assert %{1.0 => 1.0}[1.0] == 1.0
    assert %{1 => 1}[1.0] == nil

    assert Access.fetch(%{foo: :bar}, :foo) == {:ok, :bar}
    assert Access.fetch(%{foo: :bar}, :bar) == :error

    assert Access.get(%{foo: :bar}, :foo) == :bar
    assert Access.get_and_update(%{}, :foo, fn nil -> {:ok, :baz} end) == {:ok, %{foo: :baz}}

    assert Access.get_and_update(%{foo: :bar}, :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, %{foo: :baz}}

    assert Access.pop(%{foo: :bar}, :foo) == {:bar, %{}}
    assert Access.pop(%{}, :foo) == {nil, %{}}
  end

  test "for struct" do
    defmodule Sample do
      defstruct [:name]
    end

    message =
      "function AccessTest.Sample.fetch/2 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.fetch(struct(Sample, []), :name)
    end

    message =
      "function AccessTest.Sample.get_and_update/3 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.get_and_update(struct(Sample, []), :name, fn nil -> {:ok, :baz} end)
    end

    message =
      "function AccessTest.Sample.pop/2 is undefined (AccessTest.Sample does not implement the Access behaviour)"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.pop(struct(Sample, []), :name)
    end
  end

  describe "fetch!/2" do
    assert Access.fetch!(%{foo: :bar}, :foo) == :bar

    assert_raise ArgumentError,
                 ~r/the Access calls for keywords expect the key to be an atom/,
                 fn -> Access.fetch!([], "foo") end

    assert_raise KeyError,
                 ~r/key \"foo\" not found/,
                 fn -> Access.fetch!(nil, "foo") end
  end

  describe "filter/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "filters in get_in" do
      assert get_in(@test_list, [Access.filter(&(&1 > 3))]) == [4, 5, 6]
    end

    test "retains order in get_and_update_in/1" do
      assert get_and_update_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))], &{&1 * 2, &1}) ==
               {[4, 6], [1, 2, 3, 4, 5, 6]}
    end

    test "retains order in pop_in/1" do
      assert pop_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))]) == {[2, 3], [1, 4, 5, 6]}
    end

    test "chains with other access functions" do
      mixed_map_and_list = %{foo: Enum.map(@test_list, &%{value: &1})}

      assert get_in(mixed_map_and_list, [:foo, Access.filter(&(&1.value <= 3)), :value]) ==
               [1, 2, 3]
    end
  end

  describe "at/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "returns element from the end if index is negative" do
      assert get_in(@test_list, [Access.at(-2)]) == 5
    end

    test "returns nil if index is out of bounds counting from the end" do
      assert get_in(@test_list, [Access.at(-10)]) == nil
    end

    test "updates the element counting from the end if index is negative" do
      assert get_and_update_in(@test_list, [Access.at(-2)], fn prev ->
               {prev, :foo}
             end) == {5, [1, 2, 3, 4, :foo, 6]}
    end

    test "returns nil and does not update if index is out of bounds" do
      assert get_and_update_in(@test_list, [Access.at(-10)], fn prev ->
               {prev, :foo}
             end) == {nil, [1, 2, 3, 4, 5, 6]}
    end
  end
end


Add debug info to the verifier

Add debug info to the verifier

defmodule Mix.Tasks.Api.ValidateInfoEnd do
    use Mix.Task

    @shortdoc "Validates an info_end.json locally"

    @moduledoc """
      You can get an info_end.json on your User Dashboard after the voting ends.
    """

  alias Api.CompetitionActions

  def run(file_path) do
    file = if file_path == "-", do: :stdio, else: File.open!(file_path)
    doc = Poison.decode!(IO.read(file, :all))

    expected_podiums = doc["podiums"]
    votes_in_categories = doc["votes"]

    if Map.keys(expected_podiums) != Map.keys(votes_in_categories) do
      raise "Invalid file. podiums and votes have different categories!"
    end

    valid_teams =
      doc["teams"]
      |> Map.to_list
      |> Enum.reject(&(elem(&1, 1)["disqualified"]))

    valid_team_ids =
      valid_teams
      |> Enum.map(&(elem(&1, 0)))
      |> Enum.sort

    widest_team_id =
      valid_team_ids
      |> Enum.map(&String.length/1)
      |> Enum.sort
      |> List.last

    tie_breakers =
      valid_teams
      |> Map.new(fn {team_slug, attrs} ->
        {
          team_slug,
          attrs["tie_breaker"],
        }
      end)

    differences =
      Enum.map(votes_in_categories, fn {category, votes} ->
        expected_podium = expected_podiums[category]

        calculated_podium = CompetitionActions.calculate_podium(
          CompetitionActions.clean_votes_into_ballots(votes, valid_team_ids),
          valid_team_ids,
          tie_breakers
        )

        if expected_podium != calculated_podium do
          {category, expected_podium, calculated_podium}
        else
          nil
        end
      end)
      |> Enum.reject(&(&1 == nil))

    if Enum.count(differences) == 0 do
      Mix.shell.info "It looks valid! Calculated the voting locally and came to the same results."
      Mix.shell.info ""
      Mix.shell.info "Here's some debug info for the voting."

      Enum.map(votes_in_categories, fn {category, votes} ->
        separator = String.replace(category, ~r/./, "=")
        Mix.shell.info ""
        Mix.shell.info separator
        Mix.shell.info category
        Mix.shell.info separator

        preferences =
          CompetitionActions.clean_votes_into_ballots(votes, valid_team_ids)
          |> Enum.flat_map(&Markus.ballot_to_pairs(&1, valid_team_ids))
          |> Markus.pairs_to_preferences(valid_team_ids)

        widened_scores =
          preferences
          |> Markus.widen_paths(valid_team_ids)

        scores =
          widened_scores
          |> Markus.normalize_margins(valid_team_ids)

        sorted =
          scores
          |> Markus.sort_candidates_with_tie_breakers(valid_team_ids, tie_breakers)

        Mix.shell.info ""
        Mix.shell.info "Preference table"
        Mix.shell.info String.pad_leading("", widest_team_id, "-")
        preferences
        |> format_graph(sorted, widest_team_id)
        |> Mix.shell.info

        Mix.shell.info ""
        Mix.shell.info "Transitive preference table"
        Mix.shell.info String.pad_leading("", widest_team_id, "-")
        widened_scores
        |> format_graph(sorted, widest_team_id)
        |> Mix.shell.info

        Mix.shell.info ""
        Mix.shell.info "Beat table"
        Mix.shell.info String.pad_leading("", widest_team_id, "-")
        scores
        |> format_graph(
          sorted,
          widest_team_id,
           fn _ -> "  W" end
        )
        |> Mix.shell.info

        Mix.shell.info ""
          scores
          |> Markus.schwartz_set(valid_team_ids)
          |> _print_inspect(label: "Schwartz set")

        Mix.shell.info ""
        scores
        |> Markus.rank_candidates(valid_team_ids)
        |> _print_inspect(label: "Ranks")

        Mix.shell.info ""
        tie_breakers
        |> _print_inspect(label: "Tie breakers (lower wins)")

        Mix.shell.info ""
        sorted
        |> _print_inspect(label: "Final order")
      end)
    else
      Mix.shell.error "Things look funny."
      Enum.each(differences, fn {category, expected, calculated} ->
          Mix.shell.info Enum.join([
            "",
            "Podium for #{category} differs.",
            "Expected podium: #{inspect(expected)}",
            "Calculated podium: #{inspect(calculated)}",
          ], "\n")
      end)
    end
  end

  defp format_graph(scores, team_ids, widest_team_id, printer \\ nil) do
    printer = printer || fn n -> String.pad_leading(to_string(n), 3) end

    # Thanks, credo.
    internal_printer = fn scores, a, b ->
      s = scores[[a, b]]
      s_rev = scores[[b, a]]
      cond do
        a == b -> "  ."
        s == 0 and s_rev == 0 -> "  T"
        s == 0 -> "   "
        true -> printer.(s)
      end
    end

    Enum.map(team_ids, fn a ->
      ["#{String.pad_leading(a, widest_team_id)} "] ++ Enum.map(team_ids, fn b ->
        internal_printer.(scores, a, b)
      end)
      |> Enum.join("")
    end)
    |> Enum.join("\n")
  end

  defp _print_inspect(value, opts) do
    prefix = case Keyword.fetch(opts, :label) do
      {:ok, v} -> "#{v}: "
      :error -> ""
    end
    Mix.shell.info "#{prefix}#{inspect(value)}"
  end
end

defmodule Mix.Tasks.Api.ValidateInfoEnd do
    use Mix.Task

    @shortdoc "Validates an info_end.json locally"

    @moduledoc """
      You can get an info_end.json on your User Dashboard after the voting ends.
    """

  alias Api.CompetitionActions

  def run(file_path) do
    file = if file_path == "-", do: :stdio, else: File.open!(file_path)
    doc = Poison.decode!(IO.read(file, :all))

    expected_podiums = doc["podiums"]
    votes_in_categories = doc["votes"]

    if Map.keys(expected_podiums) != Map.keys(votes_in_categories) do
      raise "Invalid file. podiums and votes have different categories!"
    end

    valid_teams =
      doc["teams"]
      |> Map.to_list
      |> Enum.reject(&(elem(&1, 1)["disqualified"]))

    valid_team_ids =
      valid_teams
      |> Enum.map(&(elem(&1, 0)))

    tie_breakers =
      valid_teams
      |> Map.new(fn {team_slug, attrs} ->
        {
          team_slug,
          attrs["tie_breaker"],
        }
      end)

    differences =
      Enum.map(votes_in_categories, fn {category, votes} ->
        expected_podium = expected_podiums[category]

        calculated_podium = CompetitionActions.calculate_podium(
          CompetitionActions.clean_votes_into_ballots(votes, valid_team_ids),
          valid_team_ids,
          tie_breakers
        )

        if expected_podium != calculated_podium do
          {category, expected_podium, calculated_podium}
        else
          nil
        end
      end)
      |> Enum.reject(&(&1 == nil))

    if Enum.count(differences) == 0 do
      Mix.shell.info "It looks valid! Calculated the voting locally and came to the same results."
    else
      Mix.shell.error "Things look funny."
      Enum.each(differences, fn {category, expected, calculated} ->
          Mix.shell.info Enum.join([
            "",
            "Podium for #{category} differs.",
            "Expected podium: #{inspect(expected)}",
            "Calculated podium: #{inspect(calculated)}",
          ], "\n")
      end)
    end
  end
end


At least one but maybe more...

At least one but maybe more...

defmodule SlackCoder.Services.RandomFailureService do
  @moduledoc """
  """
  alias SlackCoder.Models.RandomFailure
  alias SlackCoder.BuildSystem.Job
  alias SlackCoder.Repo
  alias SlackCoder.Models.PR
  alias SlackCoder.Services.PRService
  require Logger

  def save(changeset) do
    changeset
    |> Repo.insert_or_update()
    |> case do
      {:ok, random_failure} ->
        {:ok, random_failure} # All is good
      errored_changeset ->
        Logger.error("Unable to save random failure\n\tchangeset: #{inspect changeset}\n\trandom failure: #{inspect changeset.data}")
        errored_changeset
    end
  end

  def save_random_failure(%PR{last_failed_jobs: []} = pr) do
    pr
    |> PRService.check_failed()
    |> save_random_failure()
  end
  def save_random_failure(%PR{last_failed_jobs: [_ | _] = last_failed_jobs} = pr) when is_list() do
    for %Job{rspec: rspec, rspec_seed: rspec_seed, cucumber: cucumber, cucumber_seed: cucumber_seed} <- last_failed_jobs do
      find_or_create_and_update!(rspec, rspec_seed, pr)
      find_or_create_and_update!(cucumber, cucumber_seed, pr)
    end
  end

  defp find_or_create_and_update!([], _seed, _pr), do: nil
  defp find_or_create_and_update!([{file, line} | failures], seed, pr) do
    RandomFailure.find_unique(pr.owner, pr.repo, file, line)
    |> Repo.one()
    |> case do
      %RandomFailure{count: count} = random_failure ->
        RandomFailure.changeset(random_failure, %{count: count + 1})
      nil ->
        RandomFailure.changeset(%RandomFailure{}, %{
          owner: pr.owner,
          repo: pr.repo,
          pr: pr.number,
          sha: pr.sha,
          file: file,
          line: to_string(line),
          seed: seed,
          count: 1,
          log_url: pr.build_url
        })
    end
    |> save()

    find_or_create_and_update!(failures, seed, pr)
  end
end

defmodule SlackCoder.Services.RandomFailureService do
  @moduledoc """
  """
  alias SlackCoder.Models.RandomFailure
  alias SlackCoder.BuildSystem.Job
  alias SlackCoder.Repo
  alias SlackCoder.Models.PR
  alias SlackCoder.Services.PRService
  require Logger

  def save(changeset) do
    changeset
    |> Repo.insert_or_update()
    |> case do
      {:ok, random_failure} ->
        {:ok, random_failure} # All is good
      errored_changeset ->
        Logger.error("Unable to save random failure\n\tchangeset: #{inspect changeset}\n\trandom failure: #{inspect changeset.data}")
        errored_changeset
    end
  end

  def save_random_failure(%PR{last_failed_jobs: []} = pr) do
    pr
    |> PRService.check_failed()
    |> save_random_failure()
  end
  def save_random_failure(%PR{last_failed_jobs: [_] = last_failed_jobs} = pr) when is_list() do
    for %Job{rspec: rspec, rspec_seed: rspec_seed, cucumber: cucumber, cucumber_seed: cucumber_seed} <- last_failed_jobs do
      find_or_create_and_update!(rspec, rspec_seed, pr)
      find_or_create_and_update!(cucumber, cucumber_seed, pr)
    end
  end

  defp find_or_create_and_update!([], _seed, _pr), do: nil
  defp find_or_create_and_update!([{file, line} | failures], seed, pr) do
    RandomFailure.find_unique(pr.owner, pr.repo, file, line)
    |> Repo.one()
    |> case do
      %RandomFailure{count: count} = random_failure ->
        RandomFailure.changeset(random_failure, %{count: count + 1})
      nil ->
        RandomFailure.changeset(%RandomFailure{}, %{
          owner: pr.owner,
          repo: pr.repo,
          pr: pr.number,
          sha: pr.sha,
          file: file,
          line: to_string(line),
          seed: seed,
          count: 1,
          log_url: pr.build_url
        })
    end
    |> save()

    find_or_create_and_update!(failures, seed, pr)
  end
end


Remove unnecesary env validation from conform.effective.

defmodule Mix.Tasks.Conform.Effective do
  @moduledoc """
  Print the effective configuration for the current project, either
  in it's entirety, or for a specific app, or setting

  ## Examples

      # Output the entire effective configuration
      mix conform.effective

      # Output the effective configuration for an application
      mix conform.effective --app myapp

      # Output the effective configuration for a specific setting
      mix conform.effective --key myapp.key.subkey

      # Output the effective configuration for a specific environment
      mix conform.effective --env prod

      # Output the effective schema for this application. Note that this includes schemas inherited from dependencies
      mix conform.effective --schema

      # output to a file instead of to stdout
      mix conform.effective --out sys.config

  Usage of `--app`, `--key` and ``--schema` options is mutually exclusive, but `--env` can be
  provided for all three use cases.

  """
  @shortdoc "Print the effective configuration for the current project"

  use    Mix.Task
  import Conform.Utils

  # Perform some actions within the context of a specific mix environment
  defp with_env(env, fun) do
    old_env = Mix.env
    try do
      # Change env
      Mix.env(env)
      fun.()
    after
      # Change back
      Mix.env(old_env)
    end
  end

  def run(args) do
    if Mix.Project.umbrella? do
      config = [build_path: Mix.Project.build_path]
      for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do
        Mix.Project.in_project(app, opts[:path], config, fn _ -> do_run(args |> parse_args) end)
      end
    else
      do_run(args |> parse_args)
    end
  end

  defp do_run(args) do
    app       = Mix.Project.config |> Keyword.get(:app)
    conf_path = Path.join([File.cwd!, "config", "#{app}.conf"])
    # Load the base configuration from config.exs if it exists, and validate it
    # If config.exs doesn't exist, proceed if a .conf file exists, otherwise there
    # is no configuration to display
    config = case File.exists?("config/config.exs") do
      true ->
        # Switch environments when reading the config
        with_env args.env, fn ->
          Path.join([File.cwd!, "config", "config.exs"]) |> Mix.Config.read!
        end
      false -> []
    end
    # Read .conf
    conf = case File.exists?(conf_path) do
      false -> nil
      true  ->
        case Conform.Conf.from_file(conf_path) do
          {:error, reason} ->
            error reason
            exit(:normal)
          {:ok, table} ->
            table
        end
    end
    # Load merged schemas
    schema = Conform.Schema.schema_path(app) |> Conform.Schema.load!
    # Translate .conf -> config, using settings from config if one is
    # not provided in the .conf. If no setting is present in either
    # the config, or the .conf, the default from the schema is used.
    effective = Conform.Translate.to_config(schema, config, conf)
    # Print the configuration as requested
    output = case args.options.type do
      :schema -> Conform.Schema.stringify(schema)
      :all    -> Conform.SysConfig.prettify(effective)
      :app    ->
        case Keyword.get(effective, args.options.app) do
          nil ->
            notice "App not found!"
            ""
          app_config ->
            Conform.SysConfig.prettify(app_config)
        end
      :key ->
        effective |> extract_key(args.options.key) |> Conform.SysConfig.prettify
    end

    case args.options.output do
      :stdout -> IO.puts(output)
      path    -> File.write!(path, output)
    end
  end

  defp extract_key(config, path) do
    # Split the path specification into a get_in spec
    spec = path |> String.split(".", trim: true) |> Enum.map(&String.to_atom/1)
    # Extract value
    try do
      config |> get_in(spec)
    rescue
      Protocol.UndefinedError ->
        notice "The value for some key in #{path} doesn't implement the Access protocol!"
        exit(:normal)
    end
  end

  defp parse_args(argv) do
    {args, _, _} = OptionParser.parse(argv)
    env = args |> Keyword.get(:env, "#{Mix.env}") |> String.to_atom
    app = args |> Keyword.get(:app)
    key = args |> Keyword.get(:key)
    schema = args |> Keyword.get(:schema)
    output = args |> Keyword.get(:out, :stdout)

    mutually_exclusive_opts = Enum.filter([app: app, key: key, schema: schema], fn({_, val}) -> val != nil end)
    if Enum.count(mutually_exclusive_opts) > 1 do
      error "the options #{mutually_exclusive_opts} are mutually exclusive"
      exit(:normal)
    end

    # populate options
    options = case {app, key, schema} do
      {nil, nil, nil}  -> %{:type => :all}
      {_, _, true} -> %{:type => :schema}
      {^app, _, _} -> %{:type => :app, :app => app |> String.to_atom}
      {_, ^key, _} -> %{:type => :key, :key => key}
    end
    options = Map.put(options, :output, output)

    %{:env => env, :options => options}
  end
end

defmodule Mix.Tasks.Conform.Effective do
  @moduledoc """
  Print the effective configuration for the current project, either
  in it's entirety, or for a specific app, or setting

  ## Examples

      # Output the entire effective configuration
      mix conform.effective

      # Output the effective configuration for an application
      mix conform.effective --app myapp

      # Output the effective configuration for a specific setting
      mix conform.effective --key myapp.key.subkey

      # Output the effective configuration for a specific environment
      mix conform.effective --env prod

      # Output the effective schema for this application. Note that this includes schemas inherited from dependencies
      mix conform.effective --schema

      # output to a file instead of to stdout
      mix conform.effective --out sys.config

  Usage of `--app`, `--key` and ``--schema` options is mutually exclusive, but `--env` can be
  provided for all three use cases.

  """
  @shortdoc "Print the effective configuration for the current project"

  use    Mix.Task
  import Conform.Utils

  # Perform some actions within the context of a specific mix environment
  defp with_env(env, fun) do
    old_env = Mix.env
    try do
      # Change env
      Mix.env(env)
      fun.()
    after
      # Change back
      Mix.env(old_env)
    end
  end

  def run(args) do
    if Mix.Project.umbrella? do
      config = [build_path: Mix.Project.build_path]
      for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do
        Mix.Project.in_project(app, opts[:path], config, fn _ -> do_run(args |> parse_args) end)
      end
    else
      do_run(args |> parse_args)
    end
  end

  defp do_run(args) do
    app       = Mix.Project.config |> Keyword.get(:app)
    conf_path = Path.join([File.cwd!, "config", "#{app}.conf"])
    # Load the base configuration from config.exs if it exists, and validate it
    # If config.exs doesn't exist, proceed if a .conf file exists, otherwise there
    # is no configuration to display
    config = case File.exists?("config/config.exs") do
      true ->
        # Switch environments when reading the config
        with_env args.env, fn ->
          Path.join([File.cwd!, "config", "config.exs"]) |> Mix.Config.read!
        end
      false -> []
    end
    # Read .conf
    conf = case File.exists?(conf_path) do
      false -> nil
      true  ->
        case Conform.Conf.from_file(conf_path) do
          {:error, reason} ->
            error reason
            exit(:normal)
          {:ok, table} ->
            table
        end
    end
    # Load merged schemas
    schema = Conform.Schema.schema_path(app) |> Conform.Schema.load!
    # Translate .conf -> config, using settings from config if one is
    # not provided in the .conf. If no setting is present in either
    # the config, or the .conf, the default from the schema is used.
    effective = Conform.Translate.to_config(schema, config, conf)
    # Print the configuration as requested
    output = case args.options.type do
      :schema -> Conform.Schema.stringify(schema)
      :all    -> Conform.SysConfig.prettify(effective)
      :app    ->
        case Keyword.get(effective, args.options.app) do
          nil ->
            notice "App not found!"
            ""
          app_config ->
            Conform.SysConfig.prettify(app_config)
        end
      :key ->
        effective |> extract_key(args.options.key) |> Conform.SysConfig.prettify
    end

    case args.options.output do
      :stdout -> IO.puts(output)
      path    -> File.write!(path, output)
    end
  end

  defp extract_key(config, path) do
    # Split the path specification into a get_in spec
    spec = path |> String.split(".", trim: true) |> Enum.map(&String.to_atom/1)
    # Extract value
    try do
      config |> get_in(spec)
    rescue
      Protocol.UndefinedError ->
        notice "The value for some key in #{path} doesn't implement the Access protocol!"
        exit(:normal)
    end
  end

  defp parse_args(argv) do
    {args, _, _} = OptionParser.parse(argv)
    env = args |> Keyword.get(:env, "#{Mix.env}") |> String.to_atom
    app = args |> Keyword.get(:app)
    key = args |> Keyword.get(:key)
    schema = args |> Keyword.get(:schema)
    output = args |> Keyword.get(:out, :stdout)

    mutually_exclusive_opts = Enum.filter([app: app, key: key, schema: schema], fn({_, val}) -> val != nil end)
    if Enum.count(mutually_exclusive_opts) > 1 do
      error "the options #{mutually_exclusive_opts} are mutually exclusive"
      exit(:normal)
    end

    # populate options
    options = case {app, key, schema} do
      {nil, nil, nil}  -> %{:type => :all}
      {_, _, true} -> %{:type => :schema}
      {^app, _, _} -> %{:type => :app, :app => app |> String.to_atom}
      {_, ^key, _} -> %{:type => :key, :key => key}
    end
    options = Map.put(options, :output, output)

    # Make sure env is a valid value
    unless env in [:test, :dev, :prod] do
      error "The value provided for --env is not a valid environment"
      exit(:normal)
    end
    %{:env => env, :options => options}
  end
end


Support for Erlang's #rec{_ = '_' } on match and update (#4523)

defmodule Record do
  @moduledoc """
  Module to work with, define and import records.

  Records are simply tuples where the first element is an atom:

      iex> Record.is_record {User, "john", 27}
      true

  This module provides conveniences for working with records at
  compilation time, where compile-time field names are used to
  manipulate the tuples, providing fast operations on top of
  the tuples' compact structure.

  In Elixir, records are used mostly in two situations:

    1. to work with short, internal data
    2. to interface with Erlang records

  The macros `defrecord/3` and `defrecordp/3` can be used to create
  records while `extract/2` can be used to extract records from Erlang
  files.

  ## Types

  Types can be defined for tuples with the `record/2` macro (only available
  in typespecs). Like with the generated record macros it will expand to
  a tuple.

      defmodule MyModule do
        require Record
        Record.defrecord :user, name: "john", age: 25

        @type user :: record(:user, name: String.t, age: integer)
        # expands to: "@type user :: {:user, String.t, integer}"
      end
  """

  @doc """
  Extracts record information from an Erlang file.

  Returns a quoted expression containing the fields as a list
  of tuples. It expects the record name to be an atom and the
  library path to be a string at expansion time.

  ## Examples

      iex> Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
      [size: :undefined, type: :undefined, access: :undefined, atime: :undefined,
       mtime: :undefined, ctime: :undefined, mode: :undefined, links: :undefined,
       major_device: :undefined, minor_device: :undefined, inode: :undefined,
       uid: :undefined, gid: :undefined]

  """
  def extract(name, opts) when is_atom(name) and is_list(opts) do
    Record.Extractor.extract(name, opts)
  end

  @doc """
  Extracts all records information from an Erlang file.

  Returns a keyword list containing extracted record names as keys, and
  lists of tuples describing the fields as values. It expects a named
  argument :from or :from_lib, which correspond to *include* or
  *include_lib* attribute from Erlang modules, respectively.

  """
  def extract_all(opts) when is_list(opts) do
    Record.Extractor.extract_all(opts)
  end

  @doc """
  Checks if the given `data` is a record of `kind`.

  This is implemented as a macro so it can be used in guard clauses.

  ## Examples

      iex> record = {User, "john", 27}
      iex> Record.is_record(record, User)
      true

  """
  defmacro is_record(data, kind) do
    case Macro.Env.in_guard?(__CALLER__) do
      true ->
        quote do
          is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
            and :erlang.element(1, unquote(data)) == unquote(kind)
        end
      false ->
        quote do
          result = unquote(data)
          is_tuple(result) and tuple_size(result) > 0
            and :erlang.element(1, result) == unquote(kind)
        end
    end
  end

  @doc """
  Checks if the given `data` is a record.

  This is implemented as a macro so it can be used in guard clauses.

  ## Examples

      iex> record = {User, "john", 27}
      iex> Record.is_record(record)
      true
      iex> tuple = {}
      iex> Record.is_record(tuple)
      false

  """
  defmacro is_record(data) do
    case Macro.Env.in_guard?(__CALLER__) do
      true ->
        quote do
          is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
            and is_atom(:erlang.element(1, unquote(data)))
        end
      false ->
        quote do
          result = unquote(data)
          is_tuple(result) and tuple_size(result) > 0
            and is_atom(:erlang.element(1, result))
        end
    end
  end

  @doc """
  Defines a set of macros to create and access a record.

  The macros are going to have `name`, a tag (which defaults)
  to the name if none is given, and a set of fields given by
  `kv`.

  ## Examples

      defmodule User do
        require Record
        Record.defrecord :user, [name: "meg", age: "25"]
      end

  In the example above, a set of macros named `user` but with different
  arities will be defined to manipulate the underlying record:

      # To create records
      record = user()        #=> {:user, "meg", 25}
      record = user(age: 26) #=> {:user, "meg", 26}

      # To get a field from the record
      user(record, :name) #=> "meg"

      # To update the record
      user(record, age: 26) #=> {:user, "meg", 26}

      # Convert a record to a keyword list
      user(record) #=> [name: "meg", age: 26]

  The generated macros can also be used in order to pattern match on records and
  to bind variables during the match:

      record = user() #=> {:user, "meg", 25}

      user(name: name) = record
      name #=> "meg"

  By default, Elixir uses the record name as the first element of
  the tuple (the tag). But it can be changed to something else:

      defmodule User do
        require Record
        Record.defrecord :user, User, name: nil
      end

      require User
      User.user() #=> {User, nil}

  ## Defining extracted records with anonymous functions

  If a record defines an anonymous function, an `ArgumentError`
  will occur if you attempt to create a record with it.
  This can occur unintentionally when defining a record after extracting
  it from an Erlang library that uses anonymous functions for defaults.

      Record.defrecord :my_rec, Record.extract(...)
      #=> ** (ArgumentError) invalid value for record field fun_field,
      cannot escape #Function<12.90072148/2 in :erl_eval.expr/5>.

  To work around this error, redefine the field with your own &M.f/a function,
  like so:

      defmodule MyRec do
        require Record
        Record.defrecord :my_rec, Record.extract(...) |> Keyword.merge(fun_field: &__MODULE__.foo/2)
        def foo(bar, baz), do: IO.inspect({bar, baz})
      end
  """
  defmacro defrecord(name, tag \\ nil, kv) do
    quote bind_quoted: [name: name, tag: tag, kv: kv] do
      tag = tag || name
      fields = Record.__fields__(:defrecord, kv)

      defmacro(unquote(name)(args \\ [])) do
        Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
      end

      defmacro(unquote(name)(record, args)) do
        Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
      end
    end
  end

  @doc """
  Same as `defrecord/3` but generates private macros.
  """
  defmacro defrecordp(name, tag \\ nil, kv) do
    quote bind_quoted: [name: name, tag: tag, kv: kv] do
      tag = tag || name
      fields = Record.__fields__(:defrecordp, kv)

      defmacrop(unquote(name)(args \\ [])) do
        Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
      end

      defmacrop(unquote(name)(record, args)) do
        Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
      end
    end
  end

  # Normalizes of record fields to have default values.
  @doc false
  def __fields__(type, fields) do
    :lists.map(fn
      {key, val} when is_atom(key) ->
        try do
          Macro.escape(val)
        rescue
          e in [ArgumentError] ->
            raise ArgumentError, "invalid value for record field #{key}, " <> Exception.message(e)
        else
          val -> {key, val}
        end
      key when is_atom(key) ->
        {key, nil}
      other ->
        raise ArgumentError, "#{type} fields must be atoms, got: #{inspect other}"
    end, fields)
  end

  # Callback invoked from record/0 and record/1 macros.
  @doc false
  def __access__(atom, fields, args, caller) do
    cond do
      is_atom(args) ->
        index(atom, fields, args)
      Keyword.keyword?(args) ->
        create(atom, fields, args, caller)
      true ->
        case Macro.expand(args, caller) do
          {:{}, _, [^atom|list]} when length(list) == length(fields) ->
            record = List.to_tuple([atom|list])
            Macro.escape(Record.__keyword__(atom, fields, record))
          {^atom, arg} when length(fields) == 1 ->
            Macro.escape(Record.__keyword__(atom, fields, {atom, arg}))
          _ ->
            quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
        end
    end
  end

  # Callback invoked from the record/2 macro.
  @doc false
  def __access__(atom, fields, record, args, caller) do
    cond do
      is_atom(args) ->
        get(atom, fields, record, args)
      Keyword.keyword?(args) ->
        update(atom, fields, record, args, caller)
      true ->
        msg = "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
        raise ArgumentError, msg
    end
  end

  # Gets the index of field.
  defp index(atom, fields, field) do
    if index = find_index(fields, field, 0) do
      index - 1 # Convert to Elixir index
    else
      raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect field}"
    end
  end

  # Creates a new record with the given default fields and keyword values.
  defp create(atom, fields, keyword, caller) do
    in_match = Macro.Env.in_match?(caller)

    # Backward compatiblity for Erlang, a :_ key sets all fields to a default value
    {fields, keyword} = case Keyword.pop(keyword, :_) do
      {nil, _} -> {fields, keyword}
      {default_value, keyword2} ->
        fields2 = Enum.map(fields, fn {k, _} -> {k, default_value} end)
        {fields2, keyword2}
    end

    {match, remaining} =
      Enum.map_reduce(fields, keyword, fn({field, default}, each_keyword) ->
        new_fields =
          case Keyword.has_key?(each_keyword, field) do
            true  -> Keyword.get(each_keyword, field)
            false ->
              case in_match do
                true  -> {:_, [], nil}
                false -> Macro.escape(default)
              end
          end

        {new_fields, Keyword.delete(each_keyword, field)}
      end)

    case remaining do
      [] ->
        {:{}, [], [atom|match]}
      _  ->
        keys = for {key, _} <- remaining, do: key
        raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
    end
  end

  # Updates a record given by var with the given keyword.
  defp update(atom, fields, var, keyword, caller) do
    if Macro.Env.in_match?(caller) do
      raise ArgumentError, "cannot invoke update style macro inside match"
    end

    # Backward compatiblity for Erlang, a :_ key sets all fields to a default value
    keyword = case Keyword.pop(keyword, :_) do
      {nil, _} -> keyword
      {default_value, keyword2} ->
        keyword_prepend = Enum.map(fields, fn {k, _} -> {k, default_value} end)
        keyword_prepend ++ keyword2
    end

    Enum.reduce keyword, var, fn({key, value}, acc) ->
      index = find_index(fields, key, 0)
      if index do
        quote do
          :erlang.setelement(unquote(index), unquote(acc), unquote(value))
        end
      else
        raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
      end
    end
  end

  # Gets a record key from the given var.
  defp get(atom, fields, var, key) do
    index = find_index(fields, key, 0)
    if index do
      quote do
        :erlang.element(unquote(index), unquote(var))
      end
    else
      raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
    end
  end

  defp find_index([{k, _}|_], k, i), do: i + 2
  defp find_index([{_, _}|t], k, i), do: find_index(t, k, i + 1)
  defp find_index([], _k, _i), do: nil

  # Returns a keyword list of the record
  @doc false
  def __keyword__(atom, fields, record) do
    if is_record(record, atom) do
      [_tag|values] = Tuple.to_list(record)
      join_keyword(fields, values, [])
    else
      msg = "expected argument to be a literal atom, literal keyword or a #{inspect atom} record, got runtime: #{inspect record}"
      raise ArgumentError, msg
    end
  end

  defp join_keyword([{field, _default}|fields], [value|values], acc),
    do: join_keyword(fields, values, [{field, value}| acc])
  defp join_keyword([], [], acc),
    do: :lists.reverse(acc)
end

defmodule Record do
  @moduledoc """
  Module to work with, define and import records.

  Records are simply tuples where the first element is an atom:

      iex> Record.is_record {User, "john", 27}
      true

  This module provides conveniences for working with records at
  compilation time, where compile-time field names are used to
  manipulate the tuples, providing fast operations on top of
  the tuples' compact structure.

  In Elixir, records are used mostly in two situations:

    1. to work with short, internal data
    2. to interface with Erlang records

  The macros `defrecord/3` and `defrecordp/3` can be used to create
  records while `extract/2` can be used to extract records from Erlang
  files.

  ## Types

  Types can be defined for tuples with the `record/2` macro (only available
  in typespecs). Like with the generated record macros it will expand to
  a tuple.

      defmodule MyModule do
        require Record
        Record.defrecord :user, name: "john", age: 25

        @type user :: record(:user, name: String.t, age: integer)
        # expands to: "@type user :: {:user, String.t, integer}"
      end
  """

  @doc """
  Extracts record information from an Erlang file.

  Returns a quoted expression containing the fields as a list
  of tuples. It expects the record name to be an atom and the
  library path to be a string at expansion time.

  ## Examples

      iex> Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
      [size: :undefined, type: :undefined, access: :undefined, atime: :undefined,
       mtime: :undefined, ctime: :undefined, mode: :undefined, links: :undefined,
       major_device: :undefined, minor_device: :undefined, inode: :undefined,
       uid: :undefined, gid: :undefined]

  """
  def extract(name, opts) when is_atom(name) and is_list(opts) do
    Record.Extractor.extract(name, opts)
  end

  @doc """
  Extracts all records information from an Erlang file.

  Returns a keyword list containing extracted record names as keys, and
  lists of tuples describing the fields as values. It expects a named
  argument :from or :from_lib, which correspond to *include* or
  *include_lib* attribute from Erlang modules, respectively.

  """
  def extract_all(opts) when is_list(opts) do
    Record.Extractor.extract_all(opts)
  end

  @doc """
  Checks if the given `data` is a record of `kind`.

  This is implemented as a macro so it can be used in guard clauses.

  ## Examples

      iex> record = {User, "john", 27}
      iex> Record.is_record(record, User)
      true

  """
  defmacro is_record(data, kind) do
    case Macro.Env.in_guard?(__CALLER__) do
      true ->
        quote do
          is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
            and :erlang.element(1, unquote(data)) == unquote(kind)
        end
      false ->
        quote do
          result = unquote(data)
          is_tuple(result) and tuple_size(result) > 0
            and :erlang.element(1, result) == unquote(kind)
        end
    end
  end

  @doc """
  Checks if the given `data` is a record.

  This is implemented as a macro so it can be used in guard clauses.

  ## Examples

      iex> record = {User, "john", 27}
      iex> Record.is_record(record)
      true
      iex> tuple = {}
      iex> Record.is_record(tuple)
      false

  """
  defmacro is_record(data) do
    case Macro.Env.in_guard?(__CALLER__) do
      true ->
        quote do
          is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
            and is_atom(:erlang.element(1, unquote(data)))
        end
      false ->
        quote do
          result = unquote(data)
          is_tuple(result) and tuple_size(result) > 0
            and is_atom(:erlang.element(1, result))
        end
    end
  end

  @doc """
  Defines a set of macros to create and access a record.

  The macros are going to have `name`, a tag (which defaults)
  to the name if none is given, and a set of fields given by
  `kv`.

  ## Examples

      defmodule User do
        require Record
        Record.defrecord :user, [name: "meg", age: "25"]
      end

  In the example above, a set of macros named `user` but with different
  arities will be defined to manipulate the underlying record:

      # To create records
      record = user()        #=> {:user, "meg", 25}
      record = user(age: 26) #=> {:user, "meg", 26}

      # To get a field from the record
      user(record, :name) #=> "meg"

      # To update the record
      user(record, age: 26) #=> {:user, "meg", 26}

      # Convert a record to a keyword list
      user(record) #=> [name: "meg", age: 26]

  The generated macros can also be used in order to pattern match on records and
  to bind variables during the match:

      record = user() #=> {:user, "meg", 25}

      user(name: name) = record
      name #=> "meg"

  By default, Elixir uses the record name as the first element of
  the tuple (the tag). But it can be changed to something else:

      defmodule User do
        require Record
        Record.defrecord :user, User, name: nil
      end

      require User
      User.user() #=> {User, nil}

  ## Defining extracted records with anonymous functions

  If a record defines an anonymous function, an `ArgumentError`
  will occur if you attempt to create a record with it.
  This can occur unintentionally when defining a record after extracting
  it from an Erlang library that uses anonymous functions for defaults.

      Record.defrecord :my_rec, Record.extract(...)
      #=> ** (ArgumentError) invalid value for record field fun_field,
      cannot escape #Function<12.90072148/2 in :erl_eval.expr/5>.

  To work around this error, redefine the field with your own &M.f/a function,
  like so:

      defmodule MyRec do
        require Record
        Record.defrecord :my_rec, Record.extract(...) |> Keyword.merge(fun_field: &__MODULE__.foo/2)
        def foo(bar, baz), do: IO.inspect({bar, baz})
      end
  """
  defmacro defrecord(name, tag \\ nil, kv) do
    quote bind_quoted: [name: name, tag: tag, kv: kv] do
      tag = tag || name
      fields = Record.__fields__(:defrecord, kv)

      defmacro(unquote(name)(args \\ [])) do
        Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
      end

      defmacro(unquote(name)(record, args)) do
        Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
      end
    end
  end

  @doc """
  Same as `defrecord/3` but generates private macros.
  """
  defmacro defrecordp(name, tag \\ nil, kv) do
    quote bind_quoted: [name: name, tag: tag, kv: kv] do
      tag = tag || name
      fields = Record.__fields__(:defrecordp, kv)

      defmacrop(unquote(name)(args \\ [])) do
        Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
      end

      defmacrop(unquote(name)(record, args)) do
        Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
      end
    end
  end

  # Normalizes of record fields to have default values.
  @doc false
  def __fields__(type, fields) do
    :lists.map(fn
      {key, val} when is_atom(key) ->
        try do
          Macro.escape(val)
        rescue
          e in [ArgumentError] ->
            raise ArgumentError, "invalid value for record field #{key}, " <> Exception.message(e)
        else
          val -> {key, val}
        end
      key when is_atom(key) ->
        {key, nil}
      other ->
        raise ArgumentError, "#{type} fields must be atoms, got: #{inspect other}"
    end, fields)
  end

  # Callback invoked from record/0 and record/1 macros.
  @doc false
  def __access__(atom, fields, args, caller) do
    cond do
      is_atom(args) ->
        index(atom, fields, args)
      Keyword.keyword?(args) ->
        create(atom, fields, args, caller)
      true ->
        case Macro.expand(args, caller) do
          {:{}, _, [^atom|list]} when length(list) == length(fields) ->
            record = List.to_tuple([atom|list])
            Macro.escape(Record.__keyword__(atom, fields, record))
          {^atom, arg} when length(fields) == 1 ->
            Macro.escape(Record.__keyword__(atom, fields, {atom, arg}))
          _ ->
            quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
        end
    end
  end

  # Callback invoked from the record/2 macro.
  @doc false
  def __access__(atom, fields, record, args, caller) do
    cond do
      is_atom(args) ->
        get(atom, fields, record, args)
      Keyword.keyword?(args) ->
        update(atom, fields, record, args, caller)
      true ->
        msg = "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
        raise ArgumentError, msg
    end
  end

  # Gets the index of field.
  defp index(atom, fields, field) do
    if index = find_index(fields, field, 0) do
      index - 1 # Convert to Elixir index
    else
      raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect field}"
    end
  end

  # Creates a new record with the given default fields and keyword values.
  defp create(atom, fields, keyword, caller) do
    in_match = Macro.Env.in_match?(caller)

    {match, remaining} =
      Enum.map_reduce(fields, keyword, fn({field, default}, each_keyword) ->
        new_fields =
          case Keyword.has_key?(each_keyword, field) do
            true  -> Keyword.get(each_keyword, field)
            false ->
              case in_match do
                true  -> {:_, [], nil}
                false -> Macro.escape(default)
              end
          end

        {new_fields, Keyword.delete(each_keyword, field)}
      end)

    case remaining do
      [] ->
        {:{}, [], [atom|match]}
      _  ->
        keys = for {key, _} <- remaining, do: key
        raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
    end
  end

  # Updates a record given by var with the given keyword.
  defp update(atom, fields, var, keyword, caller) do
    if Macro.Env.in_match?(caller) do
      raise ArgumentError, "cannot invoke update style macro inside match"
    end

    Enum.reduce keyword, var, fn({key, value}, acc) ->
      index = find_index(fields, key, 0)
      if index do
        quote do
          :erlang.setelement(unquote(index), unquote(acc), unquote(value))
        end
      else
        raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
      end
    end
  end

  # Gets a record key from the given var.
  defp get(atom, fields, var, key) do
    index = find_index(fields, key, 0)
    if index do
      quote do
        :erlang.element(unquote(index), unquote(var))
      end
    else
      raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
    end
  end

  defp find_index([{k, _}|_], k, i), do: i + 2
  defp find_index([{_, _}|t], k, i), do: find_index(t, k, i + 1)
  defp find_index([], _k, _i), do: nil

  # Returns a keyword list of the record
  @doc false
  def __keyword__(atom, fields, record) do
    if is_record(record, atom) do
      [_tag|values] = Tuple.to_list(record)
      join_keyword(fields, values, [])
    else
      msg = "expected argument to be a literal atom, literal keyword or a #{inspect atom} record, got runtime: #{inspect record}"
      raise ArgumentError, msg
    end
  end

  defp join_keyword([{field, _default}|fields], [value|values], acc),
    do: join_keyword(fields, values, [{field, value}| acc])
  defp join_keyword([], [], acc),
    do: :lists.reverse(acc)
end