defmodule MyspaceIPFS.Utils do @moduledoc """ Some common functions that are used throughout the library. """ @type fspath :: MyspaceIPFS.fspath() @typep response :: Tesla.Env.t() @typep okmapped :: MyspaceIPFS.okmapped() @doc """ Filter out any empty values from a list. Removes nil, {}, [], and "". """ @spec filter_empties(list) :: list def filter_empties(list) do list |> Enum.filter(fn x -> x != nil end) |> Enum.filter(fn x -> x != {} end) |> Enum.filter(fn x -> x != [] end) |> Enum.filter(fn x -> x != "" end) end @doc """ Extracts the data from a response. Given a response, it will structure the data in a way that is easier to work with. IPFS only sends strings. This function will convert the string to a list of maps. """ @spec handle_plain_response({:ok, response}) :: okmapped def handle_plain_response({:ok, response}) do with {_, tokens, _} <- :lexer.string(~c'#{response}') do if tokens == [] do {:ok, []} else tokens # The parser returns a tuple, which means okify() is not needed. |> :parser.parse() end end end # NB: There be dragons in here. This feels like a kludge. # But this is a chokepoint for all the errors so it's probably fine # and can be refactored later. # The error response when status code is 500 can contain important # information. This function will extract that information and return # it as a tuple. @doc false @spec handle_plain_response({atom, binary}) :: any def handle_plain_response({:eserver, response}) do with {_, tokens, _} <- :lexer.string(~c'#{response}') do if tokens == [] do {:ok, []} else tokens |> :parser.parse() |> (fn {_, data} -> {data} end).() |> Tuple.to_list() |> List.first() |> List.first() |> (fn data -> {:eserver, data} end).() end end end @spec handle_plain_response(binary) :: any def handle_plain_response(response) do {error, {:ok, tesla_response}} = response {error, tesla_response} end @doc false @spec handle_json_response({:ok, response}) :: okmapped def handle_json_response({:ok, response}) do response |> Jason.decode!() |> okify() end @doc false @spec handle_json_response({atom, binary}) :: any def handle_json_response({error, response}) do handle_plain_response({error, response}) end @doc false # This function could be overloaded to extract data from response. @spec handle_file_response({:ok, binary}, fspath) :: okmapped def handle_file_response({:ok, response}, output) do File.write!(output, response) {:ok, output} end @spec timestamp :: integer @doc """ Returns the current timestamp in unix time. """ def timestamp() do DateTime.utc_now() |> DateTime.to_unix() end @doc """ Returns the current timestamp in iso8601 format. """ @spec timestamp(:iso) :: binary def timestamp(:iso) do DateTime.utc_now() |> DateTime.to_iso8601() end @doc """ Wraps the data in an elixir standard response tuple. {:ok, data} or {:error, data} """ @spec okify(any) :: {:ok, any} | {:error, any} def okify({:error, _} = err), do: err def okify(res), do: {:ok, res} @doc """ Unlists a list if it only contains one element. """ @spec unlist(list) :: any def unlist(list) do case list do [x] -> x _ -> list end end @doc false @spec write_temp_file(binary, fspath) :: {:ok, fspath} def write_temp_file(data, dir \\ "/tmp") do with dir <- mktempdir(dir), name <- Nanoid.generate(), file <- dir <> "/" <> name do File.write!(file, data) {:ok, file} end end @doc false @spec mktempdir(binary) :: binary def mktempdir(parent_dir) do with dir <- Nanoid.generate(), dir_path <- parent_dir <> "/myspace-" <> dir do File.mkdir_p(dir_path) dir_path end end @doc """ Removes a temporary file. To be used in a pipe, and hence returns the data sent to it. """ def remove_temp_file(data, file) do File.rm_rf!(file) data end end