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