Skip to main content

siderite_cli/report/
channel.rs

1//! Where JSON reports go, and the envelope of a command that failed
2//! before it could produce its report.
3//!
4//! An application binary writes its report to stdout, unless the
5//! [`REPORT_PATH_ENV`] variable names a file. The `siderite` tool sets it
6//! when it runs the application through `cargo run`, and sends the
7//! child's stdout to stderr, so whatever the application factory prints
8//! can never corrupt the JSON on stdout.
9
10use super::{Diagnostic, Envelope, Severity, json};
11use crate::error::CliError;
12use serde::Serialize;
13use std::io::Write;
14use std::path::Path;
15
16/// Environment variable naming the file an application binary writes its
17/// JSON report to instead of stdout.
18pub const REPORT_PATH_ENV: &str = "SIDERITE_REPORT_PATH";
19
20type Done = Result<(), CliError>;
21
22fn io_error(target: &str, err: &std::io::Error) -> CliError {
23    CliError::Io(format!("cannot write {target}: {err}"))
24}
25
26/// Diagnostic code of a usage error (exit `2`).
27pub const USAGE_CODE: &str = "cli.E001";
28/// Diagnostic code of a command that failed without a report (exit `1`).
29pub const FAILED_CODE: &str = "cli.E002";
30/// Diagnostic code of an application that exited without writing its
31/// report (build failure, crash, or a `siderite-cli` without `--json`).
32pub const NO_REPORT_CODE: &str = "cli.E003";
33
34/// The envelope of `command` when it failed before producing a report:
35/// `ok` is `false`, `data` is `null` and one error diagnostic explains
36/// why.
37///
38/// Args:
39///   command: The command that was requested.
40///   code: [`USAGE_CODE`], [`FAILED_CODE`] or [`NO_REPORT_CODE`].
41///   message: What went wrong; it is redacted.
42///
43/// Returns:
44///   The envelope to render.
45pub fn failure(command: &str, code: &str, message: &str) -> Envelope<()> {
46    let diag = Diagnostic::new(code, Severity::Error, message);
47    Envelope::new(command, (), vec![diag])
48}
49
50/// The failure envelope of a [`CliError`].
51pub fn error_envelope(command: &str, err: &CliError) -> Envelope<()> {
52    let code = if err.exit_code() == 2 {
53        USAGE_CODE
54    } else {
55        FAILED_CODE
56    };
57    failure(command, code, &err.to_string())
58}
59
60/// Write a rendered report to `path` (the value of [`REPORT_PATH_ENV`])
61/// or, when it is `None`, to stdout.
62///
63/// Errors:
64///   [`CliError::Io`] when the report cannot be written.
65pub fn emit(rendered: &str, path: Option<&Path>) -> Result<(), CliError> {
66    if let Some(path) = path {
67        let written = std::fs::write(path, rendered);
68        let shown = path.display().to_string();
69        return written.map_err(|err| io_error(&shown, &err));
70    }
71    let mut out = std::io::stdout().lock();
72    out.write_all(rendered.as_bytes())
73        .and_then(|()| out.flush())
74        .map_err(|err| io_error("stdout", &err))
75}
76
77/// Render `report` as JSON and [`emit`] it.
78///
79/// Errors:
80///   [`CliError::Io`] when the report cannot be serialized or written.
81pub fn emit_json<T: Serialize>(report: &T, path: Option<&Path>) -> Done {
82    let rendered = json::render(report);
83    let rendered = rendered.map_err(|err| CliError::Io(err.to_string()))?;
84    emit(&rendered, path)
85}