From 8fe6fda1cf6e643529c3bf52ff57c7f0ed9bf8ee Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 12:38:01 -0600 Subject: [PATCH 1/5] chore: various wardening --- .github/workflows/ci.yml | 8 ++-- .tool-versions | 4 +- CHANGELOG.md | 7 +++ LICENSE | 2 +- README.md | 2 +- lib/scribe.ex | 16 +++---- lib/scribe/border.ex | 52 +++++++++++------------ lib/scribe/formatter.ex | 27 +++++++++++- lib/scribe/style.ex | 2 +- lib/scribe/style/github_markdown.ex | 18 ++++---- lib/scribe/style/psql.ex | 66 ++++++++++++++--------------- lib/scribe/table.ex | 4 ++ mix.lock | 20 ++++----- test/formatter_test.exs | 51 ++++++++++++++++++++++ test/table_test.exs | 30 ++++++++++++- 15 files changed, 211 insertions(+), 98 deletions(-) create mode 100644 test/formatter_test.exs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e064be8..5705aeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.18.0" - otp-version: "27.0.1" + elixir-version: "1.19" + otp-version: "28" - name: Restore dependencies cache uses: actions/cache@v3 with: @@ -53,8 +53,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.18.0" - otp-version: "27.0.1" + elixir-version: "1.19" + otp-version: "28" - name: Restore dependencies cache uses: actions/cache@v3 with: diff --git a/.tool-versions b/.tool-versions index c3bbb38..85434ba 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.18.0-otp-27 -erlang 27.0.1 +elixir 1.19.4-otp-28 +erlang 28.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 844d525..b91be9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +**Added** + +- Better documented typespecs. +- Unit tests for `Scribe.Table` and `Scribe.Formatter.Line`. + ## v0.11.0 - 2024-08-31 **Added** diff --git a/LICENSE b/LICENSE index 2540342..001bb99 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-2024 Codedge LLC (https://www.codedge.io/) +Copyright (c) 2016-2026 Codedge LLC (https://www.codedge.io/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 786f0a8..f7f4f56 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,6 @@ Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/ ## License -Copyright (c) 2016-2024 Codedge LLC (https://www.codedge.io/) +Copyright (c) 2016-2026 Codedge LLC (https://www.codedge.io/) This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/scribe/blob/main/LICENSE) for details. diff --git a/lib/scribe.ex b/lib/scribe.ex index 7b72f7d..c17e222 100644 --- a/lib/scribe.ex +++ b/lib/scribe.ex @@ -154,11 +154,11 @@ defmodule Scribe do - `:width` - Defines table width. Defaults to `:infinite` """ @type format_opts :: [ - alignment: atom, - colorize: boolean, + alignment: atom(), + colorize: boolean(), data: [...], - style: module, - width: integer + style: module(), + width: integer() ] @doc ~S""" @@ -177,7 +177,7 @@ defmodule Scribe do +----------+---------+ :ok """ - @spec print(data, format_opts) :: :ok + @spec print(data(), format_opts()) :: :ok def print(_results, opts \\ []) def print([], _opts), do: :ok @@ -191,7 +191,7 @@ defmodule Scribe do @doc ~S""" Paginates data and starts a pseudo-interactive console. """ - @spec console(data, format_opts) :: no_return + @spec console(data(), format_opts()) :: :ok def console(results, opts \\ []) do results |> format(opts) @@ -216,7 +216,7 @@ defmodule Scribe do +----------+---------+ %{test: 1234, key: :value} """ - @spec inspect(data, format_opts) :: data + @spec inspect(data(), format_opts()) :: data() def inspect(results, opts \\ []) do print(results, opts) results @@ -233,7 +233,7 @@ defmodule Scribe do iex> format(%{test: 1234}, colorize: false) "+---------+\n| :test |\n+---------+\n| 1234 |\n+---------+\n" """ - @spec format(data) :: String.t() | :ok + @spec format(data()) :: String.t() | :ok def format(_results, opts \\ []) def format([], _opts), do: :ok diff --git a/lib/scribe/border.ex b/lib/scribe/border.ex index fa5e10e..ee220cb 100644 --- a/lib/scribe/border.ex +++ b/lib/scribe/border.ex @@ -13,24 +13,24 @@ defmodule Scribe.Border do +--------+ ``` """ - defstruct top_left_corner: "", - top_edge: "", - top_right_corner: "", - right_edge: "", - bottom_right_corner: "", - bottom_edge: "", + defstruct bottom_edge: "", bottom_left_corner: "", - left_edge: "" + bottom_right_corner: "", + left_edge: "", + right_edge: "", + top_edge: "", + top_left_corner: "", + top_right_corner: "" @type t :: %__MODULE__{ - top_left_corner: String.t(), - top_edge: String.t(), - top_right_corner: String.t(), - right_edge: String.t(), - bottom_right_corner: String.t(), bottom_edge: String.t(), bottom_left_corner: String.t(), - left_edge: String.t() + bottom_right_corner: String.t(), + left_edge: String.t(), + right_edge: String.t(), + top_edge: String.t(), + top_left_corner: String.t(), + top_right_corner: String.t() } @doc ~S""" @@ -40,27 +40,27 @@ defmodule Scribe.Border do iex> new("+", "|", "-") %Scribe.Border{ - top_left_corner: "+", - top_edge: "-", - top_right_corner: "+", - right_edge: "|", - bottom_right_corner: "+", bottom_edge: "-", bottom_left_corner: "+", - left_edge: "|" + bottom_right_corner: "+", + left_edge: "|", + right_edge: "|", + top_edge: "-", + top_left_corner: "+", + top_right_corner: "+" } """ - @spec new(String.t(), String.t(), String.t()) :: t + @spec new(String.t(), String.t(), String.t()) :: t() def new(corner, v_edge, h_edge) do %__MODULE__{ - top_left_corner: corner, - top_edge: h_edge, - top_right_corner: corner, - right_edge: v_edge, - bottom_right_corner: corner, bottom_edge: h_edge, bottom_left_corner: corner, - left_edge: v_edge + bottom_right_corner: corner, + left_edge: v_edge, + right_edge: v_edge, + top_edge: h_edge, + top_left_corner: corner, + top_right_corner: corner } end end diff --git a/lib/scribe/formatter.ex b/lib/scribe/formatter.ex index e317388..914fc86 100644 --- a/lib/scribe/formatter.ex +++ b/lib/scribe/formatter.ex @@ -1,14 +1,30 @@ defmodule Scribe.Formatter.Index do @moduledoc false - defstruct row: 0, row_max: 0, col: 0, col_max: 0 + defstruct col: 0, col_max: 0, row: 0, row_max: 0 + + @type t() :: %__MODULE__{ + col: non_neg_integer(), + col_max: non_neg_integer(), + row: non_neg_integer(), + row_max: non_neg_integer() + } end defmodule Scribe.Formatter.Line do @moduledoc false - defstruct data: [], widths: [], style: nil, opts: [], index: nil + defstruct data: [], index: nil, opts: [], style: nil, widths: [] + + @type t() :: %__MODULE__{ + data: list(), + index: Scribe.Formatter.Index.t() | nil, + opts: keyword(), + style: module() | nil, + widths: [non_neg_integer()] + } alias Scribe.Formatter.{Index, Line} + @spec format(t()) :: String.t() def format(%Line{index: %Index{row: 0}} = line) do top(line) <> data(line) <> bottom(line) end @@ -17,6 +33,7 @@ defmodule Scribe.Formatter.Line do data(line) <> bottom(line) end + @spec data(t()) :: String.t() def data(%Line{} = line) do %Line{ data: row, @@ -52,6 +69,7 @@ defmodule Scribe.Formatter.Line do left_edge <> line <> "\n" end + @spec cell(term(), non_neg_integer(), atom()) :: String.t() def cell(x, width, alignment \\ :left) do len = min(String.length(" #{inspect(x)} "), width) @@ -73,6 +91,7 @@ defmodule Scribe.Formatter.Line do end end + @spec cell_value(term(), non_neg_integer(), pos_integer()) :: String.t() def cell_value(x, padding, max_width) when padding >= 0 do truncate(" #{inspect(x)}#{String.duplicate(" ", padding)} ", max_width) end @@ -81,10 +100,12 @@ defmodule Scribe.Formatter.Line do String.slice(elem, 0..width) end + @spec colorize(String.t(), String.t()) :: String.t() def colorize(string, color) do "#{color}#{string}#{IO.ANSI.reset()}" end + @spec top(t()) :: String.t() def top(%Line{widths: widths, style: style, index: index, opts: opts}) do border = style.border_at(index.row, 0, index.row_max, index.col_max) top_left = border.top_left_corner @@ -107,6 +128,7 @@ defmodule Scribe.Formatter.Line do color_prefix <> top_left <> add_newline(line) end + @spec bottom(t()) :: String.t() def bottom(%Line{widths: widths, style: style, index: index}) do border = style.border_at(index.row, 0, index.row_max, index.col_max) bottom_left = border.bottom_left_corner @@ -122,6 +144,7 @@ defmodule Scribe.Formatter.Line do bottom_left <> add_newline(line) end + @spec add_newline(String.t()) :: String.t() def add_newline(""), do: "" def add_newline(line), do: line <> "\n" end diff --git a/lib/scribe/style.ex b/lib/scribe/style.ex index 02184b1..ea1a848 100644 --- a/lib/scribe/style.ex +++ b/lib/scribe/style.ex @@ -22,7 +22,7 @@ defmodule Scribe.Style do Defaults to `Scribe.Style.Default` if not specified. """ - @spec default :: module + @spec default() :: module() def default do Application.get_env(:scribe, :style, Scribe.Style.Default) end diff --git a/lib/scribe/style/github_markdown.ex b/lib/scribe/style/github_markdown.ex index 2852da1..a1d7890 100644 --- a/lib/scribe/style/github_markdown.ex +++ b/lib/scribe/style/github_markdown.ex @@ -26,25 +26,25 @@ defmodule Scribe.Style.GithubMarkdown do bottom_edge: "-", bottom_left_corner: "|", bottom_right_corner: "|", + left_edge: "|", + right_edge: "|", top_edge: " ", top_left_corner: " ", - top_right_corner: " ", - left_edge: "|", - right_edge: "|" + top_right_corner: " " } end # All other cells def border_at(_, _, _, _) do %Scribe.Border{ - top_left_corner: "", - top_edge: "", - top_right_corner: "", - right_edge: "|", - bottom_right_corner: "", bottom_edge: "", bottom_left_corner: "", - left_edge: "|" + bottom_right_corner: "", + left_edge: "|", + right_edge: "|", + top_edge: "", + top_left_corner: "", + top_right_corner: "" } end end diff --git a/lib/scribe/style/psql.ex b/lib/scribe/style/psql.ex index 75c4fcf..c0e296b 100644 --- a/lib/scribe/style/psql.ex +++ b/lib/scribe/style/psql.ex @@ -26,81 +26,81 @@ defmodule Scribe.Style.Psql do bottom_edge: "-", bottom_left_corner: "", bottom_right_corner: "+", + left_edge: "", + right_edge: "|", top_edge: " ", top_left_corner: "", - top_right_corner: " ", - left_edge: "", - right_edge: "|" + top_right_corner: " " } end # Top right cell def border_at(0, col, _, max) when col == max - 1 do %Scribe.Border{ - top_left_corner: " ", - top_edge: " ", - top_right_corner: "", - right_edge: "", - bottom_right_corner: "", bottom_edge: "-", bottom_left_corner: "+", - left_edge: "|" + bottom_right_corner: "", + left_edge: "|", + right_edge: "", + top_edge: " ", + top_left_corner: " ", + top_right_corner: "" } end # All other top-row cells def border_at(0, _, _, _) do %Scribe.Border{ - top_left_corner: " ", - top_edge: " ", - top_right_corner: " ", - right_edge: "|", - bottom_right_corner: "+", bottom_edge: "-", bottom_left_corner: "+", - left_edge: "|" + bottom_right_corner: "+", + left_edge: "|", + right_edge: "|", + top_edge: " ", + top_left_corner: " ", + top_right_corner: " " } end # First column cells def border_at(_, 0, _, _) do %Scribe.Border{ - top_left_corner: "", - top_edge: "", - top_right_corner: "", - right_edge: "|", - bottom_right_corner: "", bottom_edge: "", bottom_left_corner: "", - left_edge: "" + bottom_right_corner: "", + left_edge: "", + right_edge: "|", + top_edge: "", + top_left_corner: "", + top_right_corner: "" } end # Last column cells def border_at(_, col, _, max) when col == max - 1 do %Scribe.Border{ - top_left_corner: "", - top_edge: "", - top_right_corner: "", - right_edge: "", - bottom_right_corner: "", bottom_edge: "", bottom_left_corner: "", - left_edge: "|" + bottom_right_corner: "", + left_edge: "|", + right_edge: "", + top_edge: "", + top_left_corner: "", + top_right_corner: "" } end # All other cells def border_at(_, _, _, _) do %Scribe.Border{ - top_left_corner: "", - top_edge: "", - top_right_corner: "", - right_edge: "|", - bottom_right_corner: "", bottom_edge: "", bottom_left_corner: "", - left_edge: "|" + bottom_right_corner: "", + left_edge: "|", + right_edge: "|", + top_edge: "", + top_left_corner: "", + top_right_corner: "" } end end diff --git a/lib/scribe/table.ex b/lib/scribe/table.ex index 31cec4c..031651d 100644 --- a/lib/scribe/table.ex +++ b/lib/scribe/table.ex @@ -4,14 +4,17 @@ defmodule Scribe.Table do alias Scribe.Style alias Scribe.Formatter.{Index, Line} + @spec table_style(keyword()) :: module() def table_style(opts) do opts[:style] || Style.default() end + @spec total_width() :: :infinity | pos_integer() def total_width do Application.get_env(:scribe, :width, :infinity) end + @spec printable_width(keyword()) :: :infinity | integer() def printable_width(opts) do case opts[:width] || total_width() do :infinity -> :infinity @@ -19,6 +22,7 @@ defmodule Scribe.Table do end end + @spec format(list(), pos_integer(), pos_integer(), keyword()) :: String.t() def format(data, rows, cols, opts \\ []) do total_width = printable_width(opts) diff --git a/mix.lock b/mix.lock index 39ab982..7ea48ae 100644 --- a/mix.lock +++ b/mix.lock @@ -1,28 +1,28 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, - "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, - "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "8533cb896ea527959923f9c3f08e7083e18ff681388ad7c9a599dd5d28e9085f"}, "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, + "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, - "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, - "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "pane": {:hex, :pane, "0.5.0", "9151be55a29c1ef3f772ceabc33bc4aa00d32846c93350416af9fe7470af7dc5", [:mix], [], "hexpm", "71ad875092bff3c249195881a56df836ca5f9f2dcd668a21dd2b1b5d9549b7b9"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "pane": {:hex, :pane, "0.5.1", "5b8d31237bad6b5b7d45b155e2a250fc64c35aadeba7844c4e76f86eaf016576", [:mix], [], "hexpm", "7040e79f99306b3335d452667bd767805b020fcc35acd5c21e5a56650b0f0b00"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, diff --git a/test/formatter_test.exs b/test/formatter_test.exs new file mode 100644 index 0000000..7b7e05c --- /dev/null +++ b/test/formatter_test.exs @@ -0,0 +1,51 @@ +defmodule Scribe.Formatter.LineTest do + use ExUnit.Case + + alias Scribe.Formatter.Line + + describe "cell/3" do + test "left-aligns by default" do + result = Line.cell("hello", 15) + assert result == " \"hello\" " + end + + test "right-aligns with :right" do + result = Line.cell("hello", 15, :right) + assert result == " \"hello\" " + end + + test "center-aligns with :center" do + result = Line.cell("hello", 15, :center) + assert result =~ "\"hello\"" + end + end + + describe "cell_value/3" do + test "returns padded cell string" do + result = Line.cell_value("test", 5, 100) + assert result == " \"test\" " + end + + test "truncates to max_width" do + result = Line.cell_value("test", 0, 5) + assert String.length(result) <= 6 + end + end + + describe "colorize/2" do + test "wraps string with color and reset" do + result = Line.colorize("hello", IO.ANSI.red()) + assert result == "#{IO.ANSI.red()}hello#{IO.ANSI.reset()}" + end + end + + describe "add_newline/1" do + test "returns empty string for empty input" do + assert Line.add_newline("") == "" + end + + test "appends newline to non-empty string" do + assert Line.add_newline("test") == "test\n" + end + end +end diff --git a/test/table_test.exs b/test/table_test.exs index 5da8256..8b13e92 100644 --- a/test/table_test.exs +++ b/test/table_test.exs @@ -1,6 +1,8 @@ defmodule Scribe.TableTest do use ExUnit.Case + alias Scribe.Table + test "format/3 returns formatted table string" do data = [ [~s("test"), ~s(1234), ~s("longer string")], @@ -15,6 +17,32 @@ defmodule Scribe.TableTest do +--------------------+------------+-----------------------------+ """ - assert Scribe.Table.format(data, 2, 3, colorize: false) == expected + assert Table.format(data, 2, 3, colorize: false) == expected + end + + describe "table_style/1" do + test "returns default style when no style option" do + assert Table.table_style([]) == Scribe.Style.Default + end + + test "returns specified style" do + assert Table.table_style(style: Scribe.Style.Psql) == Scribe.Style.Psql + end + end + + describe "total_width/0" do + test "defaults to :infinity" do + assert Table.total_width() == :infinity + end + end + + describe "printable_width/1" do + test "returns :infinity when no width specified" do + assert Table.printable_width([]) == :infinity + end + + test "subtracts 8 from specified width" do + assert Table.printable_width(width: 80) == 72 + end end end From 20275da65bc0e48b5366358d96b9269ebb8e3426 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 12:54:09 -0600 Subject: [PATCH 2/5] test: 100% coverage --- CHANGELOG.md | 34 +++++++++++++++++----------------- lib/scribe.ex | 20 ++++++++++++++------ lib/scribe/formatter.ex | 1 + lib/scribe/table.ex | 4 ++++ test/scribe_test.exs | 40 ++++++++++++++++++++++++++++++++++++++++ test/style_test.exs | 36 ++++++++++++++++++++++++++++++++++++ test/table_test.exs | 19 ++++++++++++++++++- 7 files changed, 130 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91be9c..24f5e8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,49 +2,49 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [Unreleased] -**Added** +### Added - Better documented typespecs. -- Unit tests for `Scribe.Table` and `Scribe.Formatter.Line`. +- Full unit test coverage. -## v0.11.0 - 2024-08-31 +## [0.11.0] - 2024-08-31 -**Added** +### Added - Center and right text alignment options ([#16](https://github.com/codedge-llc/scribe/pull/16)). -**Changed** +### Changed - Bumped minimum Elixir version to 1.13. -**Removed** +### Removed - Removed `Scribe.auto_inspect/1`. - Removed `Scribe.auto_inspect?/0`. -## v0.10.0 - 2019-05-29 +## [0.10.0] - 2019-05-29 - Added `:device` option to `Scribe.print/2` for printing to a specific device. Defaults to `:stdio` -## v0.9.0 - 2019-05-04 +## [0.9.0] - 2019-05-04 - `NoBorder` style added. -## v0.8.2 - 2019-01-17 +## [0.8.2] - 2019-01-17 - Support for Elixir `v1.8` -## v0.8.1 - 2018-07-25 +## [0.8.1] - 2018-07-25 - Support for Elixir `v1.7` -## v0.8.0 - 2019-03-15 +## [0.8.0] - 2019-03-15 - `:compile_auto_inspect` and `:auto_inspect` config options, both default to `false`. @@ -68,11 +68,11 @@ true again. If auto-inspect is not compiled (or disabled), `Scribe.print/2` and similar functions will continue to work as normal. -## v0.7.0 - 2018-02-19 +## [0.7.0] - 2018-02-19 - Pseudographics style added. -## v0.6.0 - 2018-02-16 +## [0.6.0] - 2018-02-16 - Overrides Inspect protocol for `List` and `Map`. These types will now automatically return in Scribe's table format. Disabled by default. @@ -80,11 +80,11 @@ functions will continue to work as normal. - `Scribe.enable`, `Scribe.disable`, and `Scribe.enabled?` added. - Minimum Elixir version bumped to `1.5`. -## v0.5.1 - 2018-01-06 +## [0.5.1] - 2018-01-06 - Bump pane dependency to v0.2.0. -## v0.5.0 - 2017-03-27 +## [0.5.0] - 2017-03-27 - `@behaviour Scribe.Style` implemented (See `/style` for example adapters) - Colorized output. diff --git a/lib/scribe.ex b/lib/scribe.ex index c17e222..5d6a423 100644 --- a/lib/scribe.ex +++ b/lib/scribe.ex @@ -183,9 +183,9 @@ defmodule Scribe do def print([], _opts), do: :ok def print(results, opts) do - dev = opts |> Keyword.get(:device, :stdio) - results = results |> format(opts) - dev |> IO.puts(results) + dev = Keyword.get(opts, :device, :stdio) + results = format(results, opts) + IO.puts(dev, results) end @doc ~S""" @@ -233,7 +233,7 @@ defmodule Scribe do iex> format(%{test: 1234}, colorize: false) "+---------+\n| :test |\n+---------+\n| 1234 |\n+---------+\n" """ - @spec format(data()) :: String.t() | :ok + @spec format(data(), format_opts()) :: String.t() | :ok def format(_results, opts \\ []) def format([], _opts), do: :ok @@ -251,24 +251,31 @@ defmodule Scribe do Table.format(table, Enum.count(table), Enum.count(keys), opts) end + @spec map_string_values([map()]) :: [term()] defp map_string_values(keys), do: Enum.map(keys, &string_value(&1)) + + @spec map_string_values(map() | struct(), [map()]) :: [term()] defp map_string_values(row, keys), do: Enum.map(keys, &string_value(row, &1)) + @spec string_value(map()) :: term() defp string_value(%{name: name, key: _key}) do name end + @spec string_value(map() | struct(), map()) :: term() defp string_value(map, %{name: _name, key: key}) when is_function(key) do - map |> key.() + key.(map) end defp string_value(map, %{name: _name, key: key}) do - map |> Map.get(key) + Map.get(map, key) end + @spec fetch_keys([map() | struct()], list() | nil) :: [map()] defp fetch_keys([first | _rest], nil), do: fetch_keys(first) defp fetch_keys(_list, opts), do: process_headers(opts) + @spec process_headers(list()) :: [map()] defp process_headers(opts) do for opt <- opts do case opt do @@ -278,6 +285,7 @@ defmodule Scribe do end end + @spec fetch_keys(map() | struct()) :: [map()] defp fetch_keys(map) do map |> Map.keys() diff --git a/lib/scribe/formatter.ex b/lib/scribe/formatter.ex index 914fc86..48bc159 100644 --- a/lib/scribe/formatter.ex +++ b/lib/scribe/formatter.ex @@ -96,6 +96,7 @@ defmodule Scribe.Formatter.Line do truncate(" #{inspect(x)}#{String.duplicate(" ", padding)} ", max_width) end + @spec truncate(String.t(), non_neg_integer()) :: String.t() defp truncate(elem, width) do String.slice(elem, 0..width) end diff --git a/lib/scribe/table.ex b/lib/scribe/table.ex index 031651d..3f9a10b 100644 --- a/lib/scribe/table.ex +++ b/lib/scribe/table.ex @@ -56,6 +56,7 @@ defmodule Scribe.Table do end) end + @spec get_max_widths(list(), pos_integer(), pos_integer()) :: [non_neg_integer()] defp get_max_widths(data, rows, cols) do for c <- 0..(cols - 1) do data @@ -65,6 +66,7 @@ defmodule Scribe.Table do end end + @spec get_width(term()) :: non_neg_integer() defp get_width(value) do value |> inspect() @@ -72,6 +74,7 @@ defmodule Scribe.Table do |> String.length() end + @spec distribute_widths([non_neg_integer()], :infinity | pos_integer()) :: [non_neg_integer()] defp distribute_widths(widths, :infinity) do widths end @@ -84,6 +87,7 @@ defmodule Scribe.Table do end) end + @spec get_column_widths(list(), pos_integer(), non_neg_integer()) :: [term()] defp get_column_widths(data, rows, col) do for row <- 0..(rows - 1) do data diff --git a/test/scribe_test.exs b/test/scribe_test.exs index 0b9b606..ec1d26c 100644 --- a/test/scribe_test.exs +++ b/test/scribe_test.exs @@ -246,4 +246,44 @@ defmodule Scribe.ScribeTest do fun = fn -> assert Scribe.inspect(val) == val end capture_io(fun) end + + describe "format/2 edge cases" do + test "returns :ok for empty list" do + assert Scribe.format([]) == :ok + end + + test "returns :ok for empty list with opts" do + assert Scribe.format([], colorize: false) == :ok + end + end + + describe "print/2 edge cases" do + test "returns :ok for empty list" do + assert Scribe.print([]) == :ok + end + + test "returns :ok for empty list with opts" do + assert Scribe.print([], colorize: false) == :ok + end + end + + describe "inspect/2 edge cases" do + test "returns empty list for empty list" do + assert Scribe.inspect([]) == [] + end + end + + describe "console/2" do + test "formats and paginates data" do + capture_io("q\n", fn -> + Scribe.console(%{test: 1234}, colorize: false) + end) + end + + test "works with default opts" do + capture_io("q\n", fn -> + Scribe.console(%{test: 1234}) + end) + end + end end diff --git a/test/style_test.exs b/test/style_test.exs index 0dce986..73251ca 100644 --- a/test/style_test.exs +++ b/test/style_test.exs @@ -106,4 +106,40 @@ defmodule Scribe.StyleTest do assert actual == expected end end + + describe "colorized output" do + @color_data [ + %{a: true, b: nil, c: 42, d: "hello", e: :ok, f: {1, 2}} + ] + + test "default style with colors" do + result = Scribe.format(@color_data, style: Scribe.Style.Default) + assert is_binary(result) + assert result =~ IO.ANSI.reset() + end + + test "psql style with colors" do + result = Scribe.format(@color_data, style: Scribe.Style.Psql) + assert is_binary(result) + assert result =~ IO.ANSI.reset() + end + + test "github_markdown style with colors" do + result = Scribe.format(@color_data, style: Scribe.Style.GithubMarkdown) + assert is_binary(result) + assert result =~ IO.ANSI.reset() + end + + test "pseudo style with colors" do + result = Scribe.format(@color_data, style: Scribe.Style.Pseudo) + assert is_binary(result) + assert result =~ IO.ANSI.reset() + end + + test "no_border style with colors" do + result = Scribe.format(@color_data, style: Scribe.Style.NoBorder) + assert is_binary(result) + assert result =~ IO.ANSI.reset() + end + end end diff --git a/test/table_test.exs b/test/table_test.exs index 8b13e92..1dc8e9c 100644 --- a/test/table_test.exs +++ b/test/table_test.exs @@ -3,7 +3,7 @@ defmodule Scribe.TableTest do alias Scribe.Table - test "format/3 returns formatted table string" do + test "format/4 returns formatted table string" do data = [ [~s("test"), ~s(1234), ~s("longer string")], [~s(0), ~s(nil), ~s(:whatever)] @@ -20,6 +20,16 @@ defmodule Scribe.TableTest do assert Table.format(data, 2, 3, colorize: false) == expected end + test "format/3 works with default opts" do + data = [ + [~s("a"), ~s("b")], + [~s(1), ~s(2)] + ] + + result = Table.format(data, 2, 2) + assert is_binary(result) + end + describe "table_style/1" do test "returns default style when no style option" do assert Table.table_style([]) == Scribe.Style.Default @@ -34,6 +44,13 @@ defmodule Scribe.TableTest do test "defaults to :infinity" do assert Table.total_width() == :infinity end + + test "returns configured width" do + Application.put_env(:scribe, :width, 120) + assert Table.total_width() == 120 + after + Application.delete_env(:scribe, :width) + end end describe "printable_width/1" do From 9ad9a826c07b298c7674635d97b7436145083017 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 12:57:32 -0600 Subject: [PATCH 3/5] ci: add dialyzer job --- .github/workflows/ci.yml | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5705aeb..9a6ac40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} + uses: actions/checkout@v4 - name: Use Node.js 18.x uses: actions/setup-node@v3 with: @@ -27,7 +25,7 @@ jobs: name: Format/Credo runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Elixir uses: erlef/setup-beam@v1 with: @@ -45,11 +43,37 @@ jobs: run: mix format --check-formatted - name: Run Credo run: mix credo + dialyzer: + name: Dialyzer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: "1.19" + otp-version: "28" + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Restore dialyzer cache + uses: actions/cache@v3 + with: + path: priv/plts + key: ${{ runner.os }}-mix-plts-${{ hashFiles('./priv/plts/') }} + restore-keys: ${{ runner.os }}-mix-plts- + - name: Install dependencies + run: mix deps.get + - name: Run dialyzer + run: mix dialyzer test: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Elixir uses: erlef/setup-beam@v1 with: From c8c671a3c3c52eebe01e028163aad71a5f89d58b Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 13:02:10 -0600 Subject: [PATCH 4/5] fix: formatting --- lib/scribe/table.ex | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/scribe/table.ex b/lib/scribe/table.ex index 3f9a10b..cfca6ef 100644 --- a/lib/scribe/table.ex +++ b/lib/scribe/table.ex @@ -4,17 +4,20 @@ defmodule Scribe.Table do alias Scribe.Style alias Scribe.Formatter.{Index, Line} + @typep width :: :infinity | pos_integer() + @typep widths :: [non_neg_integer()] + @spec table_style(keyword()) :: module() def table_style(opts) do opts[:style] || Style.default() end - @spec total_width() :: :infinity | pos_integer() + @spec total_width() :: width() def total_width do Application.get_env(:scribe, :width, :infinity) end - @spec printable_width(keyword()) :: :infinity | integer() + @spec printable_width(keyword()) :: width() def printable_width(opts) do case opts[:width] || total_width() do :infinity -> :infinity @@ -56,7 +59,7 @@ defmodule Scribe.Table do end) end - @spec get_max_widths(list(), pos_integer(), pos_integer()) :: [non_neg_integer()] + @spec get_max_widths(list(), pos_integer(), pos_integer()) :: widths() defp get_max_widths(data, rows, cols) do for c <- 0..(cols - 1) do data @@ -74,7 +77,7 @@ defmodule Scribe.Table do |> String.length() end - @spec distribute_widths([non_neg_integer()], :infinity | pos_integer()) :: [non_neg_integer()] + @spec distribute_widths(widths(), width()) :: widths() defp distribute_widths(widths, :infinity) do widths end From 098b19553309cc3d70eb635add814ebd22e724ff Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 13:05:52 -0600 Subject: [PATCH 5/5] docs: update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f5e8e..c7465c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Better documented typespecs. -- Full unit test coverage. +- Better documented typespecs. ([#31](https://github.com/codedge-llc/scribe/pull/31)) +- Full unit test coverage. ([#31](https://github.com/codedge-llc/scribe/pull/31)) ## [0.11.0] - 2024-08-31