lib/unkey_elixir_sdk.ex

defmodule UnkeyElixirSdk do
  use GenServer
  require Logger
  alias HTTPoison
  alias Jason

  @moduledoc """
  Documentation for `UnkeyElixirSdk`.
  """

  # Client
  @doc """
  Start the GenServer
  Returns {:ok, pid}

  ## Examples
      iex> UnkeyElixirSdk.start_link(%{token: "yourtoken"})
      `{:ok, pid}`

    iex> UnkeyElixirSdk.start_link(%{token: "yourtoken", base_url: "theunkeybaseurl"})


  `{:ok, pid}`
  """
  @spec start_link(map) :: {:ok, pid}
  def start_link(default) when is_map(default) do
    :ets.new(:pid_store, [:set, :public, :named_table])

    if map_size(default) === 0 do
      handle_error(
        "You need to specify at least the token in either the supervisor or via the start_link function i.e. start_link(%{token: 'mytoken'})"
      )
    end

    {:ok, pid} = GenServer.start_link(__MODULE__, default)
    # save the PID to a store so the user does not need to alwayd supply it
    :ets.insert(:pid_store, {"pid", pid})

    {:ok, pid}
  end

  @doc """
  Creates an  API key for your users

  Returns a map with the key
  `%{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}`


  ## Examples
      iex> UnkeyElixirSdk.create_key(%{"apiId" => "myapiid"})
      %{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}

     iex>  `UnkeyElixirSdk.create_key(%{
     "apiId" => "myapiid",
     "prefix" => "xyz",
     "byteLength" => 16,
     "ownerId" => "glamboyosa",
     "meta" => %{
      "hello" => "world"
     },
     "expires" => 1_686_941_966_471,
     "ratelimit" => %{
     "type" => "fast",
     "limit" => 10,
     "refillRate" => 1,
     "refillInterval" => 1000
     },
     "remaining" => 5
  })`

    %{"keyId" => "key_cm9vdCBvZiBnb29kXa", "key" => "xyz_AS5HDkXXPot2MMoPHD8jnL"}

  """

  @spec create_key(map) :: map()
  def create_key(opts) when is_map(opts) do
    if(is_nil(Map.get(opts, "apiId"))) do
      handle_error("You need to specify at least the apiId in the form %{apiId: 'yourapiId'}")
    end

    [{_m, pid}] = :ets.lookup(:pid_store, "pid")
    GenServer.call(pid, {:create_key, opts}, 6000)
  end

  @doc """
  Verify a key from your users.  You only need to send the api key from your user.

  Returns a map with whether the key is valid or not. Optionally sends `ownerId` and `meta`

  ## Examples
      iex> UnkeyElixirSdk.verify_key("xyz_AS5HDkXXPot2MMoPHD8jnL")

      `%{"valid" => true,
       "ownerId" => "chronark",
      "meta" => %{
      "hello" => "world"
      }}`
  """

  @spec verify_key(binary) :: map()
  def verify_key(key) when is_binary(key) do
    [{_m, pid}] = :ets.lookup(:pid_store, "pid")
    GenServer.call(pid, {:verify_key, key}, :infinity)
  end

  @doc """
  Delete an api key for your users

  Returns  :ok

  ## Examples
      iex> UnkeyElixirSdk.revoke_key("key_cm9vdCBvZiBnb29kXa")

      :ok
  """

  @spec revoke_key(binary) :: :ok
  def revoke_key(key) when is_binary(key) do
    [{_m, pid}] = :ets.lookup(:pid_store, "pid")
    GenServer.call(pid, {:revoke_key, key}, :infinity)
  end

  @doc """
  Updates the configuration of a key

  Takes in a `key_id` argument and a map whose members are optional
  but must have at most 1 member present.

  ```
  %{
    "name" => "my_new_key",
    "ownerId" => "still_glamboyosa",
     "meta" => %{
      "hello" => "world"
     },
     "expires" => 1_686_941_966_471,
     "ratelimit" => %{
     "type" => "fast",
     "limit" => 15,
     "refillRate" => 2,
     "refillInterval" => 500
     },
     "remaining" => 3
  }
  ```

  Returns  :ok

  ## Examples

  ```
      iex> UnkeyElixirSdk.update_key("key_cm9vdCBvZiBnb29kXa", %{
    "name" => "my_new_key",
    "ownerId" => "still_glamboyosa",
     "meta" => %{
      "hello" => "world"
     },
     "expires" => 1_686_941_966_471,
     "ratelimit" => %{
     "type" => "fast",
     "limit" => 15,
     "refillRate" => 2,
     "refillInterval" => 500
     },
     "remaining" => 3
  })
  ```

      :ok
  """

  @spec update_key(binary(), map()) :: :ok
  def update_key(key_id, opts) when is_map(opts) and is_binary(key_id) do
    [{_m, pid}] = :ets.lookup(:pid_store, "pid")
    GenServer.call(pid, {:update_key, key_id, opts}, :infinity)
  end

  # Server (callbacks)

  @impl true
  def init(elements) do
    case Map.get(elements, :base_url) do
      nil ->
        base_url = "https://api.unkey.dev/v1/keys"

        elements = Map.put(elements, :base_url, base_url)

        {:ok, elements}

      _ ->
        base_url = "https://api.unkey.dev/v1/keys"

        elements = Map.put_new(elements, :base_url, base_url)

        {:ok, elements}
    end
  end

  @impl true
  def handle_call({:create_key, opts}, _from, state) do
    body = opts |> Jason.encode!()

    case HTTPoison.post(state.base_url, body, headers(state.token)) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:reply, Map.new(Jason.decode!(body)), state}

      {:ok, %HTTPoison.Response{status_code: 404}} ->
        handle_error("Not found :(")

      {:ok, %HTTPoison.Response{status_code: 401}} ->
        handle_error("Unauthorised")

      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect(reason)
        handle_error(to_string(reason))

      {:ok, %HTTPoison.Response{body: body}} ->
        handle_error(to_string(body))

      _ ->
        handle_error(to_string("Something went wrong"))
    end
  end

  @impl true
  def handle_call({:revoke_key, key_id}, _from, state) do
    case HTTPoison.delete("#{state.base_url}/#{key_id}", headers(state.token)) do
      {:ok, %HTTPoison.Response{status_code: 200}} ->
        {:reply, :ok, state}

      {:ok, %HTTPoison.Response{status_code: 404}} ->
        handle_error("Not found :(")

      {:ok, %HTTPoison.Response{status_code: 401}} ->
        handle_error("Unauthorised")

      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect(reason)
        handle_error(to_string(reason))

      {:ok, %HTTPoison.Response{body: body}} ->
        handle_error(to_string(body))

      _ ->
        handle_error(to_string("Something went wrong"))
    end
  end

  @impl true
  def handle_call({:update_key, key_id, opts}, _from, state) do
    body =
      %{
        "name" => :undefined,
        "ownerId" => :undefined,
        "meta" => :undefined,
        "expires" => :undefined,
        "ratelimit" => :undefined,
        "remaining" => :undefined
      }
      |> Map.merge(opts)
      |> Map.filter(&(elem(&1, 1) !== :undefined))
      |> Jason.encode!()

    case HTTPoison.put("#{state.base_url}/#{key_id}", body, headers(state.token)) do
      {:ok, %HTTPoison.Response{status_code: 200}} ->
        {:reply, :ok, state}

      {:ok, %HTTPoison.Response{status_code: 404}} ->
        handle_error("Not found :(")

      {:ok, %HTTPoison.Response{status_code: 401}} ->
        handle_error("Unauthorised")

      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect(reason)
        handle_error(to_string(reason))

      {:ok, %HTTPoison.Response{body: body}} ->
        handle_error(to_string(body))

      _ ->
        handle_error(to_string("Something went wrong"))
    end
  end

  @impl true
  def handle_call({:verify_key, key}, _from, state) do
    body =
      %{"key" => key}
      |> Jason.encode!()

    case HTTPoison.post("#{state.base_url}/verify", body, headers(state.token)) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:reply, Jason.decode!(body), state}

      {:ok, %HTTPoison.Response{status_code: 404}} ->
        handle_error("Not found :(")

      {:ok, %HTTPoison.Response{status_code: 401}} ->
        handle_error("Unauthorised")

      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect(reason)
        handle_error(to_string(reason))

      {:ok, %HTTPoison.Response{body: body}} ->
        handle_error(to_string(body))

      _ ->
        handle_error(to_string("Something went wrong"))
    end
  end

  defp handle_error(error_message) when is_binary(error_message) do
    try do
      throw(error_message)
    catch
      err ->
        log_error("Error Message #{err}")
    end
  end

  defp log_error(input) when is_binary(input) do
    IO.puts(input)
  end

  defp headers(token) do
    [{"Authorization", "Bearer #{token}"}, {"Content-Type", "application/json; charset=UTF-8"}]
  end
end