lib/ex_ipfs_ipns/key.ex

defmodule ExIpfsIpns.Key do
  @moduledoc """
  ExIpfsIpns.Key module handles creating, listing, renaming, and removing IPNS keys.

  Keys are what generates an IPNS name. The key is a cryptographic key pair, and the name is a hash of the public key. The name is what is used to resolve the IPNS name.
  """
  import ExIpfs.Utils

  alias ExIpfs.Api

  @enforce_keys [:name, :id]
  defstruct [:name, :id]

  @typedoc """
  Typespec for a key.
  """
  @type t :: %__MODULE__{
          name: binary(),
          id: binary()
        }

  @typedoc """
  Result from a rename operation.
  """
  @type rename :: %{
          id: binary(),
          was: binary(),
          now: binary(),
          overwrite: boolean()
        }

  @doc """
  Create a new keypair.

  ## Parameters
    `key` - Name of the key to generate.

  ## Options
  https://docs.ipfs.io/reference/http/api/#api-v0-key-gen
  ```
  [
    type: <string>, # Key type.
    size: <int>, # Key size.
    ipns-base: <string>, # IPNS key base.
  ]
  ```
  """
  @spec gen!(binary, list) :: t() | Api.error_response()
  def gen!(key, opts \\ []) do
    Api.post_query("/key/gen?arg=" <> key, query: opts)
    |> new()
  end

  @spec gen(binary | list, list) :: {:ok, t} | Api.error_response()
  def gen(key, opts \\ [])

  def gen(key, opts) when is_binary(key) do
    gen!(key, opts)
    |> okify()
  end

  def gen(keys, opts) when is_list(keys) do
    Enum.map(keys, fn key -> gen!(key, opts) end)
    |> okify()
  end

  @doc """
  Returns the IPNS key for the given name.

  ## Parameters
  `name` - Name of the key to search for.
  `search_type` - Type of search criteria. Defaults to `:name
            If you want to search for the name of a key, use `:id`
            and provide the key id instead.
  """
  @spec get_or_create(binary(), atom) :: {:ok, t()} | ExIpfs.Api.error_response()
  def get_or_create(name, search_type \\ :name) do
    case ExIpfsIpns.Key.exists?(name) do
      true -> ExIpfsIpns.Key.search(name, search_type)
      false -> ExIpfsIpns.Key.gen(name)
    end
  end

  @doc """
  Import a key and prints imported key id.

  ## Parameters
  `key` - Name of the key to import.
  `name` - Name of the key to import.

  ## Options
  https://docs.ipfs.io/reference/http/api/#api-v0-key-import
  ```
  [
    ipns-base: <string>, # IPNS key base.
    format: <string>, # Key format.
    allow-any-key-type: <bool>, # Allow any key type.
  ]
  ```
  """
  @spec import(binary(), binary(), list()) :: {:ok, any} | Api.error_response()
  def import(key, name, opts \\ []) do
    multipart_content(key)
    |> Api.post_multipart("/key/import?arg=" <> name, query: opts)
  end

  @doc """
  List all local keypairs.

  ## Options
  https://docs.ipfs.io/reference/http/api/#api-v0-key-list
  ```
  [
    ipns-base: <string>, # IPNS key base.
  ]
  ```
  """
  @spec list(list()) :: {:ok, list(t())} | Api.error_response()
  def list(opts \\ []) do
    with %{"Keys" => keys} <- Api.post_query("/key/list", query: opts) do
      keys
      |> Enum.map(&new/1)
      |> okify()
    end
  end

  @doc """
  Search for a keypair.

  ## Parameters
  `key` - Name of the key to search for.
  `type` - Type of search criteria. Defaults to `:name
            If you want to search for the name of a key, use `:id`
            and provide the key id.
  """
  @spec search(binary, atom) :: any
  def search(key, type \\ :name) when is_atom(type) do
    with {:ok, keys} <- list() do
      case type do
        :name -> Enum.find(keys, fn k -> k.name == key end)
        :id -> Enum.find(keys, fn k -> k.id == key end)
      end
    end
  end

  @doc """
  Check if a keypair exists. This search both the name and id of the keypair.

  ## Parameters
  `key` - Name of the key to search for.
  `search_type` - Type of search criteria. Defaults to `:both`
            If you want to search for the `id` of a key, use `:name`
            and provide the key name.
            If you want to search for the `name` of a key, use `:id`
            and provide the key id.
  """
  @spec exists?(binary, atom) :: boolean
  def exists?(key, search_type \\ :both) when is_binary(key) do
    case search_type do
      :both -> search(key) != nil or search(key, :id) != nil
      :name -> search(key) != nil
      :id -> search(key, :id) != nil
    end
  end

  @doc """
  Rename a keypair.

  https://docs.ipfs.io/reference/http/api/#api-v0-key-rename

  ## Parameters
    `old` - Name of the key to rename.
    `new` - New name of the key.

  ## Options
  ```
  [
    ipns-base: <string>, # IPNS key base.
    force: <bool>, # Allow to overwrite existing key.
  ]
  ```
  """
  @spec rename(binary(), binary(), list()) :: {:ok, rename()} | Api.error_response()
  def rename(old, new, opts \\ []) do
    Api.post_query("/key/rename?arg=" <> old <> "&arg=" <> new, query: opts)
    |> rename()
    |> okify()
  end

  @doc """
  Remove a keypair.

  https://docs.ipfs.io/reference/http/api/#api-v0-key-rm

  ## Parameters
    `key` - Name of the key to remove.

    Also takes a list of keys to remove.

  ## Options
  ```
  [
    # ipns-base: <string>, # IPNS key base.
  ]
  ```
  """
  @spec rm!(binary(), list()) :: t() | Api.error_response()
  def rm!(key, opts \\ []) do
    Api.post_query("/key/rm?arg=" <> key, query: opts)
    |> new()
  end

  @spec rm(binary() | list(), list()) :: {:ok, t() | list(t())} | Api.error_response()

  def rm(keys, opts \\ [])

  def rm(key, opts) when is_binary(key) do
    rm!(key, opts)
    |> okify()
  end

  def rm(keys, opts) when is_list(keys) do
    Enum.map(keys, fn key -> rm!(key, opts) end)
    |> okify()
  end

  # HELPERS
  defp new({:error, data}), do: {:error, data}

  defp new(key) when is_map(key) do
    %__MODULE__{
      name: key["Name"],
      id: key["Id"]
    }
  end

  defp new(key) when is_binary(key) do
    %__MODULE__{
      name: key,
      id: nil
    }
  end

  @spec rename(any) :: rename() | Api.error_response()
  defp rename(data) do
    case data do
      {:error, response} ->
        {:error, response}

      rename ->
        %{
          id: rename["Id"],
          was: rename["Was"],
          now: rename["Now"],
          overwrite: rename["Overwrite"]
        }
    end
  end
end