Skip to main content

pebble_cms/global/
config.rs

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}