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 macros;
59mod plugins;
60mod schema_reader;
61mod types;
62
63use generators::{
64    generate_enum_module, generate_model_module_with_style, generate_type_module,
65    generate_view_module,
66};
67
68/// Generate models from a Prax schema file.
69///
70/// This macro reads a `.prax` schema file at compile time and generates
71/// type-safe Rust code for all models, enums, and types defined in the schema.
72///
73/// # Example
74///
75/// ```rust,ignore
76/// prax::prax_schema!("schema.prax");
77///
78/// // Now you can use the generated types:
79/// let user = client.user().find_unique(user::id::equals(1)).exec().await?;
80/// ```
81///
82/// # Generated Code
83///
84/// For each model in the schema, this macro generates:
85/// - A module with the model name (snake_case)
86/// - A `Data` struct representing a row from the database
87/// - A `CreateInput` struct for creating new records
88/// - A `UpdateInput` struct for updating records
89/// - Field modules with filter operations (`equals`, `contains`, `in_`, etc.)
90/// - A `WhereParam` enum for type-safe filtering
91/// - An `OrderByParam` enum for sorting
92/// - Select and Include builders for partial queries
93#[proc_macro]
94pub fn prax_schema(input: TokenStream) -> TokenStream {
95    let input = parse_macro_input!(input as LitStr);
96    let schema_path = input.value();
97
98    match generate_from_schema(&schema_path) {
99        Ok(tokens) => tokens.into(),
100        Err(err) => {
101            let err_msg = err.to_string();
102            quote! {
103                compile_error!(#err_msg);
104            }
105            .into()
106        }
107    }
108}
109
110/// Derive macro for defining Prax models manually.
111///
112/// This derive macro allows you to define models in Rust code instead of
113/// using a `.prax` schema file. It generates the same query builder methods
114/// and type-safe operations.
115///
116/// # Attributes
117///
118/// ## Struct-level
119/// - `#[prax(table = "table_name")]` - Map to a different table name
120/// - `#[prax(schema = "schema_name")]` - Specify database schema
121///
122/// ## Field-level
123/// - `#[prax(id)]` - Mark as primary key
124/// - `#[prax(auto)]` - Auto-increment field
125/// - `#[prax(unique)]` - Unique constraint
126/// - `#[prax(default = value)]` - Default value
127/// - `#[prax(column = "col_name")]` - Map to different column
128/// - `#[prax(relation(...))]` - Define relation
129///
130/// # Example
131///
132/// ```rust,ignore
133/// #[derive(prax::Model)]
134/// #[prax(table = "users")]
135/// struct User {
136///     #[prax(id, auto)]
137///     id: i32,
138///
139///     #[prax(unique)]
140///     email: String,
141///
142///     #[prax(column = "display_name")]
143///     name: Option<String>,
144///
145///     #[prax(default = "now()")]
146///     created_at: chrono::DateTime<chrono::Utc>,
147/// }
148/// ```
149#[proc_macro_derive(Model, attributes(prax))]
150pub fn derive_model(input: TokenStream) -> TokenStream {
151    let input = parse_macro_input!(input as DeriveInput);
152
153    match generators::derive_model_impl(&input) {
154        Ok(tokens) => tokens.into(),
155        Err(err) => err.to_compile_error().into(),
156    }
157}
158
159/// `prax::find_many!` — schema-aware declarative DSL for the
160/// fluent-builder's `find_many` operation. See spec §4 for the full
161/// grammar.
162///
163/// ```rust,ignore
164/// prax::find_many!(client.user, {
165///     where: { email: { contains: "@example.com" } },
166///     order_by: { created_at: desc },
167///     take: 10,
168/// });
169/// ```
170#[proc_macro]
171pub fn find_many(input: TokenStream) -> TokenStream {
172    match macros::ops::find_many::expand_find_many(input.into()) {
173        Ok(t) => t.into(),
174        Err(e) => e.to_compile_error().into(),
175    }
176}
177
178/// `prax::find_unique!` — schema-aware DSL targeting `find_unique`.
179/// The `where:` block must match a single `@unique` (or `@id`) column.
180#[proc_macro]
181pub fn find_unique(input: TokenStream) -> TokenStream {
182    match macros::ops::find_unique::expand_find_unique(input.into()) {
183        Ok(t) => t.into(),
184        Err(e) => e.to_compile_error().into(),
185    }
186}
187
188/// `prax::find_first!` — schema-aware DSL targeting `find_first`.
189#[proc_macro]
190pub fn find_first(input: TokenStream) -> TokenStream {
191    match macros::ops::find_first::expand_find_first(input.into()) {
192        Ok(t) => t.into(),
193        Err(e) => e.to_compile_error().into(),
194    }
195}
196
197/// `prax::count!` — schema-aware DSL targeting `count`. Phase 3 only
198/// supports the `where:` key; the Prisma-style `_count` aggregate
199/// (`select: { _count: { posts: true } }`) is phase 6.
200#[proc_macro]
201pub fn count(input: TokenStream) -> TokenStream {
202    match macros::ops::count::expand_count(input.into()) {
203        Ok(t) => t.into(),
204        Err(e) => e.to_compile_error().into(),
205    }
206}
207
208/// `prax::aggregate!` — schema-aware DSL targeting `aggregate`. Accepts
209/// `where:`, `_count:`, `_sum:`, `_avg:`, `_min:`, `_max:` keys. At least one
210/// aggregate key is required.
211///
212/// ```rust,ignore
213/// prax::aggregate!(client.user, {
214///     where: { active: true },
215///     _sum: { views: true, score: true },
216///     _avg: { score: true },
217///     _count: { _all: true },
218/// });
219/// ```
220#[proc_macro]
221pub fn aggregate(input: TokenStream) -> TokenStream {
222    match macros::ops::aggregate::expand_aggregate(input.into()) {
223        Ok(t) => t.into(),
224        Err(e) => e.to_compile_error().into(),
225    }
226}
227
228/// `prax::group_by!` — schema-aware DSL targeting `group_by_columns`.
229/// Accepts `by:` (required), `where:`, `_count:`, `_sum:`, `_avg:`, `_min:`,
230/// `_max:`, and `having:` keys.
231///
232/// ```rust,ignore
233/// prax::group_by!(client.user, {
234///     by: [team_id, region],
235///     where: { active: true },
236///     _count: { _all: true },
237///     _sum: { views: true },
238///     having: { _count: { _all: { gt: 5 } } },
239/// });
240/// ```
241#[proc_macro]
242pub fn group_by(input: TokenStream) -> TokenStream {
243    match macros::ops::group_by::expand_group_by(input.into()) {
244        Ok(t) => t.into(),
245        Err(e) => e.to_compile_error().into(),
246    }
247}
248
249/// `prax::delete!` — schema-aware DSL targeting `delete`. The
250/// `where:` block must match a unique column.
251#[proc_macro]
252pub fn delete(input: TokenStream) -> TokenStream {
253    match macros::ops::delete::expand_delete(input.into()) {
254        Ok(t) => t.into(),
255        Err(e) => e.to_compile_error().into(),
256    }
257}
258
259/// `prax::delete_many!` — schema-aware DSL targeting `delete_many`.
260/// The `where:` block is the non-unique form.
261///
262/// **Warning:** an empty / `Filter::None` filter matches every row in
263/// the table. See `WhereInput`'s trait-level note.
264#[proc_macro]
265pub fn delete_many(input: TokenStream) -> TokenStream {
266    match macros::ops::delete_many::expand_delete_many(input.into()) {
267        Ok(t) => t.into(),
268        Err(e) => e.to_compile_error().into(),
269    }
270}
271
272/// `prax::r#where!` — schema-aware shape macro returning a
273/// `<Model>WhereInput` value. Composes with the read macros via
274/// `..spread`:
275///
276/// ```rust,ignore
277/// let active = prax::r#where!(User, { active: true });
278/// let _ = prax::find_many!(client.user, {
279///     ..active,
280///     email: { contains: "@x.com" },
281/// });
282/// ```
283///
284/// Exported as `r#where` because `where` is a Rust keyword and the
285/// raw-identifier prefix is required at the call site whenever the
286/// macro is reached through a path (`prax::r#where!(...)`).
287#[proc_macro]
288pub fn r#where(input: TokenStream) -> TokenStream {
289    match macros::ops::shape::expand_where_shape(input.into()) {
290        Ok(t) => t.into(),
291        Err(e) => e.to_compile_error().into(),
292    }
293}
294
295/// `prax::include!` — schema-aware shape macro returning a
296/// `<Model>Include` value. Composes with the read macros via
297/// `..spread` to build reusable relation-include shapes.
298///
299/// ```rust,ignore
300/// let with_posts = prax::include!(User, { posts: true });
301/// let _ = prax::find_unique!(client.user, {
302///     where: { id: 1 },
303///     include: { ..with_posts },
304/// });
305/// ```
306///
307/// Distinct from `std::include!` — they live in different modules and
308/// there is no ambiguity at the call site as long as the path is
309/// fully qualified (`prax::include!`).
310#[proc_macro]
311pub fn include(input: TokenStream) -> TokenStream {
312    match macros::ops::shape::expand_include_shape(input.into()) {
313        Ok(t) => t.into(),
314        Err(e) => e.to_compile_error().into(),
315    }
316}
317
318/// `prax::select!` — schema-aware shape macro returning a
319/// `<Model>Select` value. Composes with the read macros via `..spread`.
320///
321/// ```rust,ignore
322/// let lite = prax::select!(User, { id: true, email: true });
323/// let _ = prax::find_many!(client.user, {
324///     select: { ..lite },
325/// });
326/// ```
327#[proc_macro]
328pub fn select(input: TokenStream) -> TokenStream {
329    match macros::ops::shape::expand_select_shape(input.into()) {
330        Ok(t) => t.into(),
331        Err(e) => e.to_compile_error().into(),
332    }
333}
334
335/// `prax::order_by!` — schema-aware shape macro returning an
336/// `OrderBy` value. Accepts either a single `{ field: dir }` block or
337/// a list of such blocks for multi-key sorts.
338///
339/// ```rust,ignore
340/// let newest_first = prax::order_by!(User, { created_at: desc });
341/// let _ = prax::find_many!(client.user, {
342///     order_by: { created_at: desc },
343/// });
344/// // or as a list:
345/// let by_active_then_email = prax::order_by!(User, [
346///     { active: desc },
347///     { email: asc },
348/// ]);
349/// ```
350#[proc_macro]
351pub fn order_by(input: TokenStream) -> TokenStream {
352    match macros::ops::shape::expand_order_by_shape(input.into()) {
353        Ok(t) => t.into(),
354        Err(e) => e.to_compile_error().into(),
355    }
356}
357
358/// `prax::create!` — schema-aware DSL targeting `create`. Top-level
359/// keys: `data:` (required), `include` xor `select`. Phase 5a is
360/// scalar-only — relation operators inside `data:` (nested writes)
361/// land in phase 5b.
362///
363/// ```rust,ignore
364/// prax::create!(client.user, {
365///     data: { email: "a@x.com", name: "Alice", age: 30 },
366///     select: { id: true, email: true },
367/// });
368/// ```
369#[proc_macro]
370pub fn create(input: TokenStream) -> TokenStream {
371    match macros::ops::create::expand_create(input.into()) {
372        Ok(t) => t.into(),
373        Err(e) => e.to_compile_error().into(),
374    }
375}
376
377/// `prax::update!` — schema-aware DSL targeting `update`. Top-level
378/// keys: `where:` (required, unique), `data:` (required), `include`
379/// xor `select`. Atomic operators (`increment`, `decrement`,
380/// `multiply`, `divide`, `unset`) work via `{ <op>: V }` blocks inside
381/// `data:` — see spec §4.
382///
383/// ```rust,ignore
384/// prax::update!(client.user, {
385///     where: { id: 1 },
386///     data: {
387///         name: "Renamed",
388///         age: { increment: 1 },
389///         last_seen: { unset: true },
390///     },
391///     select: { id: true, age: true },
392/// });
393/// ```
394#[proc_macro]
395pub fn update(input: TokenStream) -> TokenStream {
396    match macros::ops::update::expand_update(input.into()) {
397        Ok(t) => t.into(),
398        Err(e) => e.to_compile_error().into(),
399    }
400}
401
402/// `prax::upsert!` — schema-aware DSL targeting `upsert`. Top-level
403/// keys: `where:` (required, unique), `create:` (required), `update:`
404/// (required), `include` xor `select`. On hit applies the `update:`
405/// payload; on miss inserts the `create:` payload.
406///
407/// ```rust,ignore
408/// prax::upsert!(client.user, {
409///     where: { email: "a@x.com" },
410///     create: { email: "a@x.com", name: "Alice", active: true, created_at: @(now) },
411///     update: { name: { set: "Renamed" }, age: { increment: 1 } },
412///     select: { id: true },
413/// });
414/// ```
415#[proc_macro]
416pub fn upsert(input: TokenStream) -> TokenStream {
417    match macros::ops::upsert::expand_upsert(input.into()) {
418        Ok(t) => t.into(),
419        Err(e) => e.to_compile_error().into(),
420    }
421}
422
423/// `prax::create_many!` — schema-aware DSL targeting `create_many`.
424/// Top-level keys: `data:` (required list of blocks),
425/// `skip_duplicates:` (optional bool).
426///
427/// ```rust,ignore
428/// prax::create_many!(client.user, {
429///     data: [
430///         { email: "a@x.com", name: "Alice" },
431///         { email: "b@x.com", name: "Bob" },
432///     ],
433///     skip_duplicates: true,
434/// });
435/// ```
436#[proc_macro]
437pub fn create_many(input: TokenStream) -> TokenStream {
438    match macros::ops::create_many::expand_create_many(input.into()) {
439        Ok(t) => t.into(),
440        Err(e) => e.to_compile_error().into(),
441    }
442}
443
444/// `prax::update_many!` — schema-aware DSL targeting `update_many`.
445/// Top-level keys: `where:` (optional non-unique filter), `data:`
446/// (required).
447///
448/// **Warning:** an empty/omitted `where:` matches every row in the
449/// table — see the trait-level note on `WhereInput`.
450///
451/// ```rust,ignore
452/// prax::update_many!(client.user, {
453///     where: { active: false },
454///     data: { active: true },
455/// });
456/// ```
457#[proc_macro]
458pub fn update_many(input: TokenStream) -> TokenStream {
459    match macros::ops::update_many::expand_update_many(input.into()) {
460        Ok(t) => t.into(),
461        Err(e) => e.to_compile_error().into(),
462    }
463}
464
465/// `prax::cursor!` — schema-aware shape macro returning a
466/// `<Model>WhereUniqueInput` value for use as a `cursor:` argument to
467/// the read macros.
468///
469/// The block must have exactly one entry whose key refers to an
470/// `@id` or `@unique` column on the model.
471///
472/// ```rust,ignore
473/// let from = prax::cursor!(User, { id: 42 });
474/// let _ = prax::find_many!(client.user, {
475///     cursor: { id: 42 },
476///     take: 10,
477/// });
478/// ```
479#[proc_macro]
480pub fn cursor(input: TokenStream) -> TokenStream {
481    match macros::ops::shape::expand_cursor_shape(input.into()) {
482        Ok(t) => t.into(),
483        Err(e) => e.to_compile_error().into(),
484    }
485}
486
487/// Internal function to generate code from a schema file.
488fn generate_from_schema(schema_path: &str) -> Result<proc_macro2::TokenStream, syn::Error> {
489    use plugins::{PluginConfig, PluginContext, PluginRegistry};
490    use schema_reader::read_schema_with_config;
491
492    // Read and parse the schema file along with prax.toml configuration
493    let schema_with_config = read_schema_with_config(schema_path).map_err(|e| {
494        syn::Error::new(
495            proc_macro2::Span::call_site(),
496            format!("Failed to parse schema: {}", e),
497        )
498    })?;
499
500    let schema = schema_with_config.schema;
501    let model_style = schema_with_config.model_style;
502
503    // Initialize plugin system with model_style from prax.toml
504    // This auto-enables graphql plugins when model_style is GraphQL
505    let plugin_config = PluginConfig::with_model_style(model_style);
506    let plugin_registry = PluginRegistry::with_builtins();
507    let plugin_ctx = PluginContext::new(&schema, &plugin_config);
508
509    let mut output = proc_macro2::TokenStream::new();
510
511    // Run plugin start hooks
512    let start_output = plugin_registry.run_start(&plugin_ctx);
513    output.extend(start_output.tokens);
514    output.extend(start_output.root_items);
515
516    // Generate enums first (models may reference them)
517    for (_, enum_def) in &schema.enums {
518        output.extend(generate_enum_module(enum_def)?);
519
520        // Run plugin enum hooks
521        let plugin_output = plugin_registry.run_enum(&plugin_ctx, enum_def);
522        if !plugin_output.is_empty() {
523            // Add plugin output to the enum module
524            output.extend(plugin_output.tokens);
525        }
526    }
527
528    // Generate composite types
529    for (_, type_def) in &schema.types {
530        output.extend(generate_type_module(type_def)?);
531
532        // Run plugin type hooks
533        let plugin_output = plugin_registry.run_type(&plugin_ctx, type_def);
534        if !plugin_output.is_empty() {
535            output.extend(plugin_output.tokens);
536        }
537    }
538
539    // Generate views
540    for (_, view_def) in &schema.views {
541        output.extend(generate_view_module(view_def)?);
542
543        // Run plugin view hooks
544        let plugin_output = plugin_registry.run_view(&plugin_ctx, view_def);
545        if !plugin_output.is_empty() {
546            output.extend(plugin_output.tokens);
547        }
548    }
549
550    // Generate models with the configured model style
551    for (_, model_def) in &schema.models {
552        output.extend(generate_model_module_with_style(
553            model_def,
554            &schema,
555            model_style,
556        )?);
557
558        // Run plugin model hooks
559        let plugin_output = plugin_registry.run_model(&plugin_ctx, model_def);
560        if !plugin_output.is_empty() {
561            output.extend(plugin_output.tokens);
562        }
563    }
564
565    // Run plugin finish hooks
566    let finish_output = plugin_registry.run_finish(&plugin_ctx);
567    output.extend(finish_output.tokens);
568    output.extend(finish_output.root_items);
569
570    // Generate plugin documentation
571    output.extend(plugins::generate_plugin_docs(&plugin_registry));
572
573    Ok(output)
574}