lib/installer/mix_creator.ex

defmodule MishkaInstaller.Installer.MixCreator do
  @moduledoc """
  In version 0.0.2, the MishkaInstaller library was used to download a dependency from the project's own `mix.exs` file,
  so this module was written to edit this file.
  In fact, this module uses the Sourceror library to change the mentioned `mix.exs` file (with `AST`).
  Another use of this module is reading information from the programmer's Git or custom link.

  - Warning: in the next versions of MishkaInstaller, instead of `mix.exs`, the client project will be downloaded directly from
  `Git` or **hex.pm** site. If this update is executed, the original project will not be changed.
  - This module is not going to be deleted in new versions.
  """

  @doc """
  With the help of this function, you can make a backup copy of `mix.exs` and `mix.lock` of your project and keep it in the
  `deployment/extensions` path.
  If this function is used with one input, it targets the `mix.exs` file, and if the second input is `:lock` atom,
  it targets the `mix.lock` file to keep a backup copy.
  With the help of this function, you can make a backup copy of `mix.exs` and `mix.lock` of your project and keep
  it in the `deployment/extensions` path.
  If this function is used with one input, it targets the `mix.exs` file, and if the second input is `:lock` atom,
  it targets the `mix.lock` file to keep a backup copy.

  ## Examples

  ```elixir
  MishkaInstaller.Installer.MixCreator.backup_mix("mix.exs")
  MishkaInstaller.Installer.MixCreator.backup_mix("mix.lock", :lock)
  ```
  """
  @spec backup_mix(binary) :: {:error, atom} | {:ok, non_neg_integer}
  def backup_mix(mix_path), do: File.copy(mix_path, backup_path())

  @doc """
  Read `backup_mix/1` description.
  """
  @spec backup_mix(binary(), :lock) :: {:error, atom} | {:ok, non_neg_integer}
  def backup_mix(lock_path, :lock), do: File.copy(lock_path, backup_path("original_mix.lock"))

  @doc """
  This function is also the same as the `backup_mix/1` function, with the difference that it returns the backed-up version
  to the project path. Both functions use the `File.copy/2` function just to improve the naming and also to warn the programmer
  that it has been replaced in this file.

  ## Examples

  ```elixir
  MishkaInstaller.Installer.MixCreator.restore_mix("mix.exs")
  MishkaInstaller.Installer.MixCreator.restore_mix("mix.lock", :lock)
  ```
  """
  @spec restore_mix(binary) :: {:error, atom} | {:ok, non_neg_integer}
  def restore_mix(mix_path), do: File.copy(backup_path(), mix_path)

  @doc """
  Read `restore_mix/1` description.
  """

  @spec restore_mix(binary(), :lock) :: {:error, atom} | {:ok, non_neg_integer}
  def restore_mix(lock_path, :lock), do: File.copy(backup_path("original_mix.lock"), lock_path)

  @doc """
  This function receives a list of libraries stored in the `extensions.json` file along with the `mix.exs` path of the file
  that needs to be changed, and after that, it changes the `deps` function in `mix.exs` and overwrites it with the new libraries.

  ## Examples

  ```elixir
  mix_path = MishkaInstaller.get_config(:mix)
  MixCreator.create_mix(mix_path.project[:deps], "mix_path")
  ```

  As you see, we pass the current dependencies to let this function merge it with `extensions.json`
  """
  @spec create_mix([tuple()], binary()) :: :ok | {:error, atom}
  def create_mix(list_of_deps, mix_path) do
    content =
      File.read!(mix_path)
      |> Sourceror.parse_string!()
      |> Sourceror.postwalk(fn
        {:defp, meta, [{:deps, _, _} = fun, [{{_, _, [:do]}, {:__block__, block_meta, [deps]}}]]},
        state ->
          deps =
            MishkaInstaller.Installer.DepHandler.append_mix(list_of_deps)
            |> Enum.map(fn item ->
              [app_name | options] = Tuple.to_list(item)
              create_mix_postwalk(app_name, options, dep_line(deps, block_meta))
            end)

          {{:defp, meta, [fun, [do: {:__block__, block_meta, [deps]}]]}, state}

        other, state ->
          {other, state}
      end)
      |> Sourceror.to_string()

    File.write(mix_path, content)
  end

  defp dep(:git, name, url, dep_line, other_options) do
    {:__block__, [closing: [line: dep_line], line: dep_line],
     [
       {{:__block__, [line: dep_line], [name]},
        [
          {{:__block__, [format: :keyword, line: dep_line], [:git]},
           {:__block__, [delimiter: "\"", line: dep_line], [url]}}
        ] ++ implement_other_options(other_options, dep_line)}
     ]}
  end

  defp dep(:path, name, path, dep_line, other_options) do
    {:__block__, [closing: [line: dep_line], line: dep_line],
     [
       {{:__block__, [line: dep_line], [name]},
        [
          {{:__block__, [format: :keyword, line: dep_line], [:path]},
           {:__block__, [delimiter: "\"", line: dep_line], [path]}}
        ] ++ implement_other_options(other_options, dep_line)}
     ]}
  end

  defp dep(:in_umbrella, name, value, dep_line) do
    {:__block__, [closing: [line: dep_line], line: dep_line],
     [
       {{:__block__, [line: dep_line], [name]},
        [
          {{:__block__, [format: :keyword, line: dep_line], [:in_umbrella]},
           {:__block__, [delimiter: "\"", line: dep_line], [value]}}
        ]}
     ]}
  end

  defp dep(name, full_version, dep_line, other_options) do
    {:{},
     [trailing_comments: [], leading_comments: [], closing: [line: dep_line], line: dep_line],
     [
       {:__block__, [trailing_comments: [], leading_comments: [], line: dep_line], [name]},
       {:__block__,
        [trailing_comments: [], leading_comments: [], delimiter: "\"", line: dep_line],
        clean_mix_version(full_version)},
       implement_other_options(other_options, dep_line, :nested)
     ]}
  end

  defp dep(name, full_version, dep_line) do
    {:__block__, [line: dep_line],
     [{name, {:__block__, [line: dep_line, delimiter: "\""], clean_mix_version(full_version)}}]}
  end

  defp create_mix_postwalk(app_name, [[{:path, value} | other_options] = h | _t], dep_line)
       when is_list(h) do
    dep(:path, app_name, value, dep_line, other_options)
  end

  defp create_mix_postwalk(app_name, [[{:git, value} | other_options] = h | _t], dep_line)
       when is_list(h) do
    dep(:git, app_name, value, dep_line, other_options)
  end

  defp create_mix_postwalk(app_name, [[in_umbrella: value]], dep_line) do
    dep(:in_umbrella, app_name, value, dep_line)
  end

  defp create_mix_postwalk(app_name, [version | t], dep_line)
       when is_binary(version) and t == [] do
    dep(app_name, version, dep_line)
  end

  defp create_mix_postwalk(app_name, [version | [other_options]], dep_line)
       when is_binary(version) and is_list(other_options) do
    dep(app_name, version, dep_line, other_options)
  end

  defp implement_other_options(other_options, dep_line) do
    Enum.map(other_options, fn {key, value} ->
      {{:__block__, [format: :keyword, line: dep_line], [key]},
       {:__block__, [delimiter: "\"", line: dep_line], [value]}}
    end)
  end

  defp implement_other_options(other_options, dep_line, :nested) do
    Enum.map(other_options, fn {key, value} ->
      {{:__block__,
        [trailing_comments: [], leading_comments: [], format: :keyword, line: dep_line], [key]},
       {:__block__, [trailing_comments: [], leading_comments: [], line: dep_line], [value]}}
    end)
  end

  defp dep_line(deps, block_meta) do
    case List.last(deps) do
      {_, meta, _} -> meta[:line] || block_meta[:line]
      _ -> block_meta[:line]
    end + 1
  end

  defp clean_mix_version(full_version) do
    case String.trim(full_version) |> String.split(" ") do
      [type, version] ->
        ["#{type} " <> version]

      _ ->
        full_version =
          full_version
          |> String.replace("~>", "")
          |> String.replace(">=", "")
          |> String.trim()

        ["~> " <> full_version]
    end
  end

  defp backup_path(file_name \\ "original_mix.exs") do
    path = MishkaInstaller.get_config(:project_path)
    Path.join(path, ["deployment/", "extensions/", "#{file_name}"])
  end
end