Skip to main content

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}