raw
1use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
2
3static CWD: LazyLock<PathBuf> = LazyLock::new(|| {
4 std::env::current_dir()
5 .expect("failed to get current working directory")
6 .canonicalize()
7 .expect("failed to canonicalize current working directory")
8});
9
10#[derive(Debug, miette::Diagnostic, thiserror::Error)]
11pub enum ConfigError {
12 #[error("failed to read config file")]
13 ReadFile(#[source] std::io::Error),
14 #[error("failed to deserialize config file")]
15 Deserialize(#[source] serde_toml::de::Error),
16 #[error("failed to create missing directory")]
17 CreateDir(#[source] std::io::Error),
18 #[error("failed to canonicalize config's file paths ({1})")]
19 Canonicalize(#[source] std::io::Error, PathBuf),
20}
21
22#[derive(Debug, Clone, serde::Deserialize)]
23#[serde(default)]
24pub struct Config {
25 pub build: PathBuf,
26 pub r#static: PathBuf,
27 pub templates: PathBuf,
28 pub vault: PathBuf,
29
30 pub port: u16,
31
32 pub default_template: Option<String>,
33
34 pub assets: Vec<AssetConfig>,
35
36 pub modules: ModulesConfig,
37
38 pub pages: Vec<PageConfig>,
39
40 pub data: Option<serde_json::Value>,
41}
42
43impl Config {
44 pub fn load() -> Result<Self, ConfigError> {
45 let path = CWD.join("sable.toml");
46
47 let config = if path.exists() {
48 let config = std::fs::read_to_string(path).map_err(ConfigError::ReadFile)?;
49
50 let mut config: Self =
51 serde_toml::from_str(&config).map_err(ConfigError::Deserialize)?;
52
53 config.canonicalize()?;
54
55 config
56 } else {
57 Self::default()
58 };
59
60 Ok(config)
61 }
62
63 fn canonicalize(&mut self) -> Result<(), ConfigError> {
64 std::fs::create_dir_all(&self.build).map_err(ConfigError::CreateDir)?;
65
66 self.build = self
67 .build
68 .canonicalize()
69 .map_err(|err| ConfigError::Canonicalize(err, self.build.clone()))?;
70 self.r#static = self
71 .r#static
72 .canonicalize()
73 .map_err(|err| ConfigError::Canonicalize(err, self.r#static.clone()))?;
74 self.templates = self
75 .templates
76 .canonicalize()
77 .map_err(|err| ConfigError::Canonicalize(err, self.templates.clone()))?;
78 self.vault = self
79 .vault
80 .canonicalize()
81 .map_err(|err| ConfigError::Canonicalize(err, self.vault.clone()))?;
82
83 Ok(())
84 }
85}
86
87impl Default for Config {
88 fn default() -> Self {
89 Self {
90 build: CWD.join("dist"),
91 r#static: CWD.join("static"),
92 templates: CWD.join("templates"),
93 vault: CWD.join("content"),
94
95 port: 3000,
96
97 default_template: None,
98
99 assets: Vec::new(),
100
101 modules: ModulesConfig::default(),
102
103 pages: Vec::new(),
104
105 data: None,
106 }
107 }
108}
109
110#[derive(Debug, Clone, serde::Deserialize)]
111pub struct AssetConfig {
112 pub output: PathBuf,
113 pub command: String,
114 #[serde(default)]
115 pub env: Option<HashMap<String, String>>,
116}
117
118#[derive(Debug, Clone, Default, serde::Deserialize)]
119#[serde(default)]
120pub struct ModulesConfig {
121 pub rss: RssConfig,
122}
123
124#[derive(Debug, Clone, Default, serde::Deserialize)]
125#[serde(default)]
126pub struct RssConfig {
127 pub enabled: bool,
128 pub template: String,
129 pub path: String,
130}
131
132#[derive(Debug, Clone, Default, serde::Deserialize)]
133#[serde(default)]
134pub struct PageConfig {
135 pub template: String,
136 pub path: String,
137 pub metadata: Option<serde_json::Value>,
138}
139