use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode};
use crate::model::{
AdrEntry, AdrSpec, GuardEntry, GuardSpec, ReleasesFile, WorkItemEntry, WorkItemSpec,
};
use crate::schema::{ArtifactSchema, validate_toml_value, with_schema_header};
use crate::ui;
use crate::write::WriteOp;
use std::path::Path;
pub struct LoadResult<T> {
pub items: Vec<T>,
pub warnings: Vec<Diagnostic>,
}
pub fn load_adrs(config: &Config) -> Result<Vec<AdrEntry>, Diagnostic> {
load_adrs_with_warnings(config).map(|r| r.items)
}
pub fn load_adrs_with_warnings(config: &Config) -> Result<LoadResult<AdrEntry>, Diagnostic> {
let adr_dir = config.adr_dir();
if !adr_dir.exists() {
return Ok(LoadResult {
items: vec![],
warnings: vec![],
});
}
let mut adrs = Vec::new();
let mut warnings = Vec::new();
let entries = std::fs::read_dir(&adr_dir).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
adr_dir.display().to_string(),
)
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "toml") {
match load_adr(config, &path) {
Ok(adr) => adrs.push(adr),
Err(e) => warnings.push(e),
}
}
}
adrs.sort_by(|a, b| a.spec.govctl.id.cmp(&b.spec.govctl.id));
Ok(LoadResult {
items: adrs,
warnings,
})
}
pub fn load_adr(config: &Config, path: &Path) -> Result<AdrEntry, Diagnostic> {
let content = std::fs::read_to_string(path).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
let raw: toml::Value = toml::from_str(&content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0301AdrSchemaInvalid,
format!("Invalid TOML: {e}"),
path.display().to_string(),
)
})?;
validate_toml_value(ArtifactSchema::Adr, config, path, &raw)?;
let spec: AdrSpec = raw.try_into().map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0301AdrSchemaInvalid,
format!("Invalid ADR structure: {e}"),
path.display().to_string(),
)
})?;
Ok(AdrEntry {
spec,
path: path.to_path_buf(),
})
}
pub fn write_adr(
path: &Path,
spec: &AdrSpec,
op: WriteOp,
display_path: Option<&Path>,
) -> Result<(), Diagnostic> {
let body = toml::to_string_pretty(spec).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
format!("Failed to serialize TOML: {e}"),
path.display().to_string(),
)
})?;
let content = with_schema_header(ArtifactSchema::Adr, &body);
match op {
WriteOp::Execute => {
std::fs::write(path, &content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
}
WriteOp::Preview => {
let output_path = display_path.unwrap_or(path);
ui::dry_run_file_preview(output_path, &content);
}
}
Ok(())
}
pub fn load_work_items(config: &Config) -> Result<Vec<WorkItemEntry>, Diagnostic> {
load_work_items_with_warnings(config).map(|r| r.items)
}
pub fn load_work_items_with_warnings(
config: &Config,
) -> Result<LoadResult<WorkItemEntry>, Diagnostic> {
let work_dir = config.work_dir();
if !work_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(&work_dir).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
work_dir.display().to_string(),
)
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "toml") {
match load_work_item(config, &path) {
Ok(item) => items.push(item),
Err(e) => warnings.push(e),
}
}
}
items.sort_by(|a, b| a.spec.govctl.id.cmp(&b.spec.govctl.id));
Ok(LoadResult { items, warnings })
}
#[allow(dead_code)]
pub fn load_guards(config: &Config) -> Result<Vec<GuardEntry>, Diagnostic> {
load_guards_with_warnings(config).map(|r| r.items)
}
pub fn load_guards_with_warnings(config: &Config) -> Result<LoadResult<GuardEntry>, Diagnostic> {
let guard_dir = config.guard_dir();
if !guard_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(&guard_dir).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
guard_dir.display().to_string(),
)
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "toml") {
match load_guard(config, &path) {
Ok(item) => items.push(item),
Err(e) => warnings.push(e),
}
}
}
items.sort_by(|a, b| a.spec.govctl.id.cmp(&b.spec.govctl.id));
Ok(LoadResult { items, warnings })
}
pub fn load_guard(config: &Config, path: &Path) -> Result<GuardEntry, Diagnostic> {
let content = std::fs::read_to_string(path).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
let raw: toml::Value = toml::from_str(&content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E1001GuardSchemaInvalid,
format!("Invalid TOML: {e}"),
path.display().to_string(),
)
})?;
validate_toml_value(ArtifactSchema::Guard, config, path, &raw)?;
let spec: GuardSpec = raw.try_into().map_err(|e| {
Diagnostic::new(
DiagnosticCode::E1001GuardSchemaInvalid,
format!("Invalid verification guard structure: {e}"),
path.display().to_string(),
)
})?;
Ok(GuardEntry {
spec,
path: path.to_path_buf(),
})
}
pub fn load_work_item(config: &Config, path: &Path) -> Result<WorkItemEntry, Diagnostic> {
let content = std::fs::read_to_string(path).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
let raw: toml::Value = toml::from_str(&content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0401WorkSchemaInvalid,
format!("Invalid TOML: {e}"),
path.display().to_string(),
)
})?;
validate_toml_value(ArtifactSchema::WorkItem, config, path, &raw)?;
let spec: WorkItemSpec = raw.try_into().map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0401WorkSchemaInvalid,
format!("Invalid work item structure: {e}"),
path.display().to_string(),
)
})?;
Ok(WorkItemEntry {
spec,
path: path.to_path_buf(),
})
}
pub fn write_work_item(
path: &Path,
spec: &WorkItemSpec,
op: WriteOp,
display_path: Option<&Path>,
) -> Result<(), Diagnostic> {
let body = toml::to_string_pretty(spec).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
format!("Failed to serialize TOML: {e}"),
path.display().to_string(),
)
})?;
let content = with_schema_header(ArtifactSchema::WorkItem, &body);
match op {
WriteOp::Execute => {
std::fs::write(path, &content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
}
WriteOp::Preview => {
let output_path = display_path.unwrap_or(path);
ui::dry_run_file_preview(output_path, &content);
}
}
Ok(())
}
pub fn write_guard(
path: &Path,
spec: &GuardSpec,
op: WriteOp,
display_path: Option<&Path>,
) -> Result<(), Diagnostic> {
let body = toml::to_string_pretty(spec).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
format!("Failed to serialize TOML: {e}"),
path.display().to_string(),
)
})?;
let content = with_schema_header(ArtifactSchema::Guard, &body);
match op {
WriteOp::Execute => {
std::fs::write(path, &content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
}
WriteOp::Preview => {
let output_path = display_path.unwrap_or(path);
ui::dry_run_file_preview(output_path, &content);
}
}
Ok(())
}
pub fn load_releases(config: &Config) -> Result<ReleasesFile, Diagnostic> {
let path = config.releases_path();
if !path.exists() {
return Ok(ReleasesFile::default());
}
let content = std::fs::read_to_string(&path).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path.display().to_string(),
)
})?;
let raw: toml::Value = toml::from_str(&content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0704ReleaseSchemaInvalid,
format!("Invalid releases.toml: {e}"),
path.display().to_string(),
)
})?;
validate_toml_value(ArtifactSchema::Release, config, &path, &raw)?;
let releases: ReleasesFile = raw.try_into().map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0704ReleaseSchemaInvalid,
format!("Invalid release structure: {e}"),
path.display().to_string(),
)
})?;
for release in &releases.releases {
semver::Version::parse(&release.version).map_err(|_| {
Diagnostic::new(
DiagnosticCode::E0701ReleaseInvalidSemver,
format!("Invalid semver version: {}", release.version),
path.display().to_string(),
)
})?;
}
Ok(releases)
}
pub fn validate_version(version: &str) -> Result<semver::Version, String> {
semver::Version::parse(version).map_err(|_| format!("Invalid semver: {version}"))
}
pub fn write_releases(
config: &Config,
releases: &ReleasesFile,
op: WriteOp,
) -> Result<(), Diagnostic> {
let path = config.releases_path();
let path_display = config.display_path(&path);
let body = toml::to_string_pretty(releases).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
format!("Failed to serialize releases: {e}"),
path_display.display().to_string(),
)
})?;
let content = with_schema_header(ArtifactSchema::Release, &body);
match op {
WriteOp::Execute => {
std::fs::write(&path, &content).map_err(|e| {
Diagnostic::new(
DiagnosticCode::E0901IoError,
e.to_string(),
path_display.display().to_string(),
)
})?;
}
WriteOp::Preview => {
ui::dry_run_file_preview(&path_display, &content);
}
}
Ok(())
}