defmodule Scribe do @moduledoc ~S""" Pretty-print tables of structs and maps ## Usage A common use case is printing results from an Ecto query. # %User{id: nil, email: nil} iex(1)> User |> limit(5) |> Repo.all() |> Scribe.print() +-------------+----------------------------+------+ | :__struct__ | :email | :id | +-------------+----------------------------+------+ | User | "myles_fisher@beahan.com" | 5171 | | User | "dawson_bartell@lynch.org" | 4528 | | User | "hassan1972@langworth.com" | 1480 | | User | "kiera.schulist@koch.com" | 2084 | | User | "cynthia1970@mann.name" | 6599 | +-------------+----------------------------+------+ ## Pagination Scribe uses [pane](https://github.com/codedge-llc/pane) to paginate large tables. Use with `Scribe.console/2`. # %User{id: nil, email: nil, first_name: nil, last_name: nil} iex(1)> User |> limit(5) |> Repo.all |> Scribe.console +-------------+------------------------+-------------+-------+------------+ | :__struct__ | :email | :first_name | :id | :last_name | +-------------+------------------------+-------------+-------+------------+ | User | "celestine_satterfield | "Gene" | 9061 | "Krajcik" | | User | "lynn1978@bednar.org" | "Maeve" | 9865 | "Gerlach" | | User | "melisa1975@hilll.biz" | "Theodora" | 2262 | "Wunsch" | | User | "furman.grady@ryan.org | "Oswaldo" | 4977 | "Simonis" | | User | "caesar_hirthe@reynold | "Arjun" | 3907 | "Prohaska" | +-------------+------------------------+-------------+-------+------------+ [1 of 1] (j)next (k)prev (q)quit ## Printing Custom Tables `Scribe.print/2` takes a list of of columns on the `:data` options key to customize output. You can use either the atom key or customize the header with `{"Custom Title", :key}`. # %User{id: nil, email: nil, first_name: nil, last_name: nil} User |> limit(5) |> Repo.all |> Scribe.print(data: [{"ID", :id}, :first_name, :last_name]) +------+--------------+-------------+ | "ID" | :first_name | :last_name | +------+--------------+-------------+ | 9061 | "Gene" | "Krajcik" | | 9865 | "Maeve" | "Gerlach" | | 2262 | "Theodora" | "Wunsch" | | 4977 | "Oswaldo" | "Simonis" | | 3907 | "Arjun" | "Prohaska" | +------+--------------+-------------+ ### Function Columns You can specify functions that take the given row's struct or map as its only argument. # %User{id: nil, email: nil, first_name: nil, last_name: nil} results = User |> limit(5) |> Repo.all |> Scribe.print(data: [{"ID", :id}, {"Full Name", fn(x) -> "#{x.last_name}, #{x.first_name}" end}]) +--------------------------+----------------------------------------------+ | "ID" | "Full Name" | +--------------------------+----------------------------------------------+ | 9061 | "Krajcik, Gene" | | 9865 | "Gerlach, Maeve" | | 2262 | "Wunsch, Theodora" | | 4977 | "Simonis, Oswaldo" | | 3907 | "Prohaska, Arjun" | +--------------------------+----------------------------------------------+ ## Styling Options ### Width Pass a `width` option to define table width. iex> Scribe.print(data, width: 80) +-------------------+-----------------------------------------------------+ | :id | :key | +-------------------+-----------------------------------------------------+ | 910 | "B1786AC67B4DEB19" | | 313 | "30CB8A2DE4750070" | | 25 | "D0859205FC7E7298" | | 647 | "8F0060AD0BD6AB04" | | 253 | "65509A684D619182" | +-------------------+-----------------------------------------------------+ ### Disable Colors iex> Scribe.print(data, colorize: false) ### Text Alignment Pass an `alignment` option of `:left`, `:center`, or `:right` for text alignment. Defaults to `:left`. iex> Scribe.print(data, alignment: :center) +------------------------------+------------+--------+ | :body | :current | :id | +------------------------------+------------+--------+ | "A rather short string." | true | 1234 | | "A rather short string." | false | 2222 | | "A rather short string." | true | 4444 | +------------------------------+------------+--------+ iex> Scribe.print(data, alignment: :right) +------------------------------+------------+--------+ | :body | :current | :id | +------------------------------+------------+--------+ | "A rather short string." | true | 1234 | | "A rather short string." | false | 2222 | | "A rather short string." | true | 4444 | +------------------------------+------------+--------+ ### Styles Scribe supports five styling formats natively, with support for custom adapters. See more in `Scribe.Style`. """ alias Scribe.Table @typedoc ~S""" Printable data. Can be either a struct, map, or list of structs/maps. """ @type data :: [map] | [struct] | map | struct @typedoc ~S""" Options for configuring table output. - `:alignment` - Define text alignment in cells. Defaults to `:left`. - `:colorize` - When `false`, disables colored output. Defaults to `true` - `:data` - Defines table headers - `:device` - Where to print (defaults to STDOUT) - `:style` - Style callback module. Defaults to `Scribe.Style.Default` - `:width` - Defines table width. Defaults to `:infinite` """ @type format_opts :: [ alignment: atom, colorize: boolean, data: [...], style: module, width: integer ] @doc ~S""" Prints a table from given data. ## Examples iex> print([]) :ok iex> Scribe.print(%{key: :value, test: 1234}, colorize: false) +----------+---------+ | :key | :test | +----------+---------+ | :value | 1234 | +----------+---------+ :ok """ @spec print(data, format_opts) :: :ok def print(_results, opts \\ []) def print([], _opts), do: :ok def print(results, opts) do dev = opts |> Keyword.get(:device, :stdio) results = results |> format(opts) dev |> IO.puts(results) end @doc ~S""" Paginates data and starts a pseudo-interactive console. """ @spec console(data, format_opts) :: no_return def console(results, opts \\ []) do results |> format(opts) |> Pane.console() end @doc ~S""" Prints a table from given data and returns the data. Useful for inspecting pipe chains. ## Examples iex> Scribe.inspect([]) [] iex> Scribe.inspect(%{key: :value, test: 1234}, colorize: false) +----------+---------+ | :key | :test | +----------+---------+ | :value | 1234 | +----------+---------+ %{test: 1234, key: :value} """ @spec inspect(data, format_opts) :: data def inspect(results, opts \\ []) do print(results, opts) results end @doc ~S""" Formats data into a printable table string. ## Examples iex> format([]) :ok iex> format(%{test: 1234}, colorize: false) "+---------+\n| :test |\n+---------+\n| 1234 |\n+---------+\n" """ @spec format(data) :: String.t() | :ok def format(_results, opts \\ []) def format([], _opts), do: :ok def format(results, opts) when not is_list(results) do format([results], opts) end def format(results, opts) do keys = fetch_keys(results, opts[:data]) headers = map_string_values(keys) data = Enum.map(results, &map_string_values(&1, keys)) table = [headers | data] Table.format(table, Enum.count(table), Enum.count(keys), opts) end defp map_string_values(keys), do: Enum.map(keys, &string_value(&1)) defp map_string_values(row, keys), do: Enum.map(keys, &string_value(row, &1)) defp string_value(%{name: name, key: _key}) do name end defp string_value(map, %{name: _name, key: key}) when is_function(key) do map |> key.() end defp string_value(map, %{name: _name, key: key}) do map |> Map.get(key) end defp fetch_keys([first | _rest], nil), do: fetch_keys(first) defp fetch_keys(_list, opts), do: process_headers(opts) defp process_headers(opts) do for opt <- opts do case opt do {name, key} -> %{name: name, key: key} key -> %{name: key, key: key} end end end defp fetch_keys(map) do map |> Map.keys() |> Enum.sort() |> process_headers() end end