lib/ngrok.ex

defmodule Ngrok do
  @moduledoc """
  By including this supervisor in your application, an Ngrok process will be
  started when your application starts and stopped when your application stops

  ## Options

  * `port` (`integer` / `required`) - The port to tunnel.
  * `protocol` (`atom` / default: `https`) - The protocol to tunnel.
    (`http` / `https` / `tcp` / `tls`)
  * `additional_arguments` (`[String.t()]` / default: `[]`) - Additional options
    to pass to `ngrok`.
  * `name` (Supervisor name / default: `Ngrok`) - Name of the supervisor in your
    application.
  * `api_url` (url / default: `http://localhost:4040/api/tunnels`) - The URL of
    Ngrok's API (used to retrieve its settings).
  * `sleep_between_attempts` (timeout (ms) / default: 200) - The amount of sleep
    (in ms) to put between attempts to connect to Ngrok

  ## Config

      config :ngrok,
          # The name / path of the Ngrok executable
          executable: "ngrok"
  """

  use Supervisor

  alias Ngrok.Executable
  alias Ngrok.Settings

  @type destination_port :: pos_integer()
  @type protocol :: :http | :https | :tcp | :tls
  @type strict_name :: atom() | {:global, atom()}
  @type start_opts :: [
          port: destination_port(),
          additional_arguments: [String.t()],
          protocol: protocol(),
          api_url: String.t(),
          sleep_between_attempts: timeout(),
          name: strict_name()
        ]

  @spec start_link(opts :: start_opts()) :: Supervisor.on_start()
  def start_link(opts),
    do:
      Supervisor.start_link(
        __MODULE__,
        Keyword.take(opts, [
          :port,
          :additional_arguments,
          :protocol,
          :api_url,
          :name,
          :sleep_between_attempts
        ]),
        name: Keyword.get(opts, :name, __MODULE__)
      )

  @impl Supervisor
  def init(opts),
    do:
      Supervisor.init(
        [
          {Executable, executable_config(opts)},
          {Settings, settings_config(opts)}
        ],
        strategy: :rest_for_one
      )

  @doc """
  Retrieves the public URL of the Ngrok tunnel

  ## Example

      Ngrok.public_url # => http://(.*).ngrok.io/
  """
  @spec public_url(name :: strict_name()) :: String.t()
  def public_url(name \\ __MODULE__),
    do: name |> extend_name(Settings) |> Settings.get("public_url")

  @spec executable_config(start_opts()) :: Executable.start_opts()
  defp executable_config(opts) do
    [
      port: Keyword.fetch!(opts, :port),
      additional_arguments: Keyword.get(opts, :additional_arguments, []),
      protocol: Keyword.get(opts, :protocol, :https),
      name: opts |> Keyword.get(:name, __MODULE__) |> extend_name(Executable)
    ]
  end

  @spec settings_config(start_opts()) :: Settings.start_opts()
  defp settings_config(opts) do
    [
      port: Keyword.fetch!(opts, :port),
      protocol: Keyword.get(opts, :protocol, :https),
      api_url: Keyword.get(opts, :api_url, "http://localhost:4040/api/tunnels"),
      sleep_between_attempts: Keyword.get(opts, :sleep_between_attempts, 200),
      name: opts |> Keyword.get(:name, __MODULE__) |> extend_name(Settings)
    ]
  end

  @spec extend_name(name :: strict_name(), extend :: atom()) :: strict_name()
  defp extend_name(name, extend)

  # credo:disable-for-lines:2 Credo.Check.Warning.UnsafeToAtom
  defp extend_name({:global, name}, extend) when is_atom(name),
    do: {:global, Module.concat(name, extend)}

  # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
  defp extend_name(name, extend) when is_atom(name), do: Module.concat(name, extend)
end