1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct GlobalConfig {
8 #[serde(default)]
9 pub defaults: SiteDefaults,
10 #[serde(default)]
11 pub registry: RegistryConfig,
12 #[serde(default)]
13 pub custom: BTreeMap<String, String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SiteDefaults {
18 #[serde(default = "default_author")]
19 pub author: String,
20 #[serde(default = "default_language")]
21 pub language: String,
22 #[serde(default = "default_theme")]
23 pub theme: String,
24 #[serde(default = "default_posts_per_page")]
25 pub posts_per_page: usize,
26 #[serde(default = "default_excerpt_length")]
27 pub excerpt_length: usize,
28 #[serde(default = "default_dev_port")]
29 pub dev_port: u16,
30 #[serde(default = "default_prod_port")]
31 pub prod_port: u16,
32}
33
34impl Default for SiteDefaults {
35 fn default() -> Self {
36 Self {
37 author: default_author(),
38 language: default_language(),
39 theme: default_theme(),
40 posts_per_page: default_posts_per_page(),
41 excerpt_length: default_excerpt_length(),
42 dev_port: default_dev_port(),
43 prod_port: default_prod_port(),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct RegistryConfig {
50 #[serde(default = "default_auto_port_range_start")]
51 pub auto_port_range_start: u16,
52 #[serde(default = "default_auto_port_range_end")]
53 pub auto_port_range_end: u16,
54}
55
56impl Default for RegistryConfig {
57 fn default() -> Self {
58 Self {
59 auto_port_range_start: default_auto_port_range_start(),
60 auto_port_range_end: default_auto_port_range_end(),
61 }
62 }
63}
64
65fn default_author() -> String {
66 whoami::username()
67}
68
69fn default_language() -> String {
70 "en".to_string()
71}
72
73fn default_theme() -> String {
74 "default".to_string()
75}
76
77fn default_posts_per_page() -> usize {
78 10
79}
80
81fn default_excerpt_length() -> usize {
82 200
83}
84
85fn default_dev_port() -> u16 {
86 3000
87}
88
89fn default_prod_port() -> u16 {
90 8080
91}
92
93fn default_auto_port_range_start() -> u16 {
94 3001
95}
96
97fn default_auto_port_range_end() -> u16 {
98 3100
99}
100
101impl GlobalConfig {
102 pub fn load(path: &Path) -> Result<Self> {
103 if !path.exists() {
104 return Ok(Self::default());
105 }
106
107 let content = std::fs::read_to_string(path)
108 .with_context(|| format!("Failed to read config from {}", path.display()))?;
109 let config: GlobalConfig = toml::from_str(&content)
110 .with_context(|| format!("Failed to parse config from {}", path.display()))?;
111 Ok(config)
112 }
113
114 pub fn save(&self, path: &Path) -> Result<()> {
115 let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
116 if let Some(parent) = path.parent() {
117 std::fs::create_dir_all(parent)?;
118 }
119 std::fs::write(path, content)
120 .with_context(|| format!("Failed to write config to {}", path.display()))?;
121 Ok(())
122 }
123
124 pub fn get(&self, key: &str) -> Option<String> {
125 let parts: Vec<&str> = key.split('.').collect();
126 match parts.as_slice() {
127 ["defaults", "author"] => Some(self.defaults.author.clone()),
128 ["defaults", "language"] => Some(self.defaults.language.clone()),
129 ["defaults", "theme"] => Some(self.defaults.theme.clone()),
130 ["defaults", "posts_per_page"] => Some(self.defaults.posts_per_page.to_string()),
131 ["defaults", "excerpt_length"] => Some(self.defaults.excerpt_length.to_string()),
132 ["defaults", "dev_port"] => Some(self.defaults.dev_port.to_string()),
133 ["defaults", "prod_port"] => Some(self.defaults.prod_port.to_string()),
134 ["registry", "auto_port_range_start"] => {
135 Some(self.registry.auto_port_range_start.to_string())
136 }
137 ["registry", "auto_port_range_end"] => {
138 Some(self.registry.auto_port_range_end.to_string())
139 }
140 ["custom", k] => self.custom.get(*k).cloned(),
141 _ => None,
142 }
143 }
144
145 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
146 let parts: Vec<&str> = key.split('.').collect();
147 match parts.as_slice() {
148 ["defaults", "author"] => self.defaults.author = value.to_string(),
149 ["defaults", "language"] => self.defaults.language = value.to_string(),
150 ["defaults", "theme"] => self.defaults.theme = value.to_string(),
151 ["defaults", "posts_per_page"] => {
152 self.defaults.posts_per_page = value.parse().context("Invalid number")?
153 }
154 ["defaults", "excerpt_length"] => {
155 self.defaults.excerpt_length = value.parse().context("Invalid number")?
156 }
157 ["defaults", "dev_port"] => {
158 self.defaults.dev_port = value.parse().context("Invalid port")?
159 }
160 ["defaults", "prod_port"] => {
161 self.defaults.prod_port = value.parse().context("Invalid port")?
162 }
163 ["registry", "auto_port_range_start"] => {
164 self.registry.auto_port_range_start = value.parse().context("Invalid port")?
165 }
166 ["registry", "auto_port_range_end"] => {
167 self.registry.auto_port_range_end = value.parse().context("Invalid port")?
168 }
169 ["custom", k] => {
170 self.custom.insert(k.to_string(), value.to_string());
171 }
172 _ => anyhow::bail!("Unknown config key: {}", key),
173 }
174 Ok(())
175 }
176
177 pub fn list(&self) -> Vec<(String, String)> {
178 let mut items = vec![
179 ("defaults.author".to_string(), self.defaults.author.clone()),
180 (
181 "defaults.language".to_string(),
182 self.defaults.language.clone(),
183 ),
184 ("defaults.theme".to_string(), self.defaults.theme.clone()),
185 (
186 "defaults.posts_per_page".to_string(),
187 self.defaults.posts_per_page.to_string(),
188 ),
189 (
190 "defaults.excerpt_length".to_string(),
191 self.defaults.excerpt_length.to_string(),
192 ),
193 (
194 "defaults.dev_port".to_string(),
195 self.defaults.dev_port.to_string(),
196 ),
197 (
198 "defaults.prod_port".to_string(),
199 self.defaults.prod_port.to_string(),
200 ),
201 (
202 "registry.auto_port_range_start".to_string(),
203 self.registry.auto_port_range_start.to_string(),
204 ),
205 (
206 "registry.auto_port_range_end".to_string(),
207 self.registry.auto_port_range_end.to_string(),
208 ),
209 ];
210
211 for (k, v) in &self.custom {
212 items.push((format!("custom.{}", k), v.clone()));
213 }
214
215 items
216 }
217
218 pub fn remove(&mut self, key: &str) -> Result<bool> {
219 let parts: Vec<&str> = key.split('.').collect();
220 match parts.as_slice() {
221 ["custom", k] => Ok(self.custom.remove(*k).is_some()),
222 _ => anyhow::bail!("Can only remove custom.* keys"),
223 }
224 }
225}