restish-csv is a concrete formatter-hook plugin that renders array-shaped JSON
response bodies as CSV.
It exists primarily as the canonical formatter-plugin example: small enough to read in one sitting, but real enough to demonstrate useful output shaping and a few non-trivial decisions around rows, columns, schema freezing, and value encoding.
- provide a useful non-core formatter example
- validate the formatter session protocol for bounded, paginated, and streamed output
- keep CSV rendering rules deterministic and easy to reason about
- avoid forcing the host to buffer everything just to satisfy one plugin
- becoming a spreadsheet modeling tool
- inventing a sophisticated flattening scheme for arbitrary nested objects
- supporting every scalar or mixed-array input shape
CSV is a strong example because it is:
- clearly presentation-oriented
- broadly useful
- stateful enough to validate the formatter session model
- small enough to stay learnable
It sits at exactly the boundary where formatter plugins should shine: the host owns request execution and normalization, while the plugin owns final bytes.
The plugin advertises:
name: csvhooks: ["formatter"]formatter_names: ["csv"]
This makes -o csv available through the generic formatter selection path.
The plugin participates in the formatter session protocol.
For an ordinary non-streaming response, Restish sends a start message whose
response.body holds the full value. For paginated and event-stream output,
Restish sends item messages with one value at a time.
The plugin therefore has two modes:
- one-shot document-like mode for bounded input
- incremental mode for paginated or streamed records
Whenever restish-csv receives a value, it expects it to be:
- one object, or
- an array of objects
The plugin treats these as errors:
- a value that is neither an object nor an array of objects
- any array item that is not an object
That narrow scope is intentional. The plugin is meant to be a focused tabular formatter, not a universal coercion layer.
For bounded input, the plugin:
- scans every row object
- builds the union of all object keys
- sorts the column names for deterministic output
- writes one CSV header row
- writes one CSV record per body item
Sorted columns trade first-row ordering for deterministic output. That makes the formatter easier to test and easier to compare across runs.
Cell encoding is intentionally simple:
nullbecomes an empty field- strings are emitted as raw text values
- numbers and booleans are rendered as their scalar text form
- arrays and objects are JSON-encoded into a single cell
This keeps the plugin predictable without inventing a custom flattening scheme for nested data.
For paginated and event-stream output:
- Restish starts one plugin process.
- The plugin waits for
formattermessages on stdin. - The first object(s) it receives determine the CSV header.
- The plugin writes one header row, then one data row per streamed object.
The streaming path is intentionally more constrained than the one-shot path:
- it accepts either one object or an array of objects per
itemmessage - once the header is written, later objects cannot add new CSV columns
- newly introduced fields are ignored with a diagnostic warning
- missing fields are emitted as empty cells
That tradeoff keeps the formatter genuinely stream-friendly. CSV requires a
header before later rows can be emitted, so a plugin that wants true streaming
must either freeze the schema early or buffer indefinitely. restish-csv
chooses the former and warns on schema drift rather than corrupting already
emitted CSV.
The schema-freeze rule is one of the defining behaviors of this plugin.
Before header emission:
- discover columns from the first available object set
After header emission:
- accept only rows whose keys are a subset of the frozen header
- write missing fields as empty cells
- warn once for newly introduced columns and ignore those cells
This is the key design choice that lets the plugin stay incremental.
The plugin should fail clearly for:
- unsupported input shape
- mixed arrays containing non-object items
- malformed formatter messages
Once bytes have already been written, these errors are still real failures. The host may already have partial output, but that is acceptable for a stream-aware formatter as long as the failure is explicit.
restish-csv demonstrates the intended boundary for formatter hooks:
- Restish still owns HTTP, decoding, filtering, pagination, and normalization
- the plugin receives a normalized response model rather than
*http.Response - the plugin owns final bytes for the rendered body/value it is asked to format
- the plugin can hold just enough state to stay consistent across paginated and event-stream output without forcing the host to buffer everything
This is exactly the kind of output transformation that should be easy to move out of process without changing the core CLI pipeline.
One notable non-goal is sharing built-in syntax highlighting. Formatter plugins
receive a color hint, but they are responsible for any ANSI or styling they
want to emit. The core auto formatter's highlighting helpers remain an
in-process implementation detail.
Possible, but less valuable as a plugin example.
Too simple to really validate the session protocol.
Friendlier for some spreadsheet workflows, but it adds a lot of policy to what should remain a focused reference implementation.
- Design 019 defines the formatter-hook session model.
- Design 028 defines the document-vs-record planner that decides when this plugin sees full bodies versus incremental items.