1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9pub const CONFIG_PATH: &str = ".hblank/config.toml";
10
11#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
12#[serde(default, deny_unknown_fields)]
13pub struct Config {
14 pub fixtures: Vec<String>,
15 pub ignore: Vec<String>,
16 pub window: WindowConfig,
17}
18
19impl Default for Config {
20 fn default() -> Self {
21 Self {
22 fixtures: vec!["src/**/*.hblank.rs".to_owned()],
23 ignore: vec!["target/**".to_owned(), ".hblank/**".to_owned()],
24 window: WindowConfig::default(),
25 }
26 }
27}
28
29#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
30#[serde(default, deny_unknown_fields)]
31pub struct WindowConfig {
32 pub title: String,
33 pub width: u32,
34 pub height: u32,
35}
36
37impl Default for WindowConfig {
38 fn default() -> Self {
39 Self {
40 title: "Hblank".to_owned(),
41 width: 1440,
42 height: 900,
43 }
44 }
45}
46
47impl Config {
48 #[must_use]
50 pub fn for_project(package_name: &str) -> Self {
51 let mut config = Self::default();
52 config.window.title = format!("{package_name} ยท Hblank");
53 config
54 }
55
56 pub fn load(project_root: &Path) -> Result<Self, ConfigError> {
61 let path = project_root.join(CONFIG_PATH);
62 let source = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
63 path: path.clone(),
64 source,
65 })?;
66 let config = toml::from_str::<Self>(&source).map_err(|source| ConfigError::Parse {
67 path: path.clone(),
68 source,
69 })?;
70 config.validate()?;
71 Ok(config)
72 }
73
74 pub fn to_toml(&self) -> Result<String, ConfigError> {
79 self.validate()?;
80 toml::to_string_pretty(self).map_err(ConfigError::Serialize)
81 }
82
83 fn validate(&self) -> Result<(), ConfigError> {
84 if self.fixtures.is_empty() || self.fixtures.iter().any(String::is_empty) {
85 return Err(ConfigError::NoFixtureFilePatterns);
86 }
87 if self.window.title.trim().is_empty() {
88 return Err(ConfigError::EmptyWindowTitle);
89 }
90 if self.window.width == 0 || self.window.height == 0 {
91 return Err(ConfigError::InvalidWindowSize {
92 width: self.window.width,
93 height: self.window.height,
94 });
95 }
96 Ok(())
97 }
98}
99
100#[derive(Debug, Error)]
101pub enum ConfigError {
102 #[error("could not read Hblank config at {path}: {source}")]
103 Read {
104 path: PathBuf,
105 source: std::io::Error,
106 },
107 #[error("could not parse Hblank config at {path}: {source}")]
108 Parse {
109 path: PathBuf,
110 source: toml::de::Error,
111 },
112 #[error("could not serialize Hblank config: {0}")]
113 Serialize(toml::ser::Error),
114 #[error("Hblank config must include at least one non-empty fixture file pattern")]
115 NoFixtureFilePatterns,
116 #[error("Hblank window title cannot be empty")]
117 EmptyWindowTitle,
118 #[error("Hblank window size must be positive, received {width}x{height}")]
119 InvalidWindowSize { width: u32, height: u32 },
120}