Skip to main content

prax_typegen/
lib.rs

1//! # prax-typegen
2//!
3//! Generate TypeScript interfaces and Zod schemas from Prax ORM schema files.
4//!
5//! This crate reads `.prax` schema files and produces TypeScript output that
6//! mirrors the models, enums, and composite types defined in the schema.
7//!
8//! ## Generator Block
9//!
10//! Add a generator block to your `.prax` schema:
11//!
12//! ```prax
13//! generator typescript {
14//!   provider = "prax-typegen"
15//!   output   = "./src/types"
16//!   generate = env("TYPESCRIPT_GENERATE")
17//! }
18//! ```
19//!
20//! When `TYPESCRIPT_GENERATE` is set to `true`, `1`, or `yes` in the
21//! environment, `prax generate` will invoke this generator.
22
23mod error;
24mod mapping;
25mod typescript;
26mod zod;
27
28pub use error::TypegenError;
29pub use mapping::TypeMapper;
30pub use typescript::InterfaceGenerator;
31pub use zod::ZodGenerator;
32
33use std::path::Path;
34
35use prax_schema::{Schema, validate_schema};
36
37/// Result type for typegen operations.
38pub type Result<T> = std::result::Result<T, TypegenError>;
39
40/// Top-level generator that produces both interfaces and Zod schemas.
41pub struct Typegen {
42    pub interfaces: bool,
43    pub zod: bool,
44}
45
46impl Default for Typegen {
47    fn default() -> Self {
48        Self {
49            interfaces: true,
50            zod: true,
51        }
52    }
53}
54
55impl Typegen {
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    pub fn interfaces_only() -> Self {
61        Self {
62            interfaces: true,
63            zod: false,
64        }
65    }
66
67    pub fn zod_only() -> Self {
68        Self {
69            interfaces: false,
70            zod: true,
71        }
72    }
73
74    /// Parse a `.prax` schema file and generate TypeScript output.
75    pub fn generate_from_file(&self, schema_path: &Path) -> Result<GeneratedOutput> {
76        let input = std::fs::read_to_string(schema_path).map_err(|e| {
77            TypegenError::Io(format!("failed to read {}: {e}", schema_path.display()))
78        })?;
79        self.generate_from_str(&input)
80    }
81
82    /// Generate TypeScript output from a schema string.
83    pub fn generate_from_str(&self, input: &str) -> Result<GeneratedOutput> {
84        let schema = validate_schema(input).map_err(|e| TypegenError::Schema(e.to_string()))?;
85        self.generate(&schema)
86    }
87
88    /// Generate TypeScript output from a parsed schema.
89    pub fn generate(&self, schema: &Schema) -> Result<GeneratedOutput> {
90        let mut output = GeneratedOutput::default();
91
92        if self.interfaces {
93            output.interfaces = InterfaceGenerator::generate(schema);
94        }
95        if self.zod {
96            output.zod = ZodGenerator::generate(schema);
97        }
98
99        Ok(output)
100    }
101
102    /// Generate and write files to the output directory.
103    pub fn write_to_dir(&self, schema: &Schema, out_dir: &Path) -> Result<Vec<String>> {
104        std::fs::create_dir_all(out_dir).map_err(|e| {
105            TypegenError::Io(format!(
106                "failed to create output dir {}: {e}",
107                out_dir.display()
108            ))
109        })?;
110
111        let output = self.generate(schema)?;
112        let mut written = Vec::new();
113
114        if self.interfaces && !output.interfaces.is_empty() {
115            let path = out_dir.join("models.ts");
116            std::fs::write(&path, &output.interfaces).map_err(|e| {
117                TypegenError::Io(format!("failed to write {}: {e}", path.display()))
118            })?;
119            written.push(path.display().to_string());
120        }
121
122        if self.zod && !output.zod.is_empty() {
123            let path = out_dir.join("schemas.ts");
124            std::fs::write(&path, &output.zod).map_err(|e| {
125                TypegenError::Io(format!("failed to write {}: {e}", path.display()))
126            })?;
127            written.push(path.display().to_string());
128        }
129
130        if self.interfaces || self.zod {
131            let index = build_index(self.interfaces, self.zod);
132            let path = out_dir.join("index.ts");
133            std::fs::write(&path, index).map_err(|e| {
134                TypegenError::Io(format!("failed to write {}: {e}", path.display()))
135            })?;
136            written.push(path.display().to_string());
137        }
138
139        Ok(written)
140    }
141}
142
143/// Generated TypeScript output.
144#[derive(Debug, Default)]
145pub struct GeneratedOutput {
146    /// TypeScript interfaces.
147    pub interfaces: String,
148    /// Zod schemas.
149    pub zod: String,
150}
151
152fn build_index(interfaces: bool, zod: bool) -> String {
153    let mut lines = vec![
154        "// Auto-generated by prax-typegen. Do not edit.".to_string(),
155        String::new(),
156    ];
157    if interfaces {
158        lines.push("export * from './models';".to_string());
159    }
160    if zod {
161        lines.push("export * from './schemas';".to_string());
162    }
163    lines.push(String::new());
164    lines.join("\n")
165}
166
167/// Check the schema for a generator named `name` and return whether it's enabled.
168pub fn generator_enabled(schema: &Schema, name: &str) -> bool {
169    schema.get_generator(name).is_some_and(|g| g.is_enabled())
170}
171
172/// Resolve the output directory from the generator block, falling back to a default.
173pub fn resolve_output_dir(schema: &Schema, generator_name: &str, fallback: &str) -> String {
174    schema
175        .get_generator(generator_name)
176        .and_then(|g| g.output.as_ref())
177        .map(|s| s.to_string())
178        .unwrap_or_else(|| fallback.to_string())
179}