Why the output helpers are a pair¶
Two functions, one shape¶
render_table and render_json take the same argument — a slice of
rows — and return a string ending in a newline. That symmetry is the
whole point: a command produces rows, and something downstream picks a
renderer from a flag. The command itself never formats anything.
match format {
OutputFormat::Text => Ok(render_table(rows)),
OutputFormat::Json => render_json(rows),
}
The alternative is each command formatting its own output, which is how
CLIs end up with three subcommands that pad columns differently and a
fourth whose --json emits an object where the others emit an array.
Machine-readable output is only useful if it is predictable, and
predictability comes from there being one implementation.
Why they are not one function¶
The two signatures differ in a way that would be dishonest to hide.
render_table cannot fail; render_json can. Merging them behind a
format parameter would force the table path to return a Result it
never uses, and every caller would handle an error that cannot happen
on that branch.
They also carry different bounds — Tabled for one, Serialize for
the other — and a merged function would demand both from every caller
even when only one rendering is ever requested.
Why the table style is not configurable¶
render_table applies tabled's Style::psql() unconditionally.
There is no parameter, no builder and no configuration key.
The style is a decision made once for a whole toolkit rather than a
knob on each call. Every tool built on these helpers renders tables the
same way, which means a user who learns to read one tool's output can
read all of them, and a script that gets away with cutting on | keeps
working across them.
psql specifically because it is the least decorated style that still
has a visible column separator: no box-drawing characters to mangle in
a terminal with the wrong font or a log file with the wrong encoding,
and a header rule that makes the boundary obvious.
Adding a theming parameter would be easy and is not planned. It would turn a fixed contract into a per-call choice, and the first tool to choose differently would break the property that made the helper worth having.
If you need a different style, call tabled directly — it is a
published crate and render_table is thin enough to reimplement in
about ten lines.
Why JSON output is always an array¶
render_json serialises the slice, so the top level is a JSON array
even for one row and for none. A script can therefore write one code
path — iterate the array — instead of branching on whether today's
result happened to be a single object.
An empty result is [], not an error and not an empty string. "No rows
matched" is a valid answer to a query, and a consumer that has to
distinguish "nothing found" from "the command failed" gets that from
the exit code rather than from a mangled document.
Why render_json returns a stringified error¶
RenderError::Json carries a String rather than the original
serde_json::Error. That loses the ability to match on the underlying
cause, which is a real cost, and it is accepted because of who makes
this error happen.
serde_json fails on very little: not on non-finite floats, which
become null, and not on integer map keys, which get stringified. What
does fail is a map keyed by something with no string form, or a
hand-written Serialize impl that refuses a value. Both are bugs in
the row type — decided at compile time by whoever wrote the struct,
never by anything a user typed.
An error that only a programmer can cause does not need to be matchable at runtime. It needs to be reportable, and a string is enough for that.
The rustdoc on render_json names non-finite floats as a typical
cause, which is not true of serde_json; see
Known documentation defects.