spec_driven_docs/output.rs
1//! The output boundary: every byte the binary prints goes through here.
2//!
3//! stdout carries the result — findings, summaries, rendered documents —
4//! as plain text, which is what pre-commit shows a consumer, or as one JSON
5//! object when a subcommand's `--json` flag selects it. stderr carries
6//! exactly one JSON error envelope for operational failures. No other
7//! module (main.rs aside) prints.
8
9use std::fmt::Display;
10
11use crate::domain::finding::Finding;
12use crate::error::AppError;
13
14/// Print one result line to stdout.
15pub fn line(text: impl Display) {
16 println!("{text}");
17}
18
19/// Write pre-rendered bytes — completions, man pages — to stdout.
20pub fn raw(bytes: &[u8]) {
21 use std::io::Write;
22 let _ = std::io::stdout().write_all(bytes);
23}
24
25/// Print one result as a single pretty-printed JSON object to stdout.
26///
27/// # Errors
28///
29/// [`AppError::Other`] when the value cannot be serialized.
30pub fn json(value: &impl serde::Serialize) -> Result<(), AppError> {
31 let text = serde_json::to_string_pretty(value).map_err(anyhow::Error::from)?;
32 println!("{text}");
33 Ok(())
34}
35
36/// Print every finding, one line each, to stdout.
37pub fn findings(findings: &[Finding]) {
38 for finding in findings {
39 println!("{finding}");
40 }
41}
42
43/// Emit the one structured error envelope to stderr.
44///
45/// Violations carry no envelope: their findings are already on stdout and
46/// the exit code is the report.
47pub fn error_envelope(error: &AppError) {
48 if matches!(error, AppError::Violations { .. }) {
49 return;
50 }
51 let envelope = serde_json::json!({
52 "ok": false,
53 "error": {
54 "kind": error.kind(),
55 "message": error.to_string(),
56 "exit_code": error.exit_code(),
57 }
58 });
59 eprintln!("{envelope}");
60}