Skip to main content

prax_schema/ast/
generator.rs

1//! Generator definitions for the Prax schema AST.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use smol_str::SmolStr;
6
7use super::Span;
8
9/// A generator block in the schema.
10///
11/// Generators define code generation targets that are invoked by `prax generate`.
12///
13/// ```prax
14/// generator typescript {
15///   provider = "prax-typegen"
16///   output   = "./src/types"
17///   generate = env("TYPESCRIPT_GENERATE")
18/// }
19/// ```
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct Generator {
22    /// Generator name (the identifier after `generator`).
23    pub name: SmolStr,
24    /// The provider binary or crate name.
25    pub provider: Option<SmolStr>,
26    /// Output directory for generated files.
27    pub output: Option<SmolStr>,
28    /// Whether generation is enabled. Resolves `env()` calls at runtime.
29    pub generate: GeneratorToggle,
30    /// Additional key-value properties.
31    pub properties: IndexMap<SmolStr, GeneratorValue>,
32    /// Source location.
33    pub span: Span,
34}
35
36impl Generator {
37    pub fn new(name: impl Into<SmolStr>, span: Span) -> Self {
38        Self {
39            name: name.into(),
40            provider: None,
41            output: None,
42            generate: GeneratorToggle::Always,
43            properties: IndexMap::new(),
44            span,
45        }
46    }
47
48    /// Check whether this generator should run, resolving env vars.
49    pub fn is_enabled(&self) -> bool {
50        match &self.generate {
51            GeneratorToggle::Always => true,
52            GeneratorToggle::Never => false,
53            GeneratorToggle::Literal(val) => *val,
54            GeneratorToggle::Env(var_name) => std::env::var(var_name)
55                .map(|v| {
56                    let v = v.trim().to_lowercase();
57                    v == "true" || v == "1" || v == "yes"
58                })
59                .unwrap_or(false),
60        }
61    }
62
63    pub fn name(&self) -> &str {
64        &self.name
65    }
66}
67
68/// Controls whether a generator runs.
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub enum GeneratorToggle {
71    /// Always run (no `generate` property specified).
72    Always,
73    /// Never run.
74    Never,
75    /// Literal boolean value: `generate = true` or `generate = false`.
76    Literal(bool),
77    /// Environment variable: `generate = env("TYPESCRIPT_GENERATE")`.
78    Env(SmolStr),
79}
80
81/// A value in a generator property.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub enum GeneratorValue {
84    /// A string literal.
85    String(SmolStr),
86    /// A boolean literal.
87    Bool(bool),
88    /// An environment variable reference.
89    Env(SmolStr),
90    /// An identifier (unquoted value).
91    Ident(SmolStr),
92}
93
94impl GeneratorValue {
95    /// Resolve this value to a string, reading env vars as needed.
96    pub fn resolve(&self) -> Option<String> {
97        match self {
98            Self::String(s) => Some(s.to_string()),
99            Self::Bool(b) => Some(b.to_string()),
100            Self::Ident(s) => Some(s.to_string()),
101            Self::Env(var) => std::env::var(var.as_str()).ok(),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn span() -> Span {
111        Span::new(0, 10)
112    }
113
114    #[test]
115    fn test_generator_new() {
116        let g = Generator::new("typescript", span());
117        assert_eq!(g.name(), "typescript");
118        assert!(g.provider.is_none());
119        assert!(g.output.is_none());
120        assert!(g.is_enabled());
121    }
122
123    #[test]
124    fn test_generator_toggle_always() {
125        let g = Generator::new("test", span());
126        assert!(g.is_enabled());
127    }
128
129    #[test]
130    fn test_generator_toggle_literal_true() {
131        let mut g = Generator::new("test", span());
132        g.generate = GeneratorToggle::Literal(true);
133        assert!(g.is_enabled());
134    }
135
136    #[test]
137    fn test_generator_toggle_literal_false() {
138        let mut g = Generator::new("test", span());
139        g.generate = GeneratorToggle::Literal(false);
140        assert!(!g.is_enabled());
141    }
142
143    #[test]
144    fn test_generator_toggle_never() {
145        let mut g = Generator::new("test", span());
146        g.generate = GeneratorToggle::Never;
147        assert!(!g.is_enabled());
148    }
149
150    #[test]
151    fn test_generator_toggle_env_true() {
152        unsafe { std::env::set_var("PRAX_TEST_GEN_TOGGLE", "true") };
153        let mut g = Generator::new("test", span());
154        g.generate = GeneratorToggle::Env("PRAX_TEST_GEN_TOGGLE".into());
155        assert!(g.is_enabled());
156        unsafe { std::env::remove_var("PRAX_TEST_GEN_TOGGLE") };
157    }
158
159    #[test]
160    fn test_generator_toggle_env_false() {
161        unsafe { std::env::set_var("PRAX_TEST_GEN_TOGGLE_F", "false") };
162        let mut g = Generator::new("test", span());
163        g.generate = GeneratorToggle::Env("PRAX_TEST_GEN_TOGGLE_F".into());
164        assert!(!g.is_enabled());
165        unsafe { std::env::remove_var("PRAX_TEST_GEN_TOGGLE_F") };
166    }
167
168    #[test]
169    fn test_generator_toggle_env_missing() {
170        let mut g = Generator::new("test", span());
171        g.generate = GeneratorToggle::Env("PRAX_TEST_NONEXISTENT_VAR_999".into());
172        assert!(!g.is_enabled());
173    }
174
175    #[test]
176    fn test_generator_toggle_env_one() {
177        unsafe { std::env::set_var("PRAX_TEST_GEN_ONE", "1") };
178        let mut g = Generator::new("test", span());
179        g.generate = GeneratorToggle::Env("PRAX_TEST_GEN_ONE".into());
180        assert!(g.is_enabled());
181        unsafe { std::env::remove_var("PRAX_TEST_GEN_ONE") };
182    }
183
184    #[test]
185    fn test_generator_value_resolve_string() {
186        let v = GeneratorValue::String("hello".into());
187        assert_eq!(v.resolve(), Some("hello".to_string()));
188    }
189
190    #[test]
191    fn test_generator_value_resolve_bool() {
192        let v = GeneratorValue::Bool(true);
193        assert_eq!(v.resolve(), Some("true".to_string()));
194    }
195
196    #[test]
197    fn test_generator_value_resolve_env() {
198        unsafe { std::env::set_var("PRAX_TEST_VAL", "resolved") };
199        let v = GeneratorValue::Env("PRAX_TEST_VAL".into());
200        assert_eq!(v.resolve(), Some("resolved".to_string()));
201        unsafe { std::env::remove_var("PRAX_TEST_VAL") };
202    }
203
204    #[test]
205    fn test_generator_value_resolve_env_missing() {
206        let v = GeneratorValue::Env("PRAX_TEST_MISSING_VAL_999".into());
207        assert_eq!(v.resolve(), None);
208    }
209
210    #[test]
211    fn test_parse_generator_block() {
212        use crate::parse_schema;
213
214        let schema = parse_schema(
215            r#"
216            generator typescript {
217                provider = "prax-typegen"
218                output   = "./src/types"
219            }
220            "#,
221        )
222        .unwrap();
223
224        assert_eq!(schema.generators.len(), 1);
225        let g = schema.get_generator("typescript").unwrap();
226        assert_eq!(g.provider.as_deref(), Some("prax-typegen"));
227        assert_eq!(g.output.as_deref(), Some("./src/types"));
228        assert!(g.is_enabled());
229    }
230
231    #[test]
232    fn test_parse_generator_with_env_toggle() {
233        use crate::parse_schema;
234
235        let schema = parse_schema(
236            r#"
237            generator typescript {
238                provider = "prax-typegen"
239                output   = "./src/types"
240                generate = env("PRAX_TEST_PARSE_GEN_TOGGLE")
241            }
242            "#,
243        )
244        .unwrap();
245
246        let g = schema.get_generator("typescript").unwrap();
247        assert_eq!(
248            g.generate,
249            GeneratorToggle::Env("PRAX_TEST_PARSE_GEN_TOGGLE".into())
250        );
251
252        assert!(!g.is_enabled());
253
254        unsafe { std::env::set_var("PRAX_TEST_PARSE_GEN_TOGGLE", "true") };
255        assert!(g.is_enabled());
256        unsafe { std::env::remove_var("PRAX_TEST_PARSE_GEN_TOGGLE") };
257    }
258
259    #[test]
260    fn test_parse_generator_with_bool_toggle() {
261        use crate::parse_schema;
262
263        let schema = parse_schema(
264            r#"
265            generator disabled {
266                provider = "some-provider"
267                generate = false
268            }
269            "#,
270        )
271        .unwrap();
272
273        let g = schema.get_generator("disabled").unwrap();
274        assert_eq!(g.generate, GeneratorToggle::Literal(false));
275        assert!(!g.is_enabled());
276    }
277
278    #[test]
279    fn test_parse_multiple_generators() {
280        use crate::parse_schema;
281
282        let schema = parse_schema(
283            r#"
284            generator typescript {
285                provider = "prax-typegen"
286                output   = "./ts"
287            }
288
289            generator python {
290                provider = "prax-pygen"
291                output   = "./py"
292                generate = env("PYTHON_GENERATE")
293            }
294            "#,
295        )
296        .unwrap();
297
298        assert_eq!(schema.generators.len(), 2);
299        assert!(schema.get_generator("typescript").is_some());
300        assert!(schema.get_generator("python").is_some());
301    }
302
303    #[test]
304    fn test_enabled_generators_filters() {
305        use crate::parse_schema;
306
307        let schema = parse_schema(
308            r#"
309            generator enabled_one {
310                provider = "a"
311                generate = true
312            }
313
314            generator disabled_one {
315                provider = "b"
316                generate = false
317            }
318            "#,
319        )
320        .unwrap();
321
322        let enabled = schema.enabled_generators();
323        assert_eq!(enabled.len(), 1);
324        assert_eq!(enabled[0].name(), "enabled_one");
325    }
326
327    #[test]
328    fn test_parse_generator_extra_properties() {
329        use crate::parse_schema;
330
331        let schema = parse_schema(
332            r#"
333            generator typescript {
334                provider    = "prax-typegen"
335                output      = "./src/types"
336                emitZod     = true
337                packageName = "@myapp/types"
338            }
339            "#,
340        )
341        .unwrap();
342
343        let g = schema.get_generator("typescript").unwrap();
344        assert_eq!(
345            g.properties.get("emitZod"),
346            Some(&GeneratorValue::Bool(true))
347        );
348        assert_eq!(
349            g.properties.get("packageName"),
350            Some(&GeneratorValue::String("@myapp/types".into()))
351        );
352    }
353}