1use std::path::Path;
6
7use serde::Deserialize;
8
9#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
11#[serde(default)]
12pub struct Features {
13 pub graph: bool,
15 pub math: bool,
17 pub mermaid: bool,
19 pub search: bool,
21}
22
23impl Default for Features {
24 fn default() -> Self {
25 Self {
26 graph: true,
27 math: true,
28 mermaid: true,
29 search: true,
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
36#[serde(default)]
37pub struct ComponentsConfig {
38 pub dir: String,
40}
41
42impl Default for ComponentsConfig {
43 fn default() -> Self {
44 Self {
45 dir: "components".to_string(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
52#[serde(default)]
53pub struct SiteConfig {
54 pub title: Option<String>,
57 pub base: String,
62 pub features: Features,
63 pub components: ComponentsConfig,
64}
65
66#[derive(Debug, thiserror::Error)]
67pub enum ConfigError {
68 #[error("reading {path}: {source}")]
69 Io {
70 path: String,
71 #[source]
72 source: std::io::Error,
73 },
74 #[error("parsing {path}: {source}")]
75 Parse {
76 path: String,
77 #[source]
78 source: toml::de::Error,
79 },
80}
81
82pub fn load(project_root: &Path) -> Result<SiteConfig, ConfigError> {
85 let path = project_root.join("docgen.toml");
86 let text = match std::fs::read_to_string(&path) {
87 Ok(t) => t,
88 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(SiteConfig::default()),
89 Err(e) => {
90 return Err(ConfigError::Io {
91 path: path.display().to_string(),
92 source: e,
93 })
94 }
95 };
96 toml::from_str(&text).map_err(|e| ConfigError::Parse {
97 path: path.display().to_string(),
98 source: e,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn default_is_pre_p6_behaviour() {
108 let c = SiteConfig::default();
109 assert_eq!(c.title, None);
110 assert_eq!(c.base, "");
111 assert!(c.features.graph && c.features.math && c.features.mermaid && c.features.search);
112 assert_eq!(c.components.dir, "components");
113 }
114
115 #[test]
116 fn missing_file_yields_default() {
117 let dir = tempfile::tempdir().unwrap();
118 assert_eq!(load(dir.path()).unwrap(), SiteConfig::default());
119 }
120
121 #[test]
122 fn parses_title_base_and_feature_toggles() {
123 let dir = tempfile::tempdir().unwrap();
124 std::fs::write(
125 dir.path().join("docgen.toml"),
126 "title = \"My Docs\"\nbase = \"/docs\"\n[features]\ngraph = false\nmermaid = false\n",
127 )
128 .unwrap();
129 let c = load(dir.path()).unwrap();
130 assert_eq!(c.title.as_deref(), Some("My Docs"));
131 assert_eq!(c.base, "/docs");
132 assert!(!c.features.graph);
133 assert!(!c.features.mermaid);
134 assert!(c.features.math);
136 assert!(c.features.search);
137 }
138
139 #[test]
140 fn partial_features_table_keeps_other_defaults() {
141 let dir = tempfile::tempdir().unwrap();
142 std::fs::write(
143 dir.path().join("docgen.toml"),
144 "[features]\nsearch = false\n",
145 )
146 .unwrap();
147 let c = load(dir.path()).unwrap();
148 assert!(!c.features.search);
149 assert!(c.features.graph);
150 }
151
152 #[test]
153 fn malformed_toml_is_an_error() {
154 let dir = tempfile::tempdir().unwrap();
155 std::fs::write(dir.path().join("docgen.toml"), "title = = =\n").unwrap();
156 assert!(load(dir.path()).is_err());
157 }
158}