lib/mix/tasks/ash.formatter.ex

defmodule Mix.Tasks.Ash.Formatter do
  @shortdoc "Generates a .formatter.exs from a list of extensions, and writes it."
  @moduledoc @shortdoc
  use Mix.Task

  @formatter_exs_template """
    # THIS FILE IS AUTOGENERATED USING `mix ash.formatter`
    # DONT MODIFY IT BY HAND
    locals_without_parens = __replace_me__

    [
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
      locals_without_parens: locals_without_parens,
      export: [
        locals_without_parens: locals_without_parens
      ]
    ]

  """

  @spec run(term) :: no_return
  def run(opts) do
    Mix.Task.run("compile")
    {opts, []} = OptionParser.parse!(opts, strict: [check: :boolean, extensions: :string])

    unless opts[:extensions] do
      raise "Must supply a comma separated list of extensions to generate a .formatter.exs for"
    end

    locals_without_parens =
      opts[:extensions]
      |> String.split(",")
      |> Enum.flat_map(fn extension ->
        extension_mod = Module.concat([extension])

        case Code.ensure_compiled(extension_mod) do
          {:module, _module} -> :ok
          other -> raise "Error ensuring extension compiled #{inspect(other)}"
        end

        all_entity_builders(extension_mod.sections())
      end)
      |> Enum.uniq()
      |> Enum.sort()

    contents =
      @formatter_exs_template
      |> String.replace("__replace_me__", inspect(locals_without_parens, limit: :infinity))
      |> Code.format_string!()

    contents_with_newline = [contents, "\n"]

    if opts[:check] do
      if File.read!(".formatter.exs") != IO.iodata_to_binary(contents_with_newline) do
        raise """
        .formatter.exs is not up to date!

        Run the following command and commit the result:

        mix ash.formatter --extensions #{opts[:extensions]}
        """
      else
        IO.puts("The current .formatter.exs is correct")
      end
    else
      File.write!(".formatter.exs", contents_with_newline)
    end
  end

  defp all_entity_builders(sections) do
    Enum.flat_map(sections, fn section ->
      Enum.concat([
        all_entity_option_builders(section),
        section_option_builders(section),
        section_entity_builders(section)
      ])
    end)
  end

  defp section_entity_builders(section) do
    Enum.flat_map(section.entities(), fn entity ->
      entity_builders(entity)
    end) ++ all_entity_builders(section.sections())
  end

  defp entity_builders(entity) do
    arg_count = Enum.count(entity.args)

    [{entity.name, arg_count}, {entity.name, arg_count + 1}] ++
      flat_map_nested_entities(entity, &entity_builders/1)
  end

  defp all_entity_option_builders(section) do
    Enum.flat_map(section.entities, fn entity ->
      entity_option_builders(entity)
    end)
  end

  defp entity_option_builders(entity) do
    entity.schema
    |> Keyword.drop(entity.args)
    |> Enum.map(fn {key, _schema} ->
      {key, 1}
    end)
    |> Kernel.++(flat_map_nested_entities(entity, &entity_option_builders/1))
  end

  defp section_option_builders(section) do
    Enum.map(section.schema, fn {key, _} ->
      {key, 1}
    end)
  end

  defp flat_map_nested_entities(entity, mapper) do
    Enum.flat_map(entity.entities, fn {_, nested_entities} ->
      nested_entities
      |> List.wrap()
      |> Enum.flat_map(fn nested_entity ->
        mapper.(nested_entity)
      end)
    end)
  end
end