lib/url/tel.ex

defmodule URL.Tel do
  @moduledoc """
  Parses a `tel` URL
  """
  import NimbleParsec
  import URL.ParseHelpers.{Core, Params, Unwrap}
  alias URL.ParseHelpers.Params

  defstruct tel: nil, params: %{}

  @type t() :: %__MODULE__{
          tel: String.t(),
          params: map()
        }

  @default_territory "US"

  @doc """
  Parse a URI with the `:scheme` of "tel"

  ## Examples

      iex> tel = URI.parse "tel:+61-0407-555-987"
      iex> URL.Tel.parse(tel)
      {:ok, %URL.Tel{tel: "+61 407 555 987", params: %{}}}

      iex> tel = URI.parse "tel:0407-555-987;phone-context=+61"
      iex> URL.Tel.parse(tel)
      {:ok, %URL.Tel{tel: "+61 407 555 987", params: %{"phone-context" => "+61"}}}

  """
  @spec parse(URI.t()) :: {:ok, __MODULE__.t()} | {:error, {module(), binary()}}
  def parse(%URI{scheme: "tel", path: path}) do
    with {:ok, tel} <- unwrap(parse_tel(path)) do
      tel = struct(__MODULE__, tel)

      tel
      |> Map.put(:tel, format(tel))
      |> Params.wrap(:ok)
    end
  end

  if Code.ensure_loaded?(ExPhoneNumber) do
    defp parse_phone_number(number, territory \\ get_territory()) do
      territory = if unknown_territory?(territory), do: @default_territory, else: territory
      ExPhoneNumber.parse(number, to_string(territory))
    end

    defp unknown_territory?(territory) do
      ExPhoneNumber.Metadata.get_country_code_for_region_code(to_string(territory)) == 0
    end

    defp format(%__MODULE__{tel: tel} = url, format \\ :international) do
      phone_context = phone_context(url.params)

      case parse_phone_number(phone_context <> tel) do
        {:ok, tel} -> ExPhoneNumber.format(tel, format)
        other -> other
      end
    end

    defp phone_context(%{"phone-context" => phone_context}) do
      with {:ok, parsed_phone_context} <- unwrap(parse_tel(phone_context)) do
        Keyword.get(parsed_phone_context, :tel)
      else
        _ -> ""
      end
    end

    defp phone_context(_url) do
      ""
    end
  else
    defp format(%__MODULE__{tel: tel}, _format \\ :international) do
      tel
    end
  end

  @doc false
  def get_territory do
    cldr_territory() || gettext_territory() || @default_territory
  end

  if Code.ensure_loaded?(Cldr) do
    defp cldr_territory do
      Cldr.get_locale().territory
    end
  else
    defp cldr_territory do
      nil
    end
  end

  if Code.ensure_loaded?(Gettext) do
    defp gettext_territory do
      case String.split(Gettext.get_locale(), "_") do
        [_lang, territory] -> String.upcase(territory)
        [_lang] -> nil
      end
    end
  else
    defp gettext_territory do
      nil
    end
  end

  defparsecp(
    :parse_tel,
    tel() |> concat(params())
  )
end