Skip to main content

docgen_config/
lib.rs

1//! Parses an optional `docgen.toml`. When absent, `SiteConfig::default()`
2//! reproduces docgen's pre-P6 hard-coded behaviour exactly, so a project with
3//! no config builds identically to before.
4
5use std::path::Path;
6
7use serde::Deserialize;
8
9/// Feature toggles. All default `true` — the pre-P6 behaviour (every feature on).
10#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
11#[serde(default)]
12pub struct Features {
13    /// Emit the `/graph/` page + its island.
14    pub graph: bool,
15    /// Render math (build-time KaTeX) + link its stylesheet.
16    pub math: bool,
17    /// Allow mermaid diagrams + lazy island.
18    pub mermaid: bool,
19    /// Emit the search index + search client.
20    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/// `[components]` section.
35#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
36#[serde(default)]
37pub struct ComponentsConfig {
38    /// Project-relative directory holding `<name>/template.html` components.
39    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/// The whole resolved site config. `Default` == pre-P6 behaviour.
51#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
52#[serde(default)]
53pub struct SiteConfig {
54    /// Optional site title; when set, page `<title>` becomes `"{page} — {title}"`
55    /// (home page uses just `title`). When `None`, per-page titles are unchanged.
56    pub title: Option<String>,
57    /// Base path for the deployed site (e.g. `/docs`). Empty = served at root
58    /// (unchanged behaviour). Prefixed onto every emitted asset/nav/wikilink URL
59    /// so a sub-path deployment resolves correctly (no `<base>` tag is used —
60    /// `<base>` only affects relative URLs, but our links are root-absolute).
61    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
82/// Load `docgen.toml` from `project_root`. Missing file → `SiteConfig::default()`
83/// (not an error). Present-but-malformed → `Err`.
84pub 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        // Unspecified toggles keep their default (true).
135        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}