Skip to main content

prax_codegen/
lib.rs

1//! Procedural macros for the Prax ORM.
2//!
3//! This crate provides compile-time code generation for Prax, transforming
4//! schema definitions into type-safe Rust code.
5//!
6//! # Macros
7//!
8//! - [`prax_schema!`] - Generate models from a `.prax` schema file
9//! - [`Model`] - Derive macro for manual model definition
10//!
11//! # Plugins
12//!
13//! Code generation can be extended with plugins enabled via environment variables:
14//!
15//! ```bash
16//! # Enable debug information
17//! PRAX_PLUGIN_DEBUG=1 cargo build
18//!
19//! # Enable JSON Schema generation
20//! PRAX_PLUGIN_JSON_SCHEMA=1 cargo build
21//!
22//! # Enable GraphQL SDL generation
23//! PRAX_PLUGIN_GRAPHQL=1 cargo build
24//!
25//! # Enable custom serialization helpers
26//! PRAX_PLUGIN_SERDE=1 cargo build
27//!
28//! # Enable runtime validation
29//! PRAX_PLUGIN_VALIDATOR=1 cargo build
30//!
31//! # Enable all plugins
32//! PRAX_PLUGINS_ALL=1 cargo build
33//! ```
34//!
35//! # Example
36//!
37//! ```rust,ignore
38//! // Generate models from schema file
39//! prax::prax_schema!("schema.prax");
40//!
41//! // Or manually define with derive macro
42//! #[derive(prax::Model)]
43//! #[prax(table = "users")]
44//! struct User {
45//!     #[prax(id, auto)]
46//!     id: i32,
47//!     #[prax(unique)]
48//!     email: String,
49//!     name: Option<String>,
50//! }
51//! ```
52
53use proc_macro::TokenStream;
54use quote::quote;
55use syn::{DeriveInput, LitStr, parse_macro_input};
56
57mod generators;
58mod plugins;
59mod schema_reader;
60mod types;
61
62use generators::{
63    generate_enum_module, generate_model_module_with_style, generate_type_module,
64    generate_view_module,
65};
66
67/// Generate models from a Prax schema file.
68///
69/// This macro reads a `.prax` schema file at compile time and generates
70/// type-safe Rust code for all models, enums, and types defined in the schema.
71///
72/// # Example
73///
74/// ```rust,ignore
75/// prax::prax_schema!("schema.prax");
76///
77/// // Now you can use the generated types:
78/// let user = client.user().find_unique(user::id::equals(1)).exec().await?;
79/// ```
80///
81/// # Generated Code
82///
83/// For each model in the schema, this macro generates:
84/// - A module with the model name (snake_case)
85/// - A `Data` struct representing a row from the database
86/// - A `CreateInput` struct for creating new records
87/// - A `UpdateInput` struct for updating records
88/// - Field modules with filter operations (`equals`, `contains`, `in_`, etc.)
89/// - A `WhereParam` enum for type-safe filtering
90/// - An `OrderByParam` enum for sorting
91/// - Select and Include builders for partial queries
92#[proc_macro]
93pub fn prax_schema(input: TokenStream) -> TokenStream {
94    let input = parse_macro_input!(input as LitStr);
95    let schema_path = input.value();
96
97    match generate_from_schema(&schema_path) {
98        Ok(tokens) => tokens.into(),
99        Err(err) => {
100            let err_msg = err.to_string();
101            quote! {
102                compile_error!(#err_msg);
103            }
104            .into()
105        }
106    }
107}
108
109/// Derive macro for defining Prax models manually.
110///
111/// This derive macro allows you to define models in Rust code instead of
112/// using a `.prax` schema file. It generates the same query builder methods
113/// and type-safe operations.
114///
115/// # Attributes
116///
117/// ## Struct-level
118/// - `#[prax(table = "table_name")]` - Map to a different table name
119/// - `#[prax(schema = "schema_name")]` - Specify database schema
120///
121/// ## Field-level
122/// - `#[prax(id)]` - Mark as primary key
123/// - `#[prax(auto)]` - Auto-increment field
124/// - `#[prax(unique)]` - Unique constraint
125/// - `#[prax(default = value)]` - Default value
126/// - `#[prax(column = "col_name")]` - Map to different column
127/// - `#[prax(relation(...))]` - Define relation
128///
129/// # Example
130///
131/// ```rust,ignore
132/// #[derive(prax::Model)]
133/// #[prax(table = "users")]
134/// struct User {
135///     #[prax(id, auto)]
136///     id: i32,
137///
138///     #[prax(unique)]
139///     email: String,
140///
141///     #[prax(column = "display_name")]
142///     name: Option<String>,
143///
144///     #[prax(default = "now()")]
145///     created_at: chrono::DateTime<chrono::Utc>,
146/// }
147/// ```
148#[proc_macro_derive(Model, attributes(prax))]
149pub fn derive_model(input: TokenStream) -> TokenStream {
150    let input = parse_macro_input!(input as DeriveInput);
151
152    match generators::derive_model_impl(&input) {
153        Ok(tokens) => tokens.into(),
154        Err(err) => err.to_compile_error().into(),
155    }
156}
157
158/// Internal function to generate code from a schema file.
159fn generate_from_schema(schema_path: &str) -> Result<proc_macro2::TokenStream, syn::Error> {
160    use plugins::{PluginConfig, PluginContext, PluginRegistry};
161    use schema_reader::read_schema_with_config;
162
163    // Read and parse the schema file along with prax.toml configuration
164    let schema_with_config = read_schema_with_config(schema_path).map_err(|e| {
165        syn::Error::new(
166            proc_macro2::Span::call_site(),
167            format!("Failed to parse schema: {}", e),
168        )
169    })?;
170
171    let schema = schema_with_config.schema;
172    let model_style = schema_with_config.model_style;
173
174    // Initialize plugin system with model_style from prax.toml
175    // This auto-enables graphql plugins when model_style is GraphQL
176    let plugin_config = PluginConfig::with_model_style(model_style);
177    let plugin_registry = PluginRegistry::with_builtins();
178    let plugin_ctx = PluginContext::new(&schema, &plugin_config);
179
180    let mut output = proc_macro2::TokenStream::new();
181
182    // Run plugin start hooks
183    let start_output = plugin_registry.run_start(&plugin_ctx);
184    output.extend(start_output.tokens);
185    output.extend(start_output.root_items);
186
187    // Generate enums first (models may reference them)
188    for (_, enum_def) in &schema.enums {
189        output.extend(generate_enum_module(enum_def)?);
190
191        // Run plugin enum hooks
192        let plugin_output = plugin_registry.run_enum(&plugin_ctx, enum_def);
193        if !plugin_output.is_empty() {
194            // Add plugin output to the enum module
195            output.extend(plugin_output.tokens);
196        }
197    }
198
199    // Generate composite types
200    for (_, type_def) in &schema.types {
201        output.extend(generate_type_module(type_def)?);
202
203        // Run plugin type hooks
204        let plugin_output = plugin_registry.run_type(&plugin_ctx, type_def);
205        if !plugin_output.is_empty() {
206            output.extend(plugin_output.tokens);
207        }
208    }
209
210    // Generate views
211    for (_, view_def) in &schema.views {
212        output.extend(generate_view_module(view_def)?);
213
214        // Run plugin view hooks
215        let plugin_output = plugin_registry.run_view(&plugin_ctx, view_def);
216        if !plugin_output.is_empty() {
217            output.extend(plugin_output.tokens);
218        }
219    }
220
221    // Generate models with the configured model style
222    for (_, model_def) in &schema.models {
223        output.extend(generate_model_module_with_style(
224            model_def,
225            &schema,
226            model_style,
227        )?);
228
229        // Run plugin model hooks
230        let plugin_output = plugin_registry.run_model(&plugin_ctx, model_def);
231        if !plugin_output.is_empty() {
232            output.extend(plugin_output.tokens);
233        }
234    }
235
236    // Run plugin finish hooks
237    let finish_output = plugin_registry.run_finish(&plugin_ctx);
238    output.extend(finish_output.tokens);
239    output.extend(finish_output.root_items);
240
241    // Generate plugin documentation
242    output.extend(plugins::generate_plugin_docs(&plugin_registry));
243
244    Ok(output)
245}