cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
/// The output format requested by the user via `--output` / `-o`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum OutputFormat {
    /// Human-readable (default, existing behaviour unchanged).
    #[default]
    Human,
    /// Machine-readable JSON.
    Json,
    /// Machine-readable YAML.
    Yaml,
}

impl OutputFormat {
    pub(crate) fn from_str(s: &str) -> Option<Self> {
        match s {
            "json" => Some(OutputFormat::Json),
            "yaml" => Some(OutputFormat::Yaml),
            "human" => Some(OutputFormat::Human),
            _ => None,
        }
    }

    pub(crate) fn is_structured(self) -> bool {
        matches!(self, OutputFormat::Json | OutputFormat::Yaml)
    }
}

/// Render a serialisable value in the requested format, printing to stdout.
///
/// Falls back to `eprintln!` + exit(1) on serialisation error.
pub(crate) fn render_structured<T: serde::Serialize>(value: &T, fmt: OutputFormat) {
    use crate::infra::driving::cli::errors::{die1, CliError};
    match fmt {
        OutputFormat::Json => {
            let out = serde_json::to_string_pretty(value).unwrap_or_else(|e| {
                die1(
                    CliError::new(format!("JSON serialisation failed: {e}")).kind("internal"),
                    OutputFormat::Human,
                );
            });
            println!("{out}");
        }
        OutputFormat::Yaml => {
            let out = serde_yaml::to_string(value).unwrap_or_else(|e| {
                die1(
                    CliError::new(format!("YAML serialisation failed: {e}")).kind("internal"),
                    OutputFormat::Human,
                );
            });
            print!("{out}");
        }
        OutputFormat::Human => {
            // Caller should never reach this branch — they should call
            // the human-rendering path instead.
            unreachable!("render_structured called with Human format");
        }
    }
}

/// Scan `argv` linearly for `--output <VALUE>` or `-o <VALUE>` or
/// `--output=<VALUE>` / `-o=<VALUE>`.  This is a lightweight pre-scan that
/// runs before clap is invoked so that we can thread the format through
/// `dispatch()` without duplicating the full clap tree.
pub(crate) fn extract_output_from_args() -> OutputFormat {
    let args: Vec<String> = std::env::args().collect();
    let mut iter = args.iter().skip(1).peekable();
    while let Some(arg) = iter.next() {
        if arg == "--output" || arg == "-o" {
            if let Some(val) = iter.next() {
                return OutputFormat::from_str(val).unwrap_or_default();
            }
        } else if let Some(val) = arg
            .strip_prefix("--output=")
            .or_else(|| arg.strip_prefix("-o="))
        {
            return OutputFormat::from_str(val).unwrap_or_default();
        }
    }
    OutputFormat::Human
}