Skip to main content

wowlab_docgen_cli/context/collectors/
env_vars.rs

1use serde::Serialize;
2
3use super::{ContextEntry, to_value};
4use crate::RenderCtx;
5
6/// An environment variable entry for template rendering.
7#[derive(Debug, Serialize)]
8pub struct EnvVar {
9    pub name: String,
10    pub default: String,
11}
12
13/// Collect environment variables from `.env.example` in the template directory.
14#[must_use]
15pub fn collect(ctx: &RenderCtx<'_>) -> Vec<ContextEntry> {
16    let env_path = ctx.dir.join(".env.example");
17    let Some(content) = ctx.workspace.contents(&env_path) else {
18        return Vec::new();
19    };
20
21    let vars: Vec<EnvVar> = content
22        .lines()
23        .filter(|line| {
24            let trimmed = line.trim();
25
26            !trimmed.is_empty() && !trimmed.starts_with('#')
27        })
28        .filter_map(|line| {
29            let (name, default) = if let Some(eq) = line.find('=') {
30                // BOUNDS: `eq` is a valid index returned by `find`.
31                let key = line[..eq].trim().to_string();
32                // BOUNDS: `=` is single-byte ASCII so eq+1 is a valid char boundary.
33                let val = line[eq + 1..].trim().to_string();
34
35                (key, val)
36            } else {
37                return None;
38            };
39
40            if name.is_empty() {
41                return None;
42            }
43
44            Some(EnvVar { name, default })
45        })
46        .collect();
47
48    if vars.is_empty() {
49        return Vec::new();
50    }
51
52    vec![("env_vars", to_value(&vars))]
53}
54
55#[cfg(test)]
56mod tests {
57    use googletest::prelude::*;
58
59    use super::*;
60
61    fn parse_lines(input: &str) -> Vec<EnvVar> {
62        input
63            .lines()
64            .filter(|line| {
65                let trimmed = line.trim();
66
67                !trimmed.is_empty() && !trimmed.starts_with('#')
68            })
69            .filter_map(|line| {
70                let (name, default) = if let Some(eq) = line.find('=') {
71                    (
72                        line[..eq].trim().to_string(),
73                        line[eq + 1..].trim().to_string(),
74                    )
75                } else {
76                    return None;
77                };
78
79                if name.is_empty() {
80                    return None;
81                }
82
83                Some(EnvVar { name, default })
84            })
85            .collect()
86    }
87
88    #[gtest]
89    fn parses_key_value_pairs() -> Result<()> {
90        let input = "FOO=bar\nBAZ=\n# comment\n\nQUX=hello world";
91        let vars = parse_lines(input);
92
93        verify_that!(vars, len(eq(3)))?;
94        verify_eq!(vars[0].name, "FOO")?;
95        verify_eq!(vars[0].default, "bar")?;
96        verify_eq!(vars[1].name, "BAZ")?;
97        verify_eq!(vars[1].default, "")?;
98        verify_eq!(vars[2].name, "QUX")?;
99
100        verify_eq!(vars[2].default, "hello world")
101    }
102
103    #[gtest]
104    fn ignores_comments_and_blanks() -> Result<()> {
105        let input = "# this is a comment\n\n  # indented comment\nKEY=val";
106        let vars = parse_lines(input);
107
108        verify_that!(vars, len(eq(1)))?;
109
110        verify_eq!(vars[0].name, "KEY")
111    }
112
113    #[gtest]
114    fn skips_lines_without_equals() -> Result<()> {
115        let input = "NO_EQUALS\nGOOD=yes";
116        let vars = parse_lines(input);
117
118        verify_that!(vars, len(eq(1)))?;
119
120        verify_eq!(vars[0].name, "GOOD")
121    }
122}