lib/modkit/mount.ex

defmodule Modkit.Mount do
  alias Modkit.SnakeCase
  @flavors [:elixir, :phoenix, :mix_task]

  defmodule Point do
    @enforce_keys [
      # This is the atom prefix name given in configuration
      :prefix,

      # This is the splitted version of the prefix, containing binaries
      :pre_split,

      # This is the mount path for the prefix.
      :path,

      # This is the used flavor
      :flavor
    ]

    defstruct @enforce_keys
  end

  def define!(points) do
    case define(points) do
      {:ok, mount} -> mount
      {:error, reason} when is_binary(reason) -> raise ArgumentError, message: reason
    end
  end

  def define([]) do
    {:ok, []}
  end

  def define(raw_points) when is_list(raw_points) do
    raw_points
    |> Enum.reduce_while([], fn raw, acc ->
      case define_point(raw) do
        {:ok, point} -> {:cont, [point | acc]}
        {:error, _} = err -> {:halt, err}
      end
    end)
    |> case do
      {:error, _} = err -> err
      points -> {:ok, Enum.sort(points, &sort_points/2)}
    end
  end

  def define(other) do
    {:error, ":mount config option must be a list of points, got: #{inspect(other)}"}
  end

  def define_point({prefix, path} = p)
      when is_atom(prefix) and (is_binary(path) or path == :ignore) do
    define_point(prefix, path, [], p)
  end

  def define_point({prefix, path, opts} = p)
      when is_atom(prefix) and (is_binary(path) or path == :ignore) and is_list(opts) do
    define_point(prefix, path, opts, p)
  end

  def define_point(other) do
    invalid_point(other)
  end

  defp define_point(prefix, path, opts, original)
       when is_atom(prefix) and (is_binary(path) or path == :ignore) and is_list(opts) do
    flavor = Keyword.get(opts, :flavor, :elixir)

    with :ok <- validate(flavor in @flavors, {:invalid_flavor, flavor}) do
      {:ok, %Point{prefix: prefix, path: path, pre_split: Module.split(prefix), flavor: flavor}}
    else
      {:error, reason} -> invalid_point(original, reason)
    end
  end

  defp define_point(_, _, _, original) do
    invalid_point(original)
  end

  defp invalid_point(point) do
    {:error, "invalid point in :mount config option, got: #{inspect(point)}"}
  end

  defp invalid_point(point, reason) do
    {:error,
     "invalid point in :mount config option, got: #{inspect(point)}, error: #{inspect(reason)}"}
  end

  defp sort_points(%{pre_split: a}, %{pre_split: b}) do
    # higher precision goes first
    a > b
  end

  def resolve(mount, module) when is_atom(module) do
    resolve(mount, Module.split(module))
  end

  def resolve([p | points], mod_split) when is_list(mod_split) do
    if prefix_of?(p, mod_split) do
      case p do
        %{path: :ignore} -> :ignore
        _ -> {:ok, p}
      end
    else
      resolve(points, mod_split)
    end
  end

  def resolve([], mod_split) when is_list(mod_split) do
    {:error, :not_mounted}
  end

  def prefix_of?(%{pre_split: pre_split}, mod_split) do
    List.starts_with?(mod_split, pre_split)
  end

  defp validate(true, _) do
    :ok
  end

  defp validate(false, reason) do
    {:error, reason}
  end

  def preferred_path(mount, module) when is_atom(module) do
    with {:ok, modsplit} <- split_mod(module),
         {:ok, point} <- resolve(mount, modsplit) do
      path_rest = unprefix(modsplit, point.pre_split)
      sub_path = create_path(path_rest, point.flavor)
      path = Path.join(:lists.flatten([point.path, sub_path])) <> ".ex"
      {:ok, path}
    end
  end

  defp split_mod(module) do
    {:ok, Module.split(module)}
  rescue
    _ in ArgumentError -> {:error, :not_elixir}
  end

  defp unprefix([a | modrest], [a | prefrest]) do
    unprefix(modrest, prefrest)
  end

  defp unprefix(modrest, []) do
    modrest
  end

  defp create_path(segments, flavor) do
    do_create_path(segments, flavor)
  end

  defp do_create_path(segments, :mix_task) do
    Enum.map_join(segments, ".", &SnakeCase.to_snake/1)
  end

  defp do_create_path([segment | rest], flavor) do
    [path_segment(segment, flavor) | create_path(rest, flavor)]
  end

  defp do_create_path([], _) do
    []
  end

  defp path_segment(segment, :elixir) do
    SnakeCase.to_snake(segment)
  end

  defp path_segment(segment, :phoenix) do
    basename = path_segment(segment, :elixir)

    cond do
      # components
      segment == "Layouts" -> ["components", basename]
      String.ends_with?(segment, "Component") -> ["components", basename]
      String.ends_with?(segment, "Components") -> ["components", basename]
      # controllers
      String.ends_with?(segment, "Controller") -> ["controllers", basename]
      String.ends_with?(segment, "HTML") -> ["controllers", basename]
      String.ends_with?(segment, "JSON") -> ["controllers", basename]
      # live
      String.ends_with?(segment, "Live") -> ["live", basename]
      # views
      String.ends_with?(segment, "View") -> ["views", basename]
      # channels
      String.ends_with?(segment, "Channel") -> ["channels", basename]
      String.ends_with?(segment, "Socket") -> ["channels", basename]
      # *
      :other -> basename
    end
  end

  defp path_segment(segment, :mix_task) do
    path_segment(segment, :elixir)
  end
end