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