lib/web/router/reverse.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule Antikythera.Router.Reverse do
  @moduledoc """
  Internal helper functions for macros in `Antikythera.Router`, to generate/implement reverse routing functions.
  """

  @typep placeholder_filler :: String.t() | [String.t()]

  defun define_path_helper(path_name :: atom, path :: String.t()) :: Macro.t() do
    quote bind_quoted: [path_name: path_name, path: path] do
      {placeholder_vars, placeholder_types} =
        String.split(path, "/", trim: true)
        |> Enum.map(fn
          # credo:disable-for-lines:2 Credo.Check.Warning.UnsafeToAtom
          ":" <> name -> {Macro.var(String.to_atom(name), __MODULE__), quote(do: String.t())}
          "*" <> name -> {Macro.var(String.to_atom(name), __MODULE__), quote(do: [String.t()])}
          _ -> nil
        end)
        |> Enum.reject(&is_nil/1)
        |> Enum.unzip()

      # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
      fun_name = :"#{path_name}_path"

      @spec unquote(fun_name)(unquote_splicing(placeholder_types), %{String.t() => String.t()}) ::
              String.t()
      def unquote(fun_name)(unquote_splicing(placeholder_vars), query_params \\ %{}) do
        Antikythera.Router.Reverse.make_path(
          unquote(path),
          [unquote_splicing(placeholder_vars)],
          query_params
        )
      end
    end
  end

  defun make_path(
          path_pattern :: v[String.t()],
          fillers :: v[[placeholder_filler]],
          query_params :: v[%{String.t() => String.t()}]
        ) :: String.t() do
    replaced_path = fill_path(String.split(path_pattern, "/"), fillers, [])

    query =
      Enum.map_join(query_params, "&", fn
        {"", _} -> raise "empty query parameter name is not allowed"
        {k, s} when is_binary(s) -> URI.encode_www_form(k) <> "=" <> URI.encode_www_form(s)
        {k, i} when is_integer(i) -> URI.encode_www_form(k) <> "=" <> Integer.to_string(i)
      end)

    case query do
      "" -> replaced_path
      _ -> replaced_path <> "?" <> query
    end
  end

  defunp fill_path(patterns :: [String.t()], fillers :: [placeholder_filler], acc :: [String.t()]) ::
           String.t() do
    [], [], acc ->
      Enum.reverse(acc) |> Enum.join("/")

    [":" <> _ | patterns], [filler | fillers], acc ->
      fill_path(patterns, fillers, [encode_segment(filler) | acc])

    ["*" <> _ | patterns], [filler | fillers], acc ->
      fill_path(patterns, fillers, [encode_segment_list(filler) | acc])

    [fixed | patterns], fillers, acc ->
      fill_path(patterns, fillers, [fixed | acc])
  end

  defunp encode_segment(segment :: String.t()) :: String.t() do
    if segment == "", do: raise("empty segment is not allowed")
    URI.encode_www_form(segment)
  end

  defunp encode_segment_list(segments :: [String.t()]) :: String.t() do
    Enum.map_join(segments, "/", &encode_segment/1)
  end
end