Skip to main content

rango_orm_derive/
lib.rs

1//! `#[derive(Model)]` and `#[derive(ModelMixin)]` for [Rango ORM](https://crates.io/crates/rango-orm).
2//!
3//! Internal crate: depend on `rango-orm`, which re-exports the derives.
4#![forbid(unsafe_code)]
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::quote;
8use syn::{DeriveInput, parse_macro_input};
9
10mod implicit_pk;
11mod mixin;
12mod model;
13mod partial;
14mod utils;
15
16/// Returns the path to rango_orm_core: through the `rango-orm` facade, or directly.
17pub(crate) fn rango_core_path() -> TokenStream2 {
18    match proc_macro_crate::crate_name("rango-orm") {
19        Ok(proc_macro_crate::FoundCrate::Itself) => quote! { ::rango_orm_core },
20        Ok(proc_macro_crate::FoundCrate::Name(name)) => {
21            // The facade's lib is named `rango`; proc-macro-crate returns the
22            // dependency key, which is `rango_orm` unless the user renamed it.
23            let name = if name == "rango_orm" {
24                "rango"
25            } else {
26                name.as_str()
27            };
28            let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
29            quote! { ::#ident::rango_orm_core }
30        }
31        // No facade: the user depends on rango-orm-core directly.
32        Err(_) => quote! { ::rango_orm_core },
33    }
34}
35
36/// Derive macro for Rango models.
37///
38/// Generates:
39/// - `impl Model for MyStruct` with `table_name()` and `schema()`
40/// - Table name defaults to snake_case of the struct name (e.g. `UserProfile` → `user_profile`)
41///
42/// # Example
43/// ```rust,ignore
44/// #[derive(Model)]
45/// struct Article {
46///     id: FieldUuid,
47///     title: FieldVarchar<1, 255>,
48///     body: Option<FieldText>,
49///     #[field(index)]
50///     slug: FieldVarchar<1, 100>,
51///     #[field(auto_now_add)]
52///     created_at: FieldDateTime,
53/// }
54/// ```
55#[proc_macro_derive(Model, attributes(model, field))]
56pub fn derive_model(input: TokenStream) -> TokenStream {
57    let input = parse_macro_input!(input as DeriveInput);
58    model::expand(input)
59        .unwrap_or_else(|e| e.to_compile_error())
60        .into()
61}
62
63/// Attribute macro: adds an implicit primary key, then derives [`Model`].
64///
65/// - no PK declared: injects `pub id: FieldUuid` as the first field
66/// - `#[rango::model(pk = "bigint")]`: injects `id: FieldAutoBigInt` (database-generated);
67///   `"int"` gives `FieldAutoInt`, `"uuid"` is the default
68/// - a PK already exists (`id` field or `#[field(primary_key)]`): nothing is injected
69/// - `#[derive(Model)]` is added unless the struct already has it
70///
71/// The default is not read from `rango.toml`: cargo does not track that file,
72/// so builds would go stale when it changes.
73///
74/// ```rust,ignore
75/// #[rango::model]
76/// struct Article { title: FieldVarchar<1, 200> }
77/// ```
78#[proc_macro_attribute]
79pub fn model(args: TokenStream, input: TokenStream) -> TokenStream {
80    implicit_pk::expand(args.into(), input.into())
81        .unwrap_or_else(|e| e.to_compile_error())
82        .into()
83}
84
85/// Marker derive for abstract model mixins (never creates a table).
86///
87/// # Example
88/// ```rust,ignore
89/// #[derive(ModelMixin)]
90/// struct Timestamps {
91///     #[field(auto_now_add)]
92///     created_at: FieldDateTime,
93///     #[field(auto_now)]
94///     updated_at: FieldDateTime,
95/// }
96///
97/// #[derive(Model)]
98/// #[model(mixins(Timestamps))]
99/// struct Article {
100///     id: FieldUuid,
101///     title: FieldVarchar<1, 255>,
102/// }
103/// ```
104#[proc_macro_derive(ModelMixin, attributes(field))]
105pub fn derive_model_mixin(input: TokenStream) -> TokenStream {
106    let input = parse_macro_input!(input as DeriveInput);
107    mixin::expand(input)
108        .unwrap_or_else(|e| e.to_compile_error())
109        .into()
110}
111
112/// Returns the path to the `Model` derive: through the `rango-orm` facade, or directly.
113pub(crate) fn rango_derive_path() -> TokenStream2 {
114    match proc_macro_crate::crate_name("rango-orm") {
115        Ok(proc_macro_crate::FoundCrate::Itself) => quote! { ::rango::Model },
116        Ok(proc_macro_crate::FoundCrate::Name(name)) => {
117            let name = if name == "rango_orm" {
118                "rango"
119            } else {
120                name.as_str()
121            };
122            let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
123            quote! { ::#ident::Model }
124        }
125        Err(_) => quote! { ::rango_orm_derive::Model },
126    }
127}
128
129/// Derive macro for typed partial selects: a struct holding a subset of a model's
130/// columns, read with `QueryBuilder::select_as::<Self>()`.
131///
132/// Every field must match a model column by name and type (checked at compile time).
133///
134/// ```rust,ignore
135/// #[derive(Partial)]
136/// #[partial(model = User)]
137/// struct UserLite {
138///     id: FieldUuid,
139///     email: FieldEmail,
140/// }
141/// ```
142#[proc_macro_derive(Partial, attributes(partial))]
143pub fn derive_partial(input: TokenStream) -> TokenStream {
144    let input = parse_macro_input!(input as DeriveInput);
145    partial::expand(input)
146        .unwrap_or_else(|e| e.to_compile_error())
147        .into()
148}