From 8cabbadbfdbd7de6b9c120ad9c7954602b100806 Mon Sep 17 00:00:00 2001 From: Unnawut Leepaisalsuwanna Date: Fri, 30 Aug 2019 19:22:54 +0700 Subject: [PATCH] Replace TableRex dependency with a hard copy instead --- README.md | 2 + lib/table_rex.ex | 35 + lib/table_rex/LICENSE | 19 + lib/table_rex/README.md | 4 + lib/table_rex/cell.ex | 67 ++ lib/table_rex/column.ex | 11 + lib/table_rex/renderer.ex | 15 + lib/table_rex/renderer/text.ex | 443 +++++++ lib/table_rex/renderer/text/meta.ex | 41 + lib/table_rex/table.ex | 297 +++++ mix.exs | 3 +- test/table_rex/cell_test.exs | 43 + test/table_rex/renderer/text_test.exs | 1531 +++++++++++++++++++++++++ test/table_rex/table_rex_test.exs | 143 +++ test/table_rex/table_test.exs | 886 ++++++++++++++ 15 files changed, 3538 insertions(+), 2 deletions(-) create mode 100755 lib/table_rex.ex create mode 100644 lib/table_rex/LICENSE create mode 100644 lib/table_rex/README.md create mode 100755 lib/table_rex/cell.ex create mode 100755 lib/table_rex/column.ex create mode 100755 lib/table_rex/renderer.ex create mode 100755 lib/table_rex/renderer/text.ex create mode 100755 lib/table_rex/renderer/text/meta.ex create mode 100755 lib/table_rex/table.ex create mode 100755 test/table_rex/cell_test.exs create mode 100755 test/table_rex/renderer/text_test.exs create mode 100755 test/table_rex/table_rex_test.exs create mode 100755 test/table_rex/table_test.exs diff --git a/README.md b/README.md index 91556d7..08e3f8d 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,5 @@ iex> Licensir.Scanner.scan([]) Copyright (c) 2017-2019, Unnawut Leepaisalsuwanna. Licensir is released under the [MIT License](LICENSE). + +This project also contains a [partial copy](./tree/master/lib/table_rex) of [djm/table_rex](https://github.com/djm/table_rex). diff --git a/lib/table_rex.ex b/lib/table_rex.ex new file mode 100755 index 0000000..9b98178 --- /dev/null +++ b/lib/table_rex.ex @@ -0,0 +1,35 @@ +defmodule TableRex do + @moduledoc """ + TableRex generates configurable, text-based tables for display + """ + alias TableRex.Renderer + alias TableRex.Table + + @doc """ + A shortcut function to render with a one-liner. + Sacrifices all customisation for those that just want sane defaults. + Returns `{:ok, rendered_string}` on success and `{:error, reason}` on failure. + """ + @spec quick_render(list, list, String.t() | nil) :: Renderer.render_return() + def quick_render(rows, header \\ [], title \\ nil) when is_list(rows) and is_list(header) do + Table.new(rows, header, title) + |> Table.render() + end + + @doc """ + A shortcut function to render with a one-liner. + Sacrifices all customisation for those that just want sane defaults. + Returns the `rendered_string` on success and raises `RuntimeError` on failure. + """ + @spec quick_render!(list, list, String.t() | nil) :: String.t() | no_return + def quick_render!(rows, header \\ [], title \\ nil) when is_list(rows) and is_list(header) do + case quick_render(rows, header, title) do + {:ok, rendered} -> rendered + {:error, reason} -> raise TableRex.Error, message: reason + end + end +end + +defmodule TableRex.Error do + defexception [:message] +end diff --git a/lib/table_rex/LICENSE b/lib/table_rex/LICENSE new file mode 100644 index 0000000..aa717a7 --- /dev/null +++ b/lib/table_rex/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Darian Moody + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/table_rex/README.md b/lib/table_rex/README.md new file mode 100644 index 0000000..ea1a83b --- /dev/null +++ b/lib/table_rex/README.md @@ -0,0 +1,4 @@ +This directory contains a copy of [djm/table_rex](https://github.com/djm/table_rex). + +A hard copy is used instead of a dependency so that `mix archive.install ...`, +which does not recognize the archive's defined dependencies, is supported. diff --git a/lib/table_rex/cell.ex b/lib/table_rex/cell.ex new file mode 100755 index 0000000..59f2d7f --- /dev/null +++ b/lib/table_rex/cell.ex @@ -0,0 +1,67 @@ +defmodule TableRex.Cell do + @moduledoc """ + Defines a struct that represents a single table cell, and helper functions. + + A cell stores both the original data _and_ the string-rendered version, + this decision was taken as a tradeoff: this way uses more memory to store + the table structure but the renderers gain the ability to get direct access + to the string-coerced data rather than having to risk repeated coercion or + handle their own storage of the computer values. + + Fields: + + * `raw_value`: The un-coerced original value + + * `rendered_value`: The stringified value for rendering + + * `align`: + * `:left`: left align text in the cell. + * `:center`: center text in the cell. + * `:right`: right align text in the cell. + * `nil`: align text in cell according to column alignment. + + * `color`: the ANSI color of the cell. + + If creating a Cell manually: raw_value is the only required key to + enable that Cell to work well with the rest of TableRex. It should + be set to a piece of data that can be rendered to string. + """ + alias TableRex.Cell + + defstruct raw_value: nil, rendered_value: "", align: nil, color: nil + + @type t :: %__MODULE__{} + + @doc """ + Converts the passed value to be a normalised %Cell{} struct. + + If a non %Cell{} value is passed, this function returns a new + %Cell{} struct with: + + * the `rendered_value` key set to the stringified binary of the + value passed in. + * the `raw_value` key set to original data passed in. + * any other options passed are applied over the normal struct + defaults, which allows overriding alignment & color. + + If a %Cell{} is passed in with no `rendered_value` key, then the + `raw_value` key's value is rendered and saved against it, otherwise + the Cell is passed through untouched. This is so that advanced use + cases which require direct Cell creation and manipulation are not + hindered. + """ + @spec to_cell(Cell.t()) :: Cell.t() + def to_cell(%Cell{rendered_value: rendered_value} = cell) when rendered_value != "", do: cell + + def to_cell(%Cell{raw_value: raw_value} = cell) do + %Cell{cell | rendered_value: to_string(raw_value)} + end + + @spec to_cell(any, list) :: Cell.t() + def to_cell(value, opts \\ []) do + opts = Enum.into(opts, %{}) + + %Cell{rendered_value: to_string(value), raw_value: value} + |> Map.merge(opts) + end +end diff --git a/lib/table_rex/column.ex b/lib/table_rex/column.ex new file mode 100755 index 0000000..e97284c --- /dev/null +++ b/lib/table_rex/column.ex @@ -0,0 +1,11 @@ +defmodule TableRex.Column do + @moduledoc """ + Defines a struct that represents a column's metadata + + The align field can be one of :left, :center or :right. + """ + + defstruct align: :left, padding: 1, color: nil + + @type t :: %__MODULE__{} +end diff --git a/lib/table_rex/renderer.ex b/lib/table_rex/renderer.ex new file mode 100755 index 0000000..38fcb82 --- /dev/null +++ b/lib/table_rex/renderer.ex @@ -0,0 +1,15 @@ +defmodule TableRex.Renderer do + @moduledoc """ + An Elixir behaviour that defines the API Renderers should conform to, allowing + for display output in a variety of formats. + """ + + @typedoc "Return value of the render function." + @type render_return :: {:ok, String.t()} | {:error, String.t()} + + @doc "Returns a Map of the options and their default values required by the renderer." + @callback default_options() :: map + + @doc "Renders a passed %TableRex.Table{} struct into a string." + @callback render(table :: %TableRex.Table{}, opts :: list) :: render_return +end diff --git a/lib/table_rex/renderer/text.ex b/lib/table_rex/renderer/text.ex new file mode 100755 index 0000000..f8427b6 --- /dev/null +++ b/lib/table_rex/renderer/text.ex @@ -0,0 +1,443 @@ +defmodule TableRex.Renderer.Text do + @moduledoc """ + Renderer module which handles outputting ASCII-style tables for display. + """ + alias TableRex.Cell + alias TableRex.Table + alias TableRex.Renderer.Text.Meta + + @behaviour TableRex.Renderer + + # horizontal_styles: [:all, :header, :frame:, :off] + # vertical_styles: [:all, :frame, :off] + + # Which horizontal/vertical styles render a specific separator. + @render_horizontal_frame_styles [:all, :frame, :header] + @render_vertical_frame_styles [:all, :frame] + @render_column_separators_styles [:all] + @render_row_separators_styles [:all] + + @doc """ + Provides a level of sane defaults for the Text rendering module. + """ + def default_options do + %{ + horizontal_style: :header, + vertical_style: :all, + horizontal_symbol: "-", + vertical_symbol: "|", + intersection_symbol: "+", + top_frame_symbol: "-", + title_separator_symbol: "-", + header_separator_symbol: "-", + bottom_frame_symbol: "-" + } + end + + @doc """ + Implementation of the TableRex.Renderer behaviour. + + Available styling options. + + `horizontal_styles` controls horizontal separators and can be one of: + + * `:all`: display separators between and around every row. + * `:header`: display outer and header horizontal separators only. + * `:frame`: display outer horizontal separators only. + * `:off`: display no horizontal separators. + + `vertical_styles` controls vertical separators and can be one of: + + * `:all`: display between and around every column. + * `:frame`: display outer vertical separators only. + * `:off`: display no vertical separators. + """ + def render(table = %Table{}, opts) do + {col_widths, row_heights} = max_dimensions(table) + + # Calculations that would otherwise be carried out multiple times are done once and their + # results are stored in the %Meta{} struct which is then passed through the pipeline. + render_horizontal_frame? = opts[:horizontal_style] in @render_horizontal_frame_styles + render_vertical_frame? = opts[:vertical_style] in @render_vertical_frame_styles + render_column_separators? = opts[:vertical_style] in @render_column_separators_styles + render_row_separators? = opts[:horizontal_style] in @render_row_separators_styles + table_width = table_width(col_widths, vertical_frame?: render_vertical_frame?) + intersections = intersections(table_width, col_widths, vertical_style: opts[:vertical_style]) + + meta = %Meta{ + col_widths: col_widths, + row_heights: row_heights, + table_width: table_width, + intersections: intersections, + render_horizontal_frame?: render_horizontal_frame?, + render_vertical_frame?: render_vertical_frame?, + render_column_separators?: render_column_separators?, + render_row_separators?: render_row_separators? + } + + rendered = + {table, meta, opts, []} + |> render_top_frame + |> render_title + |> render_title_separator + |> render_header + |> render_header_separator + |> render_rows + |> render_bottom_frame + |> render_to_string + + {:ok, rendered} + end + + defp render_top_frame({table, %Meta{render_horizontal_frame?: false} = meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_top_frame({%Table{title: title} = table, meta, opts, rendered}) + when is_binary(title) do + intersections = if meta.render_vertical_frame?, do: [0, meta.table_width - 1], else: [] + + line = + render_line( + meta.table_width, + intersections, + opts[:top_frame_symbol], + opts[:intersection_symbol] + ) + + {table, meta, opts, [line | rendered]} + end + + defp render_top_frame({table, meta, opts, rendered}) do + line = + render_line( + meta.table_width, + meta.intersections, + opts[:top_frame_symbol], + opts[:intersection_symbol] + ) + + {table, meta, opts, [line | rendered]} + end + + defp render_title({%Table{title: nil} = table, meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_title({%Table{title: title} = table, meta, opts, rendered}) do + inner_width = Meta.inner_width(meta) + line = do_render_cell(title, inner_width) + + line = + if meta.render_vertical_frame? do + line |> frame_with(opts[:vertical_symbol]) + else + line + end + + {table, meta, opts, [line | rendered]} + end + + defp render_title_separator({%Table{title: nil} = table, meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_title_separator( + {table, meta, %{horizontal_style: horizontal_style} = opts, rendered} + ) + when horizontal_style in [:all, :header] do + line = + render_line( + meta.table_width, + meta.intersections, + opts[:title_separator_symbol], + opts[:intersection_symbol] + ) + + {table, meta, opts, [line | rendered]} + end + + defp render_title_separator({table, %Meta{render_vertical_frame?: true} = meta, opts, rendered}) do + line = render_line(meta.table_width, [0, meta.table_width - 1], " ", opts[:vertical_symbol]) + {table, meta, opts, [line | rendered]} + end + + defp render_title_separator( + {table, %Meta{render_vertical_frame?: false} = meta, opts, rendered} + ) do + {table, meta, opts, ["" | rendered]} + end + + defp render_header({%Table{header_row: []} = table, meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_header({%Table{header_row: header_row} = table, meta, opts, rendered}) do + separator = if meta.render_column_separators?, do: opts[:vertical_symbol], else: " " + line = render_cell_row(table, meta, header_row, separator) + + line = + if meta.render_vertical_frame? do + line |> frame_with(opts[:vertical_symbol]) + else + line + end + + {table, meta, opts, [line | rendered]} + end + + defp render_header_separator({%Table{header_row: []} = table, meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_header_separator( + {table, meta, %{horizontal_style: horizontal_style} = opts, rendered} + ) + when horizontal_style in [:all, :header] do + line = + render_line( + meta.table_width, + meta.intersections, + opts[:header_separator_symbol], + opts[:intersection_symbol] + ) + + {table, meta, opts, [line | rendered]} + end + + defp render_header_separator( + {table, %Meta{render_vertical_frame?: true} = meta, opts, rendered} + ) do + line = render_line(meta.table_width, [0, meta.table_width - 1], " ", opts[:vertical_symbol]) + {table, meta, opts, [line | rendered]} + end + + defp render_header_separator( + {table, %Meta{render_vertical_frame?: false} = meta, opts, rendered} + ) do + {table, meta, opts, ["" | rendered]} + end + + defp render_rows({%Table{rows: rows} = table, meta, opts, rendered}) do + separator = if meta.render_column_separators?, do: opts[:vertical_symbol], else: " " + lines = Enum.map(rows, &render_cell_row(table, meta, &1, separator)) + + lines = + if meta.render_vertical_frame? do + Enum.map(lines, &frame_with(&1, opts[:vertical_symbol])) + else + lines + end + + lines = + if meta.render_row_separators? do + row_separator = + render_line( + meta.table_width, + meta.intersections, + opts[:horizontal_symbol], + opts[:intersection_symbol] + ) + + Enum.intersperse(lines, row_separator) + else + lines + end + + rendered = lines ++ rendered + {table, meta, opts, rendered} + end + + defp render_bottom_frame({table, %Meta{render_horizontal_frame?: false} = meta, opts, rendered}) do + {table, meta, opts, rendered} + end + + defp render_bottom_frame({table, meta, opts, rendered}) do + line = + render_line( + meta.table_width, + meta.intersections, + opts[:bottom_frame_symbol], + opts[:intersection_symbol] + ) + + {table, meta, opts, [line | rendered]} + end + + defp render_line(table_width, intersections, separator_symbol, intersection_symbol) do + for n <- 0..(table_width - 1) do + if n in intersections, do: intersection_symbol, else: separator_symbol + end + |> Enum.join() + end + + defp render_cell_row(%Table{} = table, %Meta{} = meta, row, separator) do + row + |> Enum.with_index() + |> Enum.map(&render_cell(table, meta, &1)) + |> Enum.intersperse(separator) + |> Enum.join() + end + + defp render_cell(%Table{} = table, %Meta{} = meta, {%Cell{} = cell, col_index}) do + col_width = Meta.col_width(meta, col_index) + col_padding = Table.get_column_meta(table, col_index, :padding) + cell_align = Map.get(cell, :align) || Table.get_column_meta(table, col_index, :align) + cell_color = Map.get(cell, :color) || Table.get_column_meta(table, col_index, :color) + + do_render_cell(cell.rendered_value, col_width, col_padding, align: cell_align) + |> format_with_color(cell.rendered_value, cell_color) + end + + defp do_render_cell(value, inner_width) do + do_render_cell(value, inner_width, 0, align: :center) + end + + defp do_render_cell(value, inner_width, _padding, align: :center) do + value_len = String.length(strip_ansi_color_codes(value)) + post_value = ((inner_width - value_len) / 2) |> round + pre_value = inner_width - (post_value + value_len) + String.duplicate(" ", pre_value) <> value <> String.duplicate(" ", post_value) + end + + defp do_render_cell(value, inner_width, padding, align: align) do + value_len = String.length(strip_ansi_color_codes(value)) + alt_side_padding = inner_width - value_len - padding + + {pre_value, post_value} = + case align do + :left -> + {padding, alt_side_padding} + + :right -> + {alt_side_padding, padding} + end + + String.duplicate(" ", pre_value) <> value <> String.duplicate(" ", post_value) + end + + defp intersections(_table_width, _col_widths, vertical_style: :off), do: [] + + defp intersections(table_width, _col_widths, vertical_style: :frame) do + [0, table_width - 1] + |> Enum.into(MapSet.new()) + end + + defp intersections(table_width, col_widths, vertical_style: :all) do + col_widths = ordered_col_widths(col_widths) + + inner_intersections = + Enum.reduce(col_widths, [0], fn x, [acc_h | _] = acc -> + [acc_h + x + 1 | acc] + end) + + ([0, table_width - 1] ++ inner_intersections) + |> Enum.into(MapSet.new()) + end + + defp max_dimensions(%Table{} = table) do + {col_widths, row_heights} = + [table.header_row | table.rows] + |> Enum.with_index() + |> Enum.reduce({%{}, %{}}, &reduce_row_maximums(table, &1, &2)) + + num_columns = Map.size(col_widths) + + # Infer padding on left and right of title + title_padding = + [0, num_columns - 1] + |> Enum.map(&Table.get_column_meta(table, &1, :padding)) + |> Enum.sum() + + # Compare table body width with title width + col_separators_widths = num_columns - 1 + body_width = (col_widths |> Map.values() |> Enum.sum()) + col_separators_widths + title_width = if(is_nil(table.title), do: 0, else: String.length(table.title)) + title_padding + + # Add extra padding equally to all columns if required to match body and title width. + revised_col_widths = + if body_width >= title_width do + col_widths + else + extra_padding = ((title_width - body_width) / num_columns) |> Float.ceil() |> round + Enum.into(col_widths, %{}, fn {k, v} -> {k, v + extra_padding} end) + end + + {revised_col_widths, row_heights} + end + + defp reduce_row_maximums(%Table{} = table, {row, row_index}, {col_widths, row_heights}) do + row + |> Enum.with_index() + |> Enum.reduce({col_widths, row_heights}, &reduce_cell_maximums(table, &1, &2, row_index)) + end + + defp reduce_cell_maximums( + %Table{} = table, + {cell, col_index}, + {col_widths, row_heights}, + row_index + ) do + padding = Table.get_column_meta(table, col_index, :padding) + {width, height} = content_dimensions(cell.rendered_value, padding) + col_widths = Map.update(col_widths, col_index, width, &Enum.max([&1, width])) + row_heights = Map.update(row_heights, row_index, height, &Enum.max([&1, height])) + {col_widths, row_heights} + end + + defp content_dimensions(value, padding) when is_binary(value) and is_number(padding) do + lines = + value + |> strip_ansi_color_codes() + |> String.split("\n") + + height = Enum.count(lines) + width = Enum.max(lines) |> String.length() + {width + padding * 2, height} + end + + defp table_width(%{} = col_widths, vertical_frame?: vertical_frame?) do + width = + col_widths + |> Map.values() + |> Enum.intersperse(1) + |> Enum.sum() + + if vertical_frame?, do: width + 2, else: width + end + + defp ordered_col_widths(%{} = col_widths) do + col_widths + |> Enum.into([]) + |> Enum.sort() + |> Enum.map(&elem(&1, 1)) + end + + defp frame_with(string, frame) do + frame <> string <> frame + end + + defp render_to_string({_, _, _, rendered_lines}) when is_list(rendered_lines) do + rendered_lines + |> Enum.map(&String.trim_trailing/1) + |> Enum.reverse() + |> Enum.join("\n") + |> Kernel.<>("\n") + end + + defp format_with_color(text, _, nil), do: text + + defp format_with_color(text, value, color) when is_function(color) do + [color.(text, value) | IO.ANSI.reset()] + |> IO.ANSI.format_fragment(true) + end + + defp format_with_color(text, _, color) do + [[color | text] | IO.ANSI.reset()] + |> IO.ANSI.format_fragment(true) + end + + defp strip_ansi_color_codes(text) do + Regex.replace(~r|\e\[\d+m|u, text, "") + end +end diff --git a/lib/table_rex/renderer/text/meta.ex b/lib/table_rex/renderer/text/meta.ex new file mode 100755 index 0000000..4fe9299 --- /dev/null +++ b/lib/table_rex/renderer/text/meta.ex @@ -0,0 +1,41 @@ +defmodule TableRex.Renderer.Text.Meta do + @moduledoc """ + The data structure for the `TableRex.Renderer.Text` rendering module, it holds results + of style & dimension calculations to be passed down the render pipeline. + """ + alias TableRex.Renderer.Text.Meta + + defstruct col_widths: %{}, + row_heights: %{}, + table_width: 0, + intersections: [], + render_horizontal_frame?: false, + render_vertical_frame?: false, + render_column_separators?: false, + render_row_separators?: false + + @doc """ + Retreives the "inner width" of the table, which is the full width minus any frame. + """ + def inner_width(%Meta{table_width: table_width, render_vertical_frame?: true}) do + table_width - 2 + end + + def inner_width(%Meta{table_width: table_width, render_vertical_frame?: false}) do + table_width + end + + @doc """ + Retreives the column width at the given column index. + """ + def col_width(meta, col_index) do + Map.get(meta.col_widths, col_index) + end + + @doc """ + Retreives the row width at the given row index. + """ + def row_height(meta, row_index) do + Map.get(meta.row_heights, row_index) + end +end diff --git a/lib/table_rex/table.ex b/lib/table_rex/table.ex new file mode 100755 index 0000000..bbf61f9 --- /dev/null +++ b/lib/table_rex/table.ex @@ -0,0 +1,297 @@ +defmodule TableRex.Table do + @moduledoc """ + A set of functions for working with tables. + + The `Table` is represented internally as a struct though the + fields are private and must not be accessed directly. Instead, + use the functions in this module. + """ + alias TableRex.Cell + alias TableRex.Column + alias TableRex.Renderer + alias TableRex.Table + + defstruct title: nil, header_row: [], rows: [], columns: %{}, default_column: %Column{} + + @type t :: %__MODULE__{} + + @default_renderer Renderer.Text + + @doc """ + Creates a new blank table. + + The table created will not be able to be rendered until it has some row data. + + ## Examples + + iex> Table.new + %TableRex.Table{} + + """ + @spec new() :: Table.t() + def new, do: %Table{} + + @doc """ + Creates a new table with an initial set of rows and an optional header and title. + """ + @spec new(list, list, String.t()) :: Table.t() + def new(rows, header_row \\ [], title \\ nil) when is_list(rows) and is_list(header_row) do + new() + |> put_title(title) + |> put_header(header_row) + |> add_rows(rows) + end + + # ------------ + # Mutation API + # ------------ + + @doc """ + Sets a string as the optional table title. + Set to `nil` or `""` to remove an already set title from renders. + """ + @spec put_title(Table.t(), String.t() | nil) :: Table.t() + def put_title(%Table{} = table, ""), do: put_title(table, nil) + + def put_title(%Table{} = table, title) when is_binary(title) or is_nil(title) do + %Table{table | title: title} + end + + @doc """ + Sets a list as the optional header row. + Set to `nil` or `[]` to remove an already set header from renders. + """ + @spec put_header(Table.t(), list | nil) :: Table.t() + def put_header(%Table{} = table, nil), do: put_header(table, []) + + def put_header(%Table{} = table, header_row) when is_list(header_row) do + new_header_row = Enum.map(header_row, &Cell.to_cell(&1)) + %Table{table | header_row: new_header_row} + end + + @doc """ + Sets column level information such as padding and alignment. + """ + @spec put_column_meta(Table.t(), integer | atom | Enum.t(), Keyword.t()) :: Table.t() + def put_column_meta(%Table{} = table, col_index, col_meta) + when is_integer(col_index) and is_list(col_meta) do + col_meta = col_meta |> Enum.into(%{}) + col = get_column(table, col_index) |> Map.merge(col_meta) + new_columns = Map.put(table.columns, col_index, col) + %Table{table | columns: new_columns} + end + + def put_column_meta(%Table{} = table, :all, col_meta) when is_list(col_meta) do + col_meta = col_meta |> Enum.into(%{}) + # First update default column, then any already set columns. + table = put_in(table.default_column, Map.merge(table.default_column, col_meta)) + + new_columns = + Enum.reduce(table.columns, %{}, fn {col_index, col}, acc -> + new_col = Map.merge(col, col_meta) + Map.put(acc, col_index, new_col) + end) + + %Table{table | columns: new_columns} + end + + def put_column_meta(%Table{} = table, col_indexes, col_meta) when is_list(col_meta) do + Enum.reduce(col_indexes, table, &put_column_meta(&2, &1, col_meta)) + end + + @doc """ + Sets cell level information such as alignment. + """ + @spec put_cell_meta(Table.t(), integer, integer, Keyword.t()) :: Table.t() + def put_cell_meta(%Table{} = table, col_index, row_index, cell_meta) + when is_integer(col_index) and is_integer(row_index) and is_list(cell_meta) do + cell_meta = cell_meta |> Enum.into(%{}) + inverse_row_index = -(row_index + 1) + + rows = + List.update_at(table.rows, inverse_row_index, fn row -> + List.update_at(row, col_index, &Map.merge(&1, cell_meta)) + end) + + %Table{table | rows: rows} + end + + @doc """ + Sets cell level information for the header cells. + """ + @spec put_header_meta(Table.t(), integer | Enum.t(), Keyword.t()) :: Table.t() + def put_header_meta(%Table{} = table, col_index, cell_meta) + when is_integer(col_index) and is_list(cell_meta) do + cell_meta = cell_meta |> Enum.into(%{}) + header_row = List.update_at(table.header_row, col_index, &Map.merge(&1, cell_meta)) + %Table{table | header_row: header_row} + end + + def put_header_meta(%Table{} = table, col_indexes, cell_meta) when is_list(cell_meta) do + Enum.reduce(col_indexes, table, &put_header_meta(&2, &1, cell_meta)) + end + + @doc """ + Adds a single row to the table. + """ + @spec add_row(Table.t(), list) :: Table.t() + def add_row(%Table{} = table, row) when is_list(row) do + new_row = Enum.map(row, &Cell.to_cell(&1)) + %Table{table | rows: [new_row | table.rows]} + end + + @doc """ + Adds multiple rows to the table. + """ + @spec add_rows(Table.t(), list) :: Table.t() + def add_rows(%Table{} = table, rows) when is_list(rows) do + rows = + rows + |> Enum.reverse() + |> Enum.map(fn row -> + Enum.map(row, &Cell.to_cell(&1)) + end) + + %Table{table | rows: rows ++ table.rows} + end + + @doc """ + Removes column meta for all columns, effectively resetting + column meta back to the default options across the board. + """ + @spec clear_all_column_meta(Table.t()) :: Table.t() + def clear_all_column_meta(%Table{} = table) do + %Table{table | columns: %{}} + end + + @doc """ + Removes all row data from the table, keeping everything else. + """ + @spec clear_rows(Table.t()) :: Table.t() + def clear_rows(%Table{} = table) do + %Table{table | rows: []} + end + + @doc """ + Sorts the table rows by using the values in a specified column. + + This is very much a simple sorting function and relies on Elixir's + built-in comparison operators & types to cover the basic cases. + + As each cell retains the original value it was created with, we + use that value to sort on as this allows us to sort on many + built-in types in the most obvious fashions. + + Remember that rows are stored internally in reverse order that + they will be output in, to allow for fast insertion. + + Parameters: + + `column_index`: the 0-indexed column number to sort by + `order`: supply :desc or :asc for sort direction. + + Returns a new Table, with sorted rows. + """ + @spec sort(Table.t(), integer, atom) :: Table.t() + def sort(table, column_index, order \\ :desc) + + def sort(%Table{rows: [first_row | _]}, column_index, _order) + when length(first_row) <= column_index do + raise TableRex.Error, + message: + "You cannot sort by column #{column_index}, as the table only has #{length(first_row)} column(s)" + end + + def sort(table = %Table{rows: rows}, column_index, order) do + %Table{table | rows: Enum.sort(rows, build_sort_function(column_index, order))} + end + + defp build_sort_function(column_index, order) when order in [:desc, :asc] do + fn previous, next -> + %{raw_value: prev_value} = Enum.at(previous, column_index) + %{raw_value: next_value} = Enum.at(next, column_index) + + if order == :desc do + next_value > prev_value + else + next_value < prev_value + end + end + end + + defp build_sort_function(_column_index, order) do + raise TableRex.Error, + message: "Invalid sort order parameter: #{order}. Must be an atom, either :desc or :asc." + end + + # ------------- + # Retrieval API + # ------------- + + defp get_column(%Table{} = table, col_index) when is_integer(col_index) do + Map.get(table.columns, col_index, table.default_column) + end + + @doc """ + Retreives the value of a column meta option at the specified col_index. + If no value has been set, default values are returned. + """ + @spec get_column_meta(Table.t(), integer, atom) :: any + def get_column_meta(%Table{} = table, col_index, key) + when is_integer(col_index) and is_atom(key) do + get_column(table, col_index) + |> Map.fetch!(key) + end + + @doc """ + Returns a boolean detailing if the passed table has any row data set. + """ + @spec has_rows?(Table.t()) :: boolean + def has_rows?(%Table{rows: []}), do: false + def has_rows?(%Table{rows: rows}) when is_list(rows), do: true + + @doc """ + Returns a boolean detailing if the passed table has a header row set. + """ + @spec has_header?(Table.t()) :: boolean + def has_header?(%Table{header_row: []}), do: false + def has_header?(%Table{header_row: header_row}) when is_list(header_row), do: true + + # ------------- + # Rendering API + # ------------- + + @doc """ + Renders the current table state to string, ready for display via `IO.puts/2` or other means. + + At least one row must have been added before rendering. + + Returns `{:ok, rendered_string}` on success and `{:error, reason}` on failure. + """ + @spec render(Table.t(), list) :: Renderer.render_return() + def render(%Table{} = table, opts \\ []) when is_list(opts) do + {renderer, opts} = Keyword.pop(opts, :renderer, @default_renderer) + opts = opts |> Enum.into(renderer.default_options) + + if Table.has_rows?(table) do + renderer.render(table, opts) + else + {:error, "Table must have at least one row before being rendered"} + end + end + + @doc """ + Renders the current table state to string, ready for display via `IO.puts/2` or other means. + + At least one row must have been added before rendering. + + Returns the rendered string on success, or raises `TableRex.Error` on failure. + """ + @spec render!(Table.t(), list) :: String.t() | no_return + def render!(%Table{} = table, opts \\ []) when is_list(opts) do + case render(table, opts) do + {:ok, rendered_string} -> rendered_string + {:error, reason} -> raise TableRex.Error, message: reason + end + end +end diff --git a/mix.exs b/mix.exs index f0a0f3e..ae1c950 100644 --- a/mix.exs +++ b/mix.exs @@ -39,8 +39,7 @@ defmodule Licensir.Mixfile do defp deps do [ {:ex_doc, ">= 0.19.0", only: :dev}, - {:excoveralls, "~> 0.8", only: :test}, - {:table_rex, "~> 2.0.0"} + {:excoveralls, "~> 0.8", only: :test} ] end end diff --git a/test/table_rex/cell_test.exs b/test/table_rex/cell_test.exs new file mode 100755 index 0000000..e579853 --- /dev/null +++ b/test/table_rex/cell_test.exs @@ -0,0 +1,43 @@ +defmodule TableRex.CellTest do + use ExUnit.Case, async: true + alias TableRex.Cell + + doctest Cell + + test "default struct" do + assert %Cell{} == %Cell{ + raw_value: nil, + rendered_value: "", + align: nil, + color: nil + } + end + + test "to_cell with float value" do + assert Cell.to_cell(1.345) == %Cell{raw_value: 1.345, rendered_value: "1.345"} + end + + test "to_cell with integer value" do + assert Cell.to_cell(13) == %Cell{raw_value: 13, rendered_value: "13"} + end + + test "to_cell with binary value" do + assert Cell.to_cell("Thirteen") == %Cell{raw_value: "Thirteen", rendered_value: "Thirteen"} + end + + test "to_cell with extra options" do + assert Cell.to_cell("Thirteen", align: :left, color: :red) == + %Cell{raw_value: "Thirteen", rendered_value: "Thirteen", align: :left, color: :red} + end + + test "to_cell with %Cell{} value with no rendered_value set" do + assert Cell.to_cell(%Cell{raw_value: 1.345}) == %Cell{ + raw_value: 1.345, + rendered_value: "1.345" + } + end + + test "to_cell with %Cell{} value with set rendered_value" do + assert Cell.to_cell(%Cell{rendered_value: "17"}) == %Cell{rendered_value: "17"} + end +end diff --git a/test/table_rex/renderer/text_test.exs b/test/table_rex/renderer/text_test.exs new file mode 100755 index 0000000..35a1eeb --- /dev/null +++ b/test/table_rex/renderer/text_test.exs @@ -0,0 +1,1531 @@ +defmodule TableRex.Renderer.TextTest do + use ExUnit.Case, async: true + alias TableRex.Table + + setup do + title = "Renegade Hardware Releases" + header = ["Artist", "Track", "Year"] + + rows = [ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ] + + table = Table.new(rows, header, title) + {:ok, table: table} + end + + test "default render", %{table: table} do + {:ok, rendered} = Table.render(table) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render without title", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("") + |> Table.render() + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render without title & header", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("") + |> Table.put_header(nil) + |> Table.render() + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with title but no header", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header(nil) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with alphabetic ascending sort by artist name", %{table: table} do + {:ok, rendered} = + table + |> Table.sort(0, :asc) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + | Konflict | Cyanide | 1999 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with alphabetic descending sort by track name", %{table: table} do + {:ok, rendered} = + table + |> Table.sort(1, :desc) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + | Keaton & Hive | The Plague | 2003 | + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + """ + end + + test "default render with numeric ascending sort by year", %{table: table} do + {:ok, rendered} = + table + |> Table.sort(2, :asc) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with numeric descending sort by year", %{table: table} do + {:ok, rendered} = + table + |> Table.sort(2, :desc) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + | Keaton & Hive | The Plague | 2003 | + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + """ + end + + test "render with vertical style: frame", %{table: table} do + {:ok, rendered} = Table.render(table, vertical_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------------------------------------+ + | Artist Track Year | + +----------------------------------------------+ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + +----------------------------------------------+ + """ + end + + test "render with no title & vertical style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("") + |> Table.render(vertical_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Artist Track Year | + +----------------------------------------------+ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + +----------------------------------------------+ + """ + end + + test "render with no title or header & vertical style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("") + |> Table.put_header([]) + |> Table.render(vertical_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + +----------------------------------------------+ + """ + end + + test "render with title but no header & vertical style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(vertical_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------------------------------------+ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + +----------------------------------------------+ + """ + end + + test "render with vertical style: off", %{table: table} do + {:ok, rendered} = Table.render(table, vertical_style: :off) + + assert rendered == """ + ---------------------------------------------- + Renegade Hardware Releases + ---------------------------------------------- + Artist Track Year + ---------------------------------------------- + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + ---------------------------------------------- + """ + end + + test "render with no title & vertical style: off", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(vertical_style: :off) + + assert rendered == """ + ---------------------------------------------- + Artist Track Year + ---------------------------------------------- + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + ---------------------------------------------- + """ + end + + test "render with no title or header & vertical style: off", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(vertical_style: :off) + + assert rendered == """ + ---------------------------------------------- + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + ---------------------------------------------- + """ + end + + test "render with title but no header & vertical style: off", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(vertical_style: :off) + + assert rendered == """ + ---------------------------------------------- + Renegade Hardware Releases + ---------------------------------------------- + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + ---------------------------------------------- + """ + end + + test "render with horizontal style: off, vertical style: frame", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :off, vertical_style: :frame) + + assert rendered == """ + | Renegade Hardware Releases | + | | + | Artist Track Year | + | | + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + """ + end + + test "render with no title & horizontal style: off, vertical style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :off, vertical_style: :frame) + + assert rendered == """ + | Artist Track Year | + | | + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + """ + end + + test "render with no title or header & horizontal style: off, vertical style: frame", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :frame) + + assert rendered == """ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + """ + end + + test "render with title but not header & horizontal style: off, vertical style: frame", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :frame) + + assert rendered == """ + | Renegade Hardware Releases | + | | + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + """ + end + + test "render with horizontal style: off, vertical style: all", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :off, vertical_style: :all) + + assert rendered == """ + | Renegade Hardware Releases | + | | + | Artist | Track | Year | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + """ + end + + test "render with no title & horizontal style: off, vertical style: all", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :off, vertical_style: :all) + + assert rendered == """ + | Artist | Track | Year | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + """ + end + + test "render with no title or header & horizontal style: off, vertical style: all", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :all) + + assert rendered == """ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + """ + end + + test "render with title but no header & horizontal style: off, vertical style: all", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :all) + + assert rendered == """ + | Renegade Hardware Releases | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + """ + end + + test "render with horizontal style: off, vertical style: off", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Renegade Hardware Releases + + Artist Track Year + + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "render with no title & horizontal style: off, vertical style: off", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Artist Track Year + + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "render with no title or header & horizontal style: off, vertical style: off", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "render with title but no header & horizontal style: off, vertical style: off", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Renegade Hardware Releases + + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "render with horizontal style: frame", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + | | + | Artist | Track | Year | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title & horizontal style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :frame) + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with not title or header & horizontal style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :frame) + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with title but no header & horizontal style: frame", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :frame) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + | | + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with horizontal style: all", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :all) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title & horizontal style: all", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :all) + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title or header & horizontal style: all", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :all) + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with title but not header & horizontal style: all", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :all) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with horizontal style: all, header_separator_symbol: =", %{table: table} do + {:ok, rendered} = Table.render(table, horizontal_style: :all, header_separator_symbol: "=") + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title & horizontal style: all, header_separator_symbol: =", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render(horizontal_style: :all, header_separator_symbol: "=") + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title or header & horizontal style: all, top_frame_symbol: =", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :all, top_frame_symbol: "=") + + assert rendered == """ + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title or header & horizontal style: all, top_frame_symbol: =, bottom_frame_symbol: =", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render(horizontal_style: :all, top_frame_symbol: "=", bottom_frame_symbol: "=") + + assert rendered == """ + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +================+======================+======+ + """ + end + + test "render with title but no header & horizontal style: all, header_separator_symbol: =", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :all, header_separator_symbol: "=") + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with title but no header & horizontal style: all, title_separator_symbol: =", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render(horizontal_style: :all, title_separator_symbol: "=") + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with horizontal style: all, title_separator_symbol & header_horizontal_symbol: =", + %{table: table} do + {:ok, rendered} = + Table.render( + table, + horizontal_style: :all, + title_separator_symbol: "~", + header_separator_symbol: "=" + ) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~+~~~~~~+ + | Artist | Track | Year | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title & horizontal style: all, title_separator_symbol & header_horizontal_symbol: =", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.render( + horizontal_style: :all, + title_separator_symbol: "=", + header_separator_symbol: "=" + ) + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with no title or header & horizontal style: all, title_separator_symbol & header_horizontal_symbol: =", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_title(nil) + |> Table.put_header([]) + |> Table.render( + horizontal_style: :all, + title_separator_symbol: "=", + header_separator_symbol: "=" + ) + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "render with title but no header & horizontal style: all, title_separator_symbol & header_horizontal_symbol: =", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_header([]) + |> Table.render( + horizontal_style: :all, + title_separator_symbol: "=", + header_separator_symbol: "=" + ) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +================+======================+======+ + | Konflict | Cyanide | 1999 | + +----------------+----------------------+------+ + | Keaton & Hive | The Plague | 2003 | + +----------------+----------------------+------+ + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with alignment", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(0, align: :center) + |> Table.put_column_meta(1, align: :right) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + + {:ok, rendered} = + table + |> Table.clear_all_column_meta() + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with added padding", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, padding: 3) + |> Table.render() + + assert rendered == """ + +----------------------------------------------------------+ + | Renegade Hardware Releases | + +--------------------+--------------------------+----------+ + | Artist | Track | Year | + +--------------------+--------------------------+----------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +--------------------+--------------------------+----------+ + """ + end + + test "default render with added padding & alignment", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(0, padding: 3, align: :center) + |> Table.put_column_meta(1..2, padding: 3, align: :right) + |> Table.render() + + assert rendered == """ + +----------------------------------------------------------+ + | Renegade Hardware Releases | + +--------------------+--------------------------+----------+ + | Artist | Track | Year | + +--------------------+--------------------------+----------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +--------------------+--------------------------+----------+ + """ + end + + test "default render with added color using a named ANSI sequence", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(1, color: :red) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist |\e[31m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict |\e[31m Cyanide \e[0m| 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle |\e[31m Welcome To Shanktown \e[0m| 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with added color using an embedded ANSI sequence", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(1, color: "\e[31m") + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist |\e[31m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict |\e[31m Cyanide \e[0m| 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle |\e[31m Welcome To Shanktown \e[0m| 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with added color using multiple ANSI sequences", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(1, color: ["\e[48;5;30m", :white]) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist |\e[48;5;30m\e[37m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict |\e[48;5;30m\e[37m Cyanide \e[0m| 1999 | + | Keaton & Hive |\e[48;5;30m\e[37m The Plague \e[0m| 2003 | + | Vicious Circle |\e[48;5;30m\e[37m Welcome To Shanktown \e[0m| 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with added color using a function", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta( + 2, + color: fn t, v -> if v in ["1999", "2007"], do: [:blue, t], else: t end + ) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year \e[0m| + +----------------+----------------------+------+ + | Konflict | Cyanide |\e[34m 1999 \e[0m| + | Keaton & Hive | The Plague | 2003 \e[0m| + | Vicious Circle | Welcome To Shanktown |\e[34m 2007 \e[0m| + +----------------+----------------------+------+ + """ + end + + test "default render with increases padding across all rows (using list)", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta([0, 1, 2], align: :left, padding: 3) + |> Table.render() + + assert rendered == """ + +----------------------------------------------------------+ + | Renegade Hardware Releases | + +--------------------+--------------------------+----------+ + | Artist | Track | Year | + +--------------------+--------------------------+----------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +--------------------+--------------------------+----------+ + """ + end + + test "minimal render (zero padding)", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, padding: 0) + |> Table.render(horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Renegade Hardware Releases + + Artist Track Year + + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "default render with set column meta across all columns", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, align: :center) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with set column meta color across all columns", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, color: :red) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + |\e[31m Artist \e[0m|\e[31m Track \e[0m|\e[31m Year \e[0m| + +----------------+----------------------+------+ + |\e[31m Konflict \e[0m|\e[31m Cyanide \e[0m|\e[31m 1999 \e[0m| + |\e[31m Keaton & Hive \e[0m|\e[31m The Plague \e[0m|\e[31m 2003 \e[0m| + |\e[31m Vicious Circle \e[0m|\e[31m Welcome To Shanktown \e[0m|\e[31m 2007 \e[0m| + +----------------+----------------------+------+ + """ + end + + test "default render with set column meta across all columns and specific column override", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, align: :center) + |> Table.put_column_meta(1, align: :right) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with cell level alignment", %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(:all, align: :right) + |> Table.put_cell_meta(0, 0, align: :center) + |> Table.put_cell_meta(1, 1, align: :left) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with cell level color using a named ANSI sequence", %{table: table} do + {:ok, rendered} = + table + |> Table.put_cell_meta(1, 1, color: :red) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with cell level color using an embedded ANSI sequence", %{table: table} do + {:ok, rendered} = + table + |> Table.put_cell_meta(1, 1, color: "\e[31m") + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with cell level color using multiple ANSI sequences", %{table: table} do + {:ok, rendered} = + table + |> Table.put_cell_meta(1, 1, color: ["\e[48;5;30m", :white]) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive |\e[48;5;30m\e[37m The Plague \e[0m| 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with cell level color using a function", %{table: table} do + {:ok, rendered} = + table + |> Table.put_cell_meta(1, 1, color: fn t, _ -> ["\e[48;5;30m", :white, t] end) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive |\e[48;5;30m\e[37m The Plague \e[0m| 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with header cell alignment", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header_meta(0, align: :center) + |> Table.put_header_meta(1, align: :right) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with header cell color", %{table: table} do + {:ok, rendered} = + table + |> Table.put_header_meta(0, color: :red) + |> Table.put_header_meta(1, color: :blue) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + |\e[31m Artist \e[0m|\e[34m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with set column meta color across all columns and specific header override", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_column_meta(1, color: :red) + |> Table.put_header_meta(1, color: :blue) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist |\e[34m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict |\e[31m Cyanide \e[0m| 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle |\e[31m Welcome To Shanktown \e[0m| 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with set column meta color across all columns and clear header", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_column_meta(1, color: :red) + |> Table.put_header_meta(1, color: :reset) + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist |\e[0m Track \e[0m| Year | + +----------------+----------------------+------+ + | Konflict |\e[31m Cyanide \e[0m| 1999 | + | Keaton & Hive |\e[31m The Plague \e[0m| 2003 | + | Vicious Circle |\e[31m Welcome To Shanktown \e[0m| 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with title that is one less than the combined column widths", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Be Here Now") + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases That Be Here Now | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with title that exactly matches combined column widths", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Here Now") + |> Table.render() + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases That Are Here Now | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "default render with title that exceeds combined column widths by 1 character", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Seen Here") + |> Table.render() + + assert rendered == """ + +-------------------------------------------------+ + | Renegade Hardware Releases That Are Seen Here | + +-----------------+-----------------------+-------+ + | Artist | Track | Year | + +-----------------+-----------------------+-------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +-----------------+-----------------------+-------+ + """ + end + + test "default render with title that far exceeds combined column widths", %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Present In This Table") + |> Table.render() + + assert rendered == """ + +-------------------------------------------------------------+ + | Renegade Hardware Releases That Are Present In This Table | + +---------------------+---------------------------+-----------+ + | Artist | Track | Year | + +---------------------+---------------------------+-----------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +---------------------+---------------------------+-----------+ + """ + end + + test "default render with title that far exceeds combined column widths with irregular alignments", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Present In This Table") + |> Table.put_column_meta(0, align: :right) + |> Table.put_column_meta(1, align: :center) + |> Table.render() + + assert rendered == """ + +-------------------------------------------------------------+ + | Renegade Hardware Releases That Are Present In This Table | + +---------------------+---------------------------+-----------+ + | Artist | Track | Year | + +---------------------+---------------------------+-----------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +---------------------+---------------------------+-----------+ + """ + end + + test "default render with title exceeding combined column widths by multiple of number of columns", + %{table: table} do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases Seen In This Very Table") + |> Table.render() + + assert rendered == """ + +----------------------------------------------------+ + | Renegade Hardware Releases Seen In This Very Table | + +------------------+------------------------+--------+ + | Artist | Track | Year | + +------------------+------------------------+--------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +------------------+------------------------+--------+ + """ + end + + test "default render with title exactly matching combined column widths when only 2 columns" do + title = "Renegade Hardware Releases Shown Here" + header = ["Artist", "Track"] + + rows = [ + ["Konflict", "Cyanide"], + ["Keaton & Hive", "The Plague"], + ["Vicious Circle", "Welcome To Shanktown"] + ] + + {:ok, rendered} = + Table.new(rows, header, title) + |> Table.render() + + assert rendered === """ + +---------------------------------------+ + | Renegade Hardware Releases Shown Here | + +----------------+----------------------+ + | Artist | Track | + +----------------+----------------------+ + | Konflict | Cyanide | + | Keaton & Hive | The Plague | + | Vicious Circle | Welcome To Shanktown | + +----------------+----------------------+ + """ + end + + test "default render with title exceeding combined column widths by 1 character when only 2 columns" do + title = "Renegade Hardware Releases Shown Here!" + header = ["Artist", "Track"] + + rows = [ + ["Konflict", "Cyanide"], + ["Keaton & Hive", "The Plague"], + ["Vicious Circle", "Welcome To Shanktown"] + ] + + {:ok, rendered} = + Table.new(rows, header, title) + |> Table.render() + + assert rendered === """ + +-----------------------------------------+ + | Renegade Hardware Releases Shown Here! | + +-----------------+-----------------------+ + | Artist | Track | + +-----------------+-----------------------+ + | Konflict | Cyanide | + | Keaton & Hive | The Plague | + | Vicious Circle | Welcome To Shanktown | + +-----------------+-----------------------+ + """ + end + + test "default render with individual cells containing ANSI color codes" do + title = "Renegade Hardware Releases" + header = ["Artist", "Track", "Year"] + + rows = [ + ["Konflict", "Cyanide", IO.ANSI.format([:red, "19", :bright, "99"])], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", IO.ANSI.format(["200", :green, "7"])] + ] + + {:ok, rendered} = + rows + |> Table.new(header, title) + |> Table.render() + + assert rendered === """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | \e[31m19\e[1m99\e[0m | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 200\e[32m7\e[0m | + +----------------+----------------------+------+ + """ + end + + test "minimal render (zero padding) with title exceeding combined column widths", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Present In This Table") + |> Table.put_column_meta(:all, padding: 0) + |> Table.render(horizontal_style: :off, vertical_style: :off) + + assert rendered == """ + Renegade Hardware Releases That Are Present In This Table + + Artist Track Year + + Konflict Cyanide 1999 + Keaton & Hive The Plague 2003 + Vicious Circle Welcome To Shanktown 2007 + """ + end + + test "render with vertical style: frame with title exceeding combined column widths", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Present In This Table") + |> Table.render(vertical_style: :frame) + + assert rendered == """ + +-------------------------------------------------------------+ + | Renegade Hardware Releases That Are Present In This Table | + +-------------------------------------------------------------+ + | Artist Track Year | + +-------------------------------------------------------------+ + | Konflict Cyanide 1999 | + | Keaton & Hive The Plague 2003 | + | Vicious Circle Welcome To Shanktown 2007 | + +-------------------------------------------------------------+ + """ + end + + test "render with irregular column paddings with title exceeding combined column widths", %{ + table: table + } do + {:ok, rendered} = + table + |> Table.put_title("Renegade Hardware Releases That Are Present In This Table") + |> Table.put_column_meta(0..1, padding: 0) + |> Table.put_column_meta(2, padding: 1) + |> Table.render() + + assert rendered == """ + +------------------------------------------------------------+ + | Renegade Hardware Releases That Are Present In This Table | + +--------------------+--------------------------+------------+ + |Artist |Track | Year | + +--------------------+--------------------------+------------+ + |Konflict |Cyanide | 1999 | + |Keaton & Hive |The Plague | 2003 | + |Vicious Circle |Welcome To Shanktown | 2007 | + +--------------------+--------------------------+------------+ + """ + end +end diff --git a/test/table_rex/table_rex_test.exs b/test/table_rex/table_rex_test.exs new file mode 100755 index 0000000..325632b --- /dev/null +++ b/test/table_rex/table_rex_test.exs @@ -0,0 +1,143 @@ +defmodule TableRex.TableRexTest do + use ExUnit.Case, async: true + + doctest TableRex + + test "quick_render with title, header and rows" do + {:ok, rendered} = + TableRex.quick_render( + [ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ], + ["Artist", "Track", "Year"], + "Renegade Hardware Releases" + ) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render with header and rows" do + {:ok, rendered} = + TableRex.quick_render( + [ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ], + ["Artist", "Track", "Year"] + ) + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render with just rows" do + {:ok, rendered} = + TableRex.quick_render([ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ]) + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render with no rows to test error return" do + {:error, _reason} = TableRex.quick_render([]) + end + + test "quick_render! with title, header and rows" do + rendered = + TableRex.quick_render!( + [ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ], + ["Artist", "Track", "Year"], + "Renegade Hardware Releases" + ) + + assert rendered == """ + +----------------------------------------------+ + | Renegade Hardware Releases | + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render! with header and rows" do + rendered = + TableRex.quick_render!( + [ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ], + ["Artist", "Track", "Year"] + ) + + assert rendered == """ + +----------------+----------------------+------+ + | Artist | Track | Year | + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render! with just rows" do + rendered = + TableRex.quick_render!([ + ["Konflict", "Cyanide", 1999], + ["Keaton & Hive", "The Plague", 2003], + ["Vicious Circle", "Welcome To Shanktown", 2007] + ]) + + assert rendered == """ + +----------------+----------------------+------+ + | Konflict | Cyanide | 1999 | + | Keaton & Hive | The Plague | 2003 | + | Vicious Circle | Welcome To Shanktown | 2007 | + +----------------+----------------------+------+ + """ + end + + test "quick_render! with no rows to test error is raised" do + assert_raise TableRex.Error, fn -> + TableRex.quick_render!([]) + end + end +end diff --git a/test/table_rex/table_test.exs b/test/table_rex/table_test.exs new file mode 100755 index 0000000..902ea80 --- /dev/null +++ b/test/table_rex/table_test.exs @@ -0,0 +1,886 @@ +defmodule TableRex.TableTest do + use ExUnit.Case, async: true + alias TableRex.Cell + alias TableRex.Column + alias TableRex.Table + + doctest Table + + setup do + table = Table.new() + {:ok, table: table} + end + + test "new with no arguments", _ do + assert Table.new() == %Table{} + end + + test "new with initial rows", _ do + rows = [["Dom & Roland", "Thunder", 1998]] + + assert Table.new(rows) == %Table{ + rows: [ + [ + %Cell{ + rendered_value: "Dom & Roland", + raw_value: "Dom & Roland" + }, + %Cell{ + rendered_value: "Thunder", + raw_value: "Thunder" + }, + %Cell{ + rendered_value: "1998", + raw_value: 1998 + } + ] + ] + } + end + + test "new with initial rows and header", _ do + rows = [["Dom & Roland", "Thunder", 1998]] + header = ["Artist", "Track", "Year"] + + assert Table.new(rows, header) == %Table{ + header_row: [ + %Cell{ + raw_value: "Artist", + rendered_value: "Artist" + }, + %Cell{ + raw_value: "Track", + rendered_value: "Track" + }, + %Cell{ + raw_value: "Year", + rendered_value: "Year" + } + ], + rows: [ + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Thunder", + rendered_value: "Thunder" + }, + %Cell{ + raw_value: 1998, + rendered_value: "1998" + } + ] + ] + } + end + + test "new with initial rows, header and title", _ do + title = "Dom & Roland Releases" + rows = [["Dom & Roland", "Thunder", 1998]] + header = ["Artist", "Track", "Year"] + + assert Table.new(rows, header, title) == %Table{ + title: "Dom & Roland Releases", + header_row: [ + %Cell{ + raw_value: "Artist", + rendered_value: "Artist" + }, + %Cell{ + raw_value: "Track", + rendered_value: "Track" + }, + %Cell{ + raw_value: "Year", + rendered_value: "Year" + } + ], + rows: [ + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Thunder", + rendered_value: "Thunder" + }, + %Cell{ + raw_value: 1998, + rendered_value: "1998" + } + ] + ] + } + end + + test "adding a single row with values", %{table: table} do + row = ["Dom & Roland", "Thunder", 1998] + table = Table.add_row(table, row) + + assert table.rows == [ + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Thunder", + rendered_value: "Thunder" + }, + %Cell{ + raw_value: 1998, + rendered_value: "1998" + } + ] + ] + + second_row = ["Calyx", "Downpour", 2001] + table = Table.add_row(table, second_row) + + assert table.rows == [ + [ + %Cell{ + raw_value: "Calyx", + rendered_value: "Calyx" + }, + %Cell{ + raw_value: "Downpour", + rendered_value: "Downpour" + }, + %Cell{ + raw_value: 2001, + rendered_value: "2001" + } + ], + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Thunder", + rendered_value: "Thunder" + }, + %Cell{ + raw_value: 1998, + rendered_value: "1998" + } + ] + ] + end + + test "adding a single row with cell structs", %{table: table} do + row = [ + "Rascal & Klone", + %Cell{raw_value: "The Grind", align: :left, color: :red}, + %Cell{raw_value: 2000, align: :right} + ] + + table = Table.add_row(table, row) + + assert table.rows == [ + [ + %Cell{rendered_value: "Rascal & Klone", raw_value: "Rascal & Klone"}, + %Cell{ + rendered_value: "The Grind", + raw_value: "The Grind", + align: :left, + color: :red + }, + %Cell{rendered_value: "2000", raw_value: 2000, align: :right} + ] + ] + end + + test "adding multiple rows multiple times results in sane order output", %{table: table} do + rows = [ + ["E-Z Rollers", "Tough At The Top", %Cell{raw_value: 1998, align: :right}], + ["nCode", "Spasm", %Cell{raw_value: 1999, align: :right}] + ] + + additional_rows = [ + ["Aquasky", "Uptight", %Cell{raw_value: 2000, align: :right}], + ["Dom & Roland", "Dance All Night", %Cell{raw_value: 2004, align: :right, color: :red}] + ] + + table = Table.add_rows(table, rows) + table = Table.add_rows(table, additional_rows) + + assert table.rows == [ + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Dance All Night", + rendered_value: "Dance All Night" + }, + %Cell{ + raw_value: 2004, + rendered_value: "2004", + align: :right, + color: :red + } + ], + [ + %Cell{ + raw_value: "Aquasky", + rendered_value: "Aquasky" + }, + %Cell{ + raw_value: "Uptight", + rendered_value: "Uptight" + }, + %Cell{ + raw_value: 2000, + rendered_value: "2000", + align: :right + } + ], + [ + %Cell{ + raw_value: "nCode", + rendered_value: "nCode" + }, + %Cell{ + raw_value: "Spasm", + rendered_value: "Spasm" + }, + %Cell{ + raw_value: 1999, + rendered_value: "1999", + align: :right + } + ], + [ + %Cell{ + raw_value: "E-Z Rollers", + rendered_value: "E-Z Rollers" + }, + %Cell{ + raw_value: "Tough At The Top", + rendered_value: "Tough At The Top" + }, + %Cell{ + raw_value: 1998, + rendered_value: "1998", + align: :right + } + ] + ] + end + + test "add functions used together results in sane/expected output order", %{table: table} do + first_row = ["Blame", "Music Takes You", 1992] + + middle_rows = [ + ["Deep Blue", "The Helicopter Tune", 1993], + ["Dom & Roland", "Killa Bullet", 1999] + ] + + fourth_row = ["Omni Trio", "Lucid", 2001] + table = Table.add_row(table, first_row) + table = Table.add_rows(table, middle_rows) + table = Table.add_row(table, fourth_row) + + assert table.rows == [ + [ + %Cell{ + raw_value: "Omni Trio", + rendered_value: "Omni Trio" + }, + %Cell{ + raw_value: "Lucid", + rendered_value: "Lucid" + }, + %Cell{ + raw_value: 2001, + rendered_value: "2001" + } + ], + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland" + }, + %Cell{ + raw_value: "Killa Bullet", + rendered_value: "Killa Bullet" + }, + %Cell{ + raw_value: 1999, + rendered_value: "1999" + } + ], + [ + %Cell{ + raw_value: "Deep Blue", + rendered_value: "Deep Blue" + }, + %Cell{ + raw_value: "The Helicopter Tune", + rendered_value: "The Helicopter Tune" + }, + %Cell{ + raw_value: 1993, + rendered_value: "1993" + } + ], + [ + %Cell{ + raw_value: "Blame", + rendered_value: "Blame" + }, + %Cell{ + raw_value: "Music Takes You", + rendered_value: "Music Takes You" + }, + %Cell{ + raw_value: 1992, + rendered_value: "1992" + } + ] + ] + end + + test "setting and overriding a title", %{table: table} do + title_1 = "Metalheadz Releases" + table = Table.put_title(table, title_1) + assert table.title == title_1 + title_2 = "Moving Shadow Releases" + table = Table.put_title(table, title_2) + assert table.title == title_2 + end + + test "clearing a title", %{table: table} do + title = "Moving Shadow Releases" + table = Table.put_title(table, title) + table = Table.put_title(table, "") + assert table.title == nil + table = Table.put_title(table, title) + table = Table.put_title(table, nil) + assert table.title == nil + end + + test "setting and then overriding a header row", %{table: table} do + header_row = ["Artist"] + table = Table.put_header(table, header_row) + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist"} + ] + + header_row = ["Artist", "Track", "Year"] + table = Table.put_header(table, header_row) + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist"}, + %Cell{raw_value: "Track", rendered_value: "Track"}, + %Cell{raw_value: "Year", rendered_value: "Year"} + ] + end + + test "setting a header row with cell structs", %{table: table} do + header_row = [ + "Artist", + %Cell{raw_value: "Track", align: :left, color: :red}, + %Cell{raw_value: "Year", align: :right} + ] + + table = Table.put_header(table, header_row) + + assert table.header_row == [ + %Cell{ + raw_value: "Artist", + rendered_value: "Artist" + }, + %Cell{ + raw_value: "Track", + rendered_value: "Track", + align: :left, + color: :red + }, + %Cell{ + raw_value: "Year", + rendered_value: "Year", + align: :right + } + ] + end + + test "clearing a set header row", %{table: table} do + header_row = ["Artist"] + + table = + table + |> Table.put_header(header_row) + |> Table.put_header(nil) + + assert table.header_row == [] + + table = + table + |> Table.put_header(header_row) + |> Table.put_header([]) + + assert table.header_row == [] + end + + test "setting column meta for a specific column", %{table: table} do + assert table.columns == %{} + table = Table.put_column_meta(table, 0, align: :right) + + assert table.columns == %{ + 0 => %Column{align: :right} + } + + table = Table.put_column_meta(table, 0, align: :left, padding: 2, color: :red) + table = Table.put_column_meta(table, 1, align: :right) + + assert table.columns == %{ + 0 => %Column{align: :left, padding: 2, color: :red}, + 1 => %Column{align: :right} + } + end + + test "setting column meta across specific columns", %{table: table} do + table = Table.put_column_meta(table, 0..2, align: :right) + + assert table.columns == %{ + 0 => %Column{align: :right}, + 1 => %Column{align: :right}, + 2 => %Column{align: :right} + } + + table = Table.put_column_meta(table, 1..3, padding: 2) + + assert table.columns == %{ + 0 => %Column{align: :right}, + 1 => %Column{align: :right, padding: 2}, + 2 => %Column{align: :right, padding: 2}, + 3 => %Column{padding: 2} + } + + table = Table.put_column_meta(table, [1, 2], padding: 4) + + assert table.columns == %{ + 0 => %Column{align: :right}, + 1 => %Column{align: :right, padding: 4}, + 2 => %Column{align: :right, padding: 4}, + 3 => %Column{padding: 2} + } + + table = Table.put_column_meta(table, [2, 3], color: :red) + + assert table.columns == %{ + 0 => %Column{align: :right}, + 1 => %Column{align: :right, padding: 4}, + 2 => %Column{align: :right, padding: 4, color: :red}, + 3 => %Column{padding: 2, color: :red} + } + end + + test "setting column meta across all columns", %{table: table} do + assert Table.get_column_meta(table, 0, :align) == :left + assert Table.get_column_meta(table, 1, :align) == :left + table = Table.put_column_meta(table, :all, align: :right) + assert Table.get_column_meta(table, 0, :align) == :right + assert Table.get_column_meta(table, 1, :align) == :right + table = Table.put_column_meta(table, :all, align: :left) + assert Table.get_column_meta(table, 0, :align) == :left + assert Table.get_column_meta(table, 1, :align) == :left + assert Table.get_column_meta(table, 2, :align) == :left + end + + test "overriding column meta across all columns", %{table: table} do + table = Table.put_column_meta(table, 0, align: :right) + assert Table.get_column_meta(table, 0, :align) == :right + assert Table.get_column_meta(table, 1, :align) == :left + table = Table.put_column_meta(table, :all, align: :left) + assert Table.get_column_meta(table, 0, :align) == :left + assert Table.get_column_meta(table, 1, :align) == :left + end + + test "setting cell meta", %{table: table} do + rows = [ + ["Dom & Roland", "Thunder", 1998], + ["Calyx", "Downpour", 2001] + ] + + table = + table + |> Table.add_rows(rows) + |> Table.put_cell_meta(0, 0, align: :right) + |> Table.put_cell_meta(0, 1, color: :red) + |> Table.put_cell_meta(1, 0, color: :red) + |> Table.put_cell_meta(1, 1, align: :left) + + assert table.rows == [ + [ + %Cell{raw_value: "Calyx", rendered_value: "Calyx", align: nil, color: :red}, + %Cell{raw_value: "Downpour", rendered_value: "Downpour", align: :left, color: nil}, + %Cell{raw_value: 2001, rendered_value: "2001", align: nil, color: nil} + ], + [ + %Cell{ + raw_value: "Dom & Roland", + rendered_value: "Dom & Roland", + align: :right, + color: nil + }, + %Cell{raw_value: "Thunder", rendered_value: "Thunder", align: nil, color: :red}, + %Cell{raw_value: 1998, rendered_value: "1998", align: nil, color: nil} + ] + ] + end + + test "setting header cell meta", %{table: table} do + header = ["Artist", "Track", "Year"] + + table = + table + |> Table.put_header(header) + |> Table.put_header_meta(0, align: :left) + |> Table.put_header_meta(1, align: :right) + |> Table.put_header_meta(2, color: :red) + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist", align: :left, color: nil}, + %Cell{raw_value: "Track", rendered_value: "Track", align: :right, color: nil}, + %Cell{raw_value: "Year", rendered_value: "Year", align: nil, color: :red} + ] + end + + test "setting header cell meta across multiple cells", %{table: table} do + header = ["Artist", "Track", "Year"] + + table = + table + |> Table.put_header(header) + |> Table.put_header_meta(0..2, align: :center) + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist", align: :center}, + %Cell{raw_value: "Track", rendered_value: "Track", align: :center}, + %Cell{raw_value: "Year", rendered_value: "Year", align: :center} + ] + + table = Table.put_header_meta(table, [1, 2], align: :right) + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist", align: :center}, + %Cell{raw_value: "Track", rendered_value: "Track", align: :right}, + %Cell{raw_value: "Year", rendered_value: "Year", align: :right} + ] + end + + test "clearing rows", %{table: table} do + title = "Moving Shadow Releases" + header = ["Artist", "Track", "Year"] + + rows = [ + ["Blame", "Neptune", 1996], + ["Rob & Dom", "Distorted Dreams", 1997] + ] + + table = + table + |> Table.put_title(title) + |> Table.put_header(header) + |> Table.add_rows(rows) + |> Table.clear_rows() + + assert table.title == title + + assert table.header_row == [ + %Cell{raw_value: "Artist", rendered_value: "Artist"}, + %Cell{raw_value: "Track", rendered_value: "Track"}, + %Cell{raw_value: "Year", rendered_value: "Year"} + ] + + assert table.rows == [] + end + + test "copying to a new table", %{table: existing_table} do + new_row = ["Calyx", "Get Myself To You", 2005] + existing_table = Table.add_row(existing_table, new_row) + new_table = existing_table + + assert new_table.rows == [ + [ + %Cell{raw_value: "Calyx", rendered_value: "Calyx"}, + %Cell{raw_value: "Get Myself To You", rendered_value: "Get Myself To You"}, + %Cell{raw_value: 2005, rendered_value: "2005"} + ] + ] + + additional_row = ["E-Z Rollers", "Back To Love", 2002] + new_table = Table.add_row(new_table, additional_row) + + assert existing_table.rows == [ + [ + %Cell{raw_value: "Calyx", rendered_value: "Calyx"}, + %Cell{raw_value: "Get Myself To You", rendered_value: "Get Myself To You"}, + %Cell{raw_value: 2005, rendered_value: "2005"} + ] + ] + + assert new_table.rows == [ + [ + %Cell{raw_value: "E-Z Rollers", rendered_value: "E-Z Rollers"}, + %Cell{raw_value: "Back To Love", rendered_value: "Back To Love"}, + %Cell{raw_value: 2002, rendered_value: "2002"} + ], + [ + %Cell{raw_value: "Calyx", rendered_value: "Calyx"}, + %Cell{raw_value: "Get Myself To You", rendered_value: "Get Myself To You"}, + %Cell{raw_value: 2005, rendered_value: "2005"} + ] + ] + end + + test "get_column_meta returns correct values and defaults", %{table: table} do + table = Table.put_column_meta(table, 0, align: :right) + assert Table.get_column_meta(table, 0, :align) == :right + assert Table.get_column_meta(table, 1, :align) == :left + assert Table.get_column_meta(table, 2, :padding) == 1 + end + + test "has_rows? returns correct response", _setup do + table = Table.new() + assert table |> Table.has_rows?() == false + table = %Table{rows: [["Exile", "Silver Spirit", "2003"]]} + assert table |> Table.has_rows?() == true + table = %Table{rows: []} + assert table |> Table.has_rows?() == false + end + + test "has_header? returns correct response", _setup do + table = Table.new() + assert table |> Table.has_header?() == false + table = %Table{header_row: ["Artist", "Track", "Year"]} + assert table |> Table.has_header?() == true + table = %Table{header_row: []} + assert table |> Table.has_header?() == false + end + + defmodule TestRenderer do + @behaviour TableRex.Renderer + + def default_options do + %{ + horizontal_style: :header, + vertical_style: :all, + renderer_specific_option: true + } + end + + def render(table, render_opts) do + send(self(), {:rendering, table, render_opts}) + {:ok, "Rendered String"} + end + end + + test "render/2 default runs" do + {:ok, rendered} = + Table.new() + |> Table.add_row(["a"]) + |> Table.render() + + assert is_binary(rendered) + end + + test "render/2 calls correctly" do + {:ok, _} = + Table.new() + |> Table.add_row(["a"]) + |> Table.render(renderer: TestRenderer) + + expected_opts = %{ + horizontal_style: :header, + vertical_style: :all, + renderer_specific_option: true + } + + assert_received {:rendering, _table, ^expected_opts} + end + + test "render/2 errors when not enough rows" do + {:error, reason} = + Table.new() + |> Table.render(renderer: TestRenderer) + + assert reason == "Table must have at least one row before being rendered" + refute_received {:rendering, _, _} + end + + test "render!/2 default runs" do + rendered = + Table.new() + |> Table.add_row(["a"]) + |> Table.render!() + + assert is_binary(rendered) + end + + test "render!/2 calls correctly" do + Table.new() + |> Table.add_row(["a"]) + |> Table.render!(renderer: TestRenderer) + + expected_opts = %{ + horizontal_style: :header, + vertical_style: :all, + renderer_specific_option: true + } + + assert_received {:rendering, _table, ^expected_opts} + end + + test "render/2 raises an error on failure" do + assert_raise TableRex.Error, fn -> + Table.new() + |> Table.render!() + end + end + + test "sort/3 should sort the table using the first column (desc)" do + table = + Table.new() + |> Table.add_row([1, "a"]) + |> Table.add_row([2, "b"]) + |> Table.add_row([3, "c"]) + |> Table.add_row([3, "d"]) + |> Table.sort(0, :desc) + + # Remember: rows are stored in reverse internally. + assert table.rows == [ + [ + %Cell{raw_value: 1, rendered_value: "1"}, + %Cell{raw_value: "a", rendered_value: "a"} + ], + [ + %Cell{raw_value: 2, rendered_value: "2"}, + %Cell{raw_value: "b", rendered_value: "b"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "c", rendered_value: "c"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "d", rendered_value: "d"} + ] + ] + end + + test "sort/3 should sort the table using the first column (asc)" do + table = + Table.new() + |> Table.add_row([1, "a"]) + |> Table.add_row([2, "b"]) + |> Table.add_row([3, "c"]) + |> Table.add_row([3, "d"]) + |> Table.sort(0, :asc) + + # Remember: rows are stored in reverse internally. + assert table.rows == [ + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "c", rendered_value: "c"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "d", rendered_value: "d"} + ], + [ + %Cell{raw_value: 2, rendered_value: "2"}, + %Cell{raw_value: "b", rendered_value: "b"} + ], + [ + %Cell{raw_value: 1, rendered_value: "1"}, + %Cell{raw_value: "a", rendered_value: "a"} + ] + ] + end + + test "sort/3 should sort the table by the specified column (desc)" do + table = + Table.new() + |> Table.add_row([1, "a"]) + |> Table.add_row([2, "b"]) + |> Table.add_row([3, "c"]) + |> Table.add_row([3, "d"]) + |> Table.sort(1, :desc) + + # Remember: rows are stored in reverse internally. + assert table.rows == [ + [ + %Cell{raw_value: 1, rendered_value: "1"}, + %Cell{raw_value: "a", rendered_value: "a"} + ], + [ + %Cell{raw_value: 2, rendered_value: "2"}, + %Cell{raw_value: "b", rendered_value: "b"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "c", rendered_value: "c"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "d", rendered_value: "d"} + ] + ] + end + + test "sort/3 should sort the table by the specified column (asc)" do + table = + Table.new() + |> Table.add_row([1, "a"]) + |> Table.add_row([2, "b"]) + |> Table.add_row([3, "c"]) + |> Table.add_row([3, "d"]) + |> Table.sort(1, :asc) + + # Remember: rows are stored in reverse internally. + assert table.rows == [ + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "d", rendered_value: "d"} + ], + [ + %Cell{raw_value: 3, rendered_value: "3"}, + %Cell{raw_value: "c", rendered_value: "c"} + ], + [ + %Cell{raw_value: 2, rendered_value: "2"}, + %Cell{raw_value: "b", rendered_value: "b"} + ], + [ + %Cell{raw_value: 1, rendered_value: "1"}, + %Cell{raw_value: "a", rendered_value: "a"} + ] + ] + end + + test "sort/3 should raise when column index exists out of bounds" do + assert_raise TableRex.Error, fn -> + Table.new() + |> Table.add_row([3, "a"]) + |> Table.sort(3, :asc) + end + end + + test "sort/3 should raise when order parameter is invalid" do + assert_raise TableRex.Error, fn -> + Table.new() + |> Table.add_row([3, "a"]) + |> Table.sort(0, :crap) + end + end +end