use super::convert::{ManifestData, build_manifest_document, manifest_from_data};
use crate::config::Lint;
use crate::domain::Parsed;
use crate::domain::diff::ManifestDiff;
use crate::domain::manifest::Manifest;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
pub const MANIFEST_FILE_NAME: &str = "gx.toml";
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to read manifest file: {}", path.display())]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse manifest file: {}", path.display())]
Parse {
path: PathBuf,
#[source]
source: Box<toml::de::Error>,
},
#[error("failed to write manifest file: {}", path.display())]
Write {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to serialize manifest to TOML")]
Serialize(#[source] toml::ser::Error),
#[error("invalid manifest: {0}")]
Validation(String),
}
pub struct Store {
path: PathBuf,
}
impl Store {
#[must_use]
pub fn new(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
}
}
}
#[expect(
clippy::multiple_inherent_impl,
reason = "constructor and methods are in separate impl blocks for readability"
)]
impl Store {
pub fn save(&self, manifest: &Manifest) -> Result<(), Error> {
let doc = build_manifest_document(manifest);
fs::write(&self.path, doc.to_string()).map_err(|source| Error::Write {
path: self.path.clone(),
source,
})?;
Ok(())
}
}
pub fn parse(path: &Path) -> Result<Parsed<Manifest>, Error> {
if !path.exists() {
return Ok(Parsed {
value: Manifest::default(),
migrated: false,
});
}
let content = fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_path_buf(),
source,
})?;
let data: ManifestData = toml::from_str(&content).map_err(|source| Error::Parse {
path: path.to_path_buf(),
source: Box::new(source),
})?;
let has_gx_section = data.gx.is_some();
if has_gx_section {
let manifest = manifest_from_data(data, path, true)?;
Ok(Parsed {
value: manifest,
migrated: true,
})
} else {
let is_v1 = data.actions.versions.values().any(|v| {
v.starts_with('v')
&& v.get(1..)
.and_then(|s| s.chars().next())
.is_some_and(|c| c.is_ascii_digit())
});
let manifest = manifest_from_data(data, path, !is_v1)?;
Ok(Parsed {
value: manifest,
migrated: is_v1,
})
}
}
pub fn parse_lint_config(path: &Path) -> Result<Lint, Error> {
if !path.exists() {
return Ok(Lint::default());
}
let content = fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_path_buf(),
source,
})?;
let data: ManifestData = toml::from_str(&content).map_err(|source| Error::Parse {
path: path.to_path_buf(),
source: Box::new(source),
})?;
Ok(Lint {
rules: data.lint.rules,
})
}
pub fn create(path: &Path, diff: &ManifestDiff) -> Result<(), Error> {
let mut manifest = Manifest::default();
for (id, version) in &diff.added {
manifest.set(id.clone(), version.clone());
}
for (id, ovr) in &diff.overrides_added {
manifest.add_override(id.clone(), ovr.clone());
}
let doc = build_manifest_document(&manifest);
fs::write(path, doc.to_string()).map_err(|source| Error::Write {
path: path.to_path_buf(),
source,
})?;
Ok(())
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::assertions_on_result_states,
reason = "tests use unwrap, indexing, and other patterns freely"
)]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests use unwrap, indexing, and other patterns freely"
)]
#[path = "override_scope_tests.rs"]
mod override_scope_tests;