use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Deserialize, Serialize)]
pub struct StrayMarkConfig {
#[serde(default = "default_language")]
pub language: String,
#[serde(default)]
pub complexity: ComplexityConfig,
#[serde(default = "default_regional_scope")]
pub regional_scope: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ComplexityConfig {
#[serde(default = "default_threshold")]
pub threshold: u32,
}
fn default_threshold() -> u32 {
8
}
impl Default for ComplexityConfig {
fn default() -> Self {
Self {
threshold: default_threshold(),
}
}
}
fn default_language() -> String {
"en".to_string()
}
fn default_regional_scope() -> Vec<String> {
vec!["global".to_string(), "eu".to_string()]
}
impl Default for StrayMarkConfig {
fn default() -> Self {
Self {
language: default_language(),
complexity: ComplexityConfig::default(),
regional_scope: default_regional_scope(),
}
}
}
impl StrayMarkConfig {
pub fn load(project_root: &Path) -> Result<Self> {
let config_path = project_root.join(".straymark/config.yml");
if !config_path.exists() {
return Ok(Self::default());
}
let contents =
std::fs::read_to_string(&config_path).context("Failed to read config.yml")?;
let config: Self = serde_yaml::from_str(&contents).context("Failed to parse config.yml")?;
Ok(config)
}
pub fn has_region(&self, region: &str) -> bool {
self.regional_scope
.iter()
.any(|r| r.eq_ignore_ascii_case(region))
}
pub fn resolve_language(project_root: &Path) -> String {
let config_path = project_root.join(".straymark/config.yml");
if config_path.exists() {
return Self::load(project_root)
.map(|c| c.language)
.unwrap_or_else(|_| default_language());
}
crate::utils::detect_os_locale().unwrap_or_else(default_language)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_regional_scope() {
let cfg = StrayMarkConfig::default();
assert!(cfg.has_region("global"));
assert!(cfg.has_region("eu"));
assert!(!cfg.has_region("china"));
}
#[test]
fn test_has_region_case_insensitive() {
let cfg = StrayMarkConfig {
regional_scope: vec!["China".into(), "GLOBAL".into()],
..Default::default()
};
assert!(cfg.has_region("china"));
assert!(cfg.has_region("CHINA"));
assert!(cfg.has_region("global"));
assert!(!cfg.has_region("eu"));
}
#[test]
fn resolve_language_uses_config_value_when_present() {
let tmp = tempfile::TempDir::new().unwrap();
let dt = tmp.path().join(".straymark");
std::fs::create_dir_all(&dt).unwrap();
std::fs::write(dt.join("config.yml"), "language: zh-CN\n").unwrap();
let prev = std::env::var("LANG").ok();
unsafe { std::env::set_var("LANG", "fr_FR.UTF-8"); }
let lang = StrayMarkConfig::resolve_language(tmp.path());
if let Some(p) = prev {
unsafe { std::env::set_var("LANG", p); }
} else {
unsafe { std::env::remove_var("LANG"); }
}
assert_eq!(lang, "zh-CN");
}
#[test]
fn resolve_language_falls_back_to_default_when_no_config_no_env() {
let tmp = tempfile::TempDir::new().unwrap();
let prev_all = std::env::var("LC_ALL").ok();
let prev_lang = std::env::var("LANG").ok();
unsafe {
std::env::remove_var("LC_ALL");
std::env::set_var("LANG", "C");
}
let lang = StrayMarkConfig::resolve_language(tmp.path());
unsafe {
if let Some(p) = prev_all {
std::env::set_var("LC_ALL", p);
}
if let Some(p) = prev_lang {
std::env::set_var("LANG", p);
} else {
std::env::remove_var("LANG");
}
}
assert_eq!(lang, "en");
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Checksums {
pub version: String,
pub files: std::collections::HashMap<String, String>,
}
impl Checksums {
pub fn load(project_root: &Path) -> Result<Self> {
let path = project_root.join(".straymark/.checksums.json");
if !path.exists() {
return Ok(Self::default());
}
let contents =
std::fs::read_to_string(&path).context("Failed to read .checksums.json")?;
let checksums: Self =
serde_json::from_str(&contents).context("Failed to parse .checksums.json")?;
Ok(checksums)
}
pub fn save(&self, project_root: &Path) -> Result<()> {
let path = project_root.join(".straymark/.checksums.json");
let contents =
serde_json::to_string_pretty(self).context("Failed to serialize checksums")?;
std::fs::write(&path, contents).context("Failed to write .checksums.json")?;
Ok(())
}
}