lib/enzyme_core.ex

defmodule Bio.Restriction.Enzyme.Core do
  defmacro __using__(_) do
    quote do
      @doc """
      Return a supplier's full name from a code.

      Supplier information is encoded in a list of atoms on each struct, which
      can be used to filter the enzymes that you make available or ensure that
      they are available through a given entity.

      This function allows you to get the full name for a given entity from that
      code. Codes can be given as either an atom or a binary, either will be
      modified to ensure case insensitive comparisons.

      # Examples
          iex>Bio.Restriction.Enzyme.get_supplier(:N)
          "New England Biolabs"

          iex>Bio.Restriction.Enzyme.get_supplier("b")
          "Thermo Fisher Scientific"
      """
      @spec get_supplier(atom() | String.t()) :: String.t()
      def get_supplier(code) when is_binary(code) do
        code
        |> String.upcase()
        |> String.to_existing_atom()
        |> get_supplier()
      end

      def get_supplier(code) when is_atom(code) do
        @suppliers
        |> Map.get(code)
      end

      @doc """
      Get an enzyme struct by name, where name is either a binary or atom and
      case insensitive. Returning an error tuple, the error case is a
      three-tuple with the requested enzyme name as well as the error:

          {:error, :undef_enzyme, <enzyme>}

      # Examples
          iex>Bio.Restriction.Enzyme.get(:CviRI)
          {:ok, %Bio.Restriction.Enzyme{
              blunt?: true,
              cut_1: 2,
              cut_2: 2,
              cut_3: 0,
              cut_4: 0,
              name: "CviRI",
              pattern: "tgca"
            }}

          iex>Bio.Restriction.Enzyme.get("CviRI")
          {:ok, %Bio.Restriction.Enzyme{
              blunt?: true,
              cut_1: 2,
              cut_2: 2,
              cut_3: 0,
              cut_4: 0,
              name: "CviRI",
              pattern: "tgca"
            }}

          iex>Bio.Restriction.Enzyme.get(:not_an_enzyme)
          {:error, :undef_enzyme, "not_an_enzyme"}
      """
      @spec get(atom() | String.t()) :: t()
      def get(name) when is_atom(name) do
        name
        |> Atom.to_string()
        |> get_struct()
      end

      def get(name) when is_binary(name) do
        get_struct(name)
      end

      defp get_struct(name) do
        func_name =
          name
          |> String.downcase()
          |> String.replace("-", "_")
          |> then(&"_#{&1}")

        try do
          {:ok, apply(__MODULE__, String.to_atom(func_name), [])}
        rescue
          _ in UndefinedFunctionError -> {:error, :undef_enzyme, name}
        end
      end

      @doc """
      Like `get/1`, but raises for unknown enzymes or returns the struct.

      # Examples
          iex>Bio.Restriction.Enzyme.get!(:CviRI)
          %Bio.Restriction.Enzyme{
              blunt?: true,
              cut_1: 2,
              cut_2: 2,
              cut_3: 0,
              cut_4: 0,
              name: "CviRI",
              pattern: "tgca"
            }

          iex>Bio.Restriction.Enzyme.get!("CviRI")
          %Bio.Restriction.Enzyme{
              blunt?: true,
              cut_1: 2,
              cut_2: 2,
              cut_3: 0,
              cut_4: 0,
              name: "CviRI",
              pattern: "tgca"
            }
      """
      @spec get!(atom() | String.t()) :: t()
      def get!(name) when is_atom(name) do
        name
        |> Atom.to_string()
        |> get_struct!()
      end

      def get!(name) when is_binary(name) do
        get_struct!(name)
      end

      defp get_struct!(name) do
        func_name =
          name
          |> String.downcase()
          |> String.replace("-", "_")
          |> then(&"_#{&1}")

        try do
          apply(__MODULE__, String.to_atom(func_name), [])
        rescue
          _ in UndefinedFunctionError -> raise "Unknown restriction enzyme \#\{func_name\}"
        end
      end
    end
  end
end