govctl 0.9.2

Project governance CLI for RFC, ADR, and Work Item management
use super::LoadResult;
use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode};
use crate::schema::{ArtifactSchema, validate_toml_value, with_schema_header};
use crate::ui;
use crate::write::WriteOp;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::path::Path;

pub(super) fn load_toml_dir<T>(
    dir: &Path,
    load_one: impl Fn(&Path) -> Result<T, Diagnostic>,
    sort_items: impl Fn(&mut Vec<T>),
) -> Result<LoadResult<T>, Diagnostic> {
    if !dir.exists() {
        return Ok(LoadResult {
            items: vec![],
            warnings: vec![],
        });
    }

    let mut items = Vec::new();
    let mut warnings = Vec::new();
    let entries = std::fs::read_dir(dir)
        .map_err(|e| Diagnostic::io_error("read TOML directory", e, dir.display().to_string()))?;

    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().is_some_and(|ext| ext == "toml") {
            match load_one(&path) {
                Ok(item) => items.push(item),
                Err(e) => warnings.push(e),
            }
        }
    }

    sort_items(&mut items);
    Ok(LoadResult { items, warnings })
}

pub(super) fn load_toml_spec<T>(
    config: &Config,
    path: &Path,
    schema: ArtifactSchema,
    diagnostic_code: DiagnosticCode,
    invalid_toml_context: &str,
    invalid_structure_context: &str,
    prepare_schema_value: impl FnOnce(&mut toml::Value),
) -> Result<T, Diagnostic>
where
    T: DeserializeOwned,
{
    let content = std::fs::read_to_string(path)
        .map_err(|e| Diagnostic::io_error("read TOML file", e, path.display().to_string()))?;

    let raw: toml::Value = toml::from_str(&content).map_err(|e| {
        Diagnostic::new(
            diagnostic_code,
            format!("{invalid_toml_context}: {e}"),
            path.display().to_string(),
        )
    })?;
    let mut schema_raw = raw.clone();
    prepare_schema_value(&mut schema_raw);
    validate_toml_value(schema, config, path, &schema_raw)?;
    raw.try_into().map_err(|e| {
        Diagnostic::new(
            diagnostic_code,
            format!("{invalid_structure_context}: {e}"),
            path.display().to_string(),
        )
    })
}

pub(super) fn write_toml_spec<T>(
    path: &Path,
    schema: ArtifactSchema,
    spec: &T,
    op: WriteOp,
    display_path: Option<&Path>,
    serialize_context: &str,
) -> Result<(), Diagnostic>
where
    T: Serialize,
{
    let diagnostic_path = display_path.unwrap_or(path);
    let body = toml::to_string_pretty(spec).map_err(|e| {
        Diagnostic::new(
            DiagnosticCode::E0901IoError,
            format!("{serialize_context}: {e}"),
            diagnostic_path.display().to_string(),
        )
    })?;
    let content = with_schema_header(schema, &body);

    match op {
        WriteOp::Execute => {
            std::fs::write(path, &content).map_err(|e| {
                Diagnostic::io_error("write TOML file", e, diagnostic_path.display().to_string())
            })?;
        }
        WriteOp::Preview => {
            ui::dry_run_file_preview(diagnostic_path, &content);
        }
    }

    Ok(())
}