Skip to main content

renox_macros/
lib.rs

1//! Procedural macros for Renox. Use them through the `renox` crate:
2//! `#[derive(Model, FromRow, DbEnum, Validate)]`, `renox::migrations!()`,
3//! `renox::embedded!()` and `#[renox::test]`.
4#![warn(missing_docs)]
5
6mod db_enum;
7mod embedded;
8mod from_row;
9mod migrations;
10mod model;
11mod validate;
12
13use proc_macro::TokenStream;
14use syn::{DeriveInput, parse_macro_input};
15
16/// Implements `renox::db::Model` for a struct with named fields.
17///
18/// ```
19/// # use renox::prelude::*;
20/// # use serde::Serialize;
21/// #[derive(Model, Serialize, Default)]
22/// #[model(table = "products", soft_deletes)]
23/// struct Product {
24///     id: i64,
25///     name: String,
26///     #[model(skip)]
27///     label: String, // not a column; filled with Default when loading
28///     created_at: Option<DateTime>,
29///     updated_at: Option<DateTime>,
30///     deleted_at: Option<DateTime>,
31/// }
32/// ```
33///
34/// - `table` defaults to the struct name in snake_case (no pluralisation).
35/// - An `id` field is required; its type is the key (`i64`, `Ulid`, `Uuid` or
36///   `String`, see `renox::db::ModelKey`).
37/// - `created_at` / `updated_at` fields are filled on save.
38/// - `soft_deletes` needs a `deleted_at: Option<DateTime>` field.
39/// - `search = "title, body"` names the text columns full-text search looks
40///   in (`Model::search`, `renox::db::search`), most important first;
41///   `search_language = "simple"` changes the language from `english`.
42/// - `default_scope = "path::to::fn"` and `hooks`: see `renox::db::Model`.
43#[proc_macro_derive(Model, attributes(model))]
44pub fn derive_model(input: TokenStream) -> TokenStream {
45    let input = parse_macro_input!(input as DeriveInput);
46    model::expand(input)
47        .unwrap_or_else(|err| err.to_compile_error())
48        .into()
49}
50
51/// Implements `renox::Validate` from `#[validate(…)]` attributes on the
52/// fields, for forms that need only rules:
53///
54/// ```
55/// # use renox::prelude::*;
56/// #[derive(serde::Deserialize, Validate)]
57/// struct Signup {
58///     #[validate(required, max = 100, label = "Full name")]
59///     name: String,
60///     #[validate(required, email, unique("users", "email"))]
61///     email: String,
62///     #[validate(required, min = 8, confirmed(&self.password_confirmation))]
63///     password: String,
64///     password_confirmation: String,
65///     #[validate(max = 5, each(required, max = 20), distinct)]
66///     tags: Vec<String>,
67///     #[validate(rename = "t-shirt", one_of(&["S", "M", "L"]))]
68///     size: String,
69/// }
70/// ```
71///
72/// Each item is a call on the field's rules, in order: `required` is
73/// `.required()`, `max = 100` is `.max(100)`, `unique("users", "email")` is
74/// `.unique("users", "email")`, so every rule of `renox::validation::Field`
75/// works, and arguments may use `self`. Three are special: `each(…)` applies
76/// rules to each item of a list (errors `tags.0`, …), `distinct` refuses
77/// repeated items, and `rename = "…"` names the field as the form does. For
78/// `prepare`, `authorize` and `after`, add `#[validate(hooks)]` on the struct
79/// and `impl renox::validation::ValidateHooks`. Anything else: implement
80/// `Validate` by hand.
81#[proc_macro_derive(Validate, attributes(validate))]
82pub fn derive_validate(input: TokenStream) -> TokenStream {
83    let input = parse_macro_input!(input as DeriveInput);
84    validate::expand(input)
85        .unwrap_or_else(|err| err.to_compile_error())
86        .into()
87}
88
89/// Implements `renox::db::FromRow`, so `sql(…).fetch_as::<T>()` can read
90/// rows of any query (joins, aggregates, a few columns) into the struct.
91///
92/// ```
93/// # use renox::prelude::*;
94/// # use serde::Serialize;
95/// #[derive(FromRow, Serialize)]
96/// struct ProductRow {
97///     id: i64,
98///     name: String,
99///     #[row(rename = "category_name")]
100///     category: Option<String>,
101///     #[row(skip)]
102///     note: String, // Default
103/// }
104/// ```
105///
106/// `derive(Model)` implements `FromRow` too.
107#[proc_macro_derive(FromRow, attributes(row))]
108pub fn derive_from_row(input: TokenStream) -> TokenStream {
109    let input = parse_macro_input!(input as DeriveInput);
110    from_row::expand(input)
111        .unwrap_or_else(|err| err.to_compile_error())
112        .into()
113}
114
115/// A fieldless enum stored as text: in the database (a `TEXT` column), in
116/// forms (`<select>`), in JSON and in templates. Variants are stored in
117/// snake_case (`OnHold` → `on_hold`) unless renamed with `#[db(rename = "…")]`.
118///
119/// Generates `as_str()`, `ALL` (every variant, e.g. for a `<select>`),
120/// `Display`, `FromStr`, `Serialize`, `Deserialize`, `ToDbValue`, and
121/// decoding on every database, so the enum can be a model field.
122///
123/// ```
124/// # use renox::prelude::*;
125/// #[derive(DbEnum, Debug, Clone, Copy, PartialEq, Default)]
126/// enum Status {
127///     #[default]
128///     Draft,
129///     Published,
130///     #[db(rename = "hidden")]
131///     Archived,
132/// }
133///
134/// assert_eq!(Status::Archived.as_str(), "hidden");
135/// assert_eq!("published".parse::<Status>().unwrap(), Status::Published);
136/// assert_eq!(Status::ALL.len(), 3);
137/// ```
138#[proc_macro_derive(DbEnum, attributes(db))]
139pub fn derive_db_enum(input: TokenStream) -> TokenStream {
140    let input = parse_macro_input!(input as DeriveInput);
141    db_enum::expand(input)
142        .unwrap_or_else(|err| err.to_compile_error())
143        .into()
144}
145
146/// Embeds the SQL migrations of a directory (default `migrations`, relative
147/// to the crate root) as `&'static [renox::db::Migration]`.
148///
149/// Files are `<timestamp>_<name>.up.sql` with an optional matching
150/// `.down.sql`, or a plain `<timestamp>_<name>.sql` that can't be rolled back.
151/// Add a `build.rs` with `println!("cargo:rerun-if-changed=migrations");` so
152/// new files are picked up (`rnx new` creates it).
153#[proc_macro]
154pub fn migrations(input: TokenStream) -> TokenStream {
155    migrations::expand(input.into())
156        .unwrap_or_else(|err| err.to_compile_error())
157        .into()
158}
159
160/// Marks an async test, like `#[tokio::test]`, using the Tokio that Renox
161/// re-exports, so apps don't need `tokio` as a dependency.
162///
163/// ```
164/// # use renox::prelude::*;
165/// # use renox::testing::TestApp;
166/// #[renox::test]
167/// async fn home_page() {
168///     let app = TestApp::new(App::new()).await;
169///     app.get("/health").await.assert_ok();
170/// }
171/// ```
172#[proc_macro_attribute]
173pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
174    let attr = proc_macro2::TokenStream::from(attr);
175    let item = proc_macro2::TokenStream::from(item);
176    let extra = if attr.is_empty() {
177        quote::quote! {}
178    } else {
179        quote::quote! { #attr, }
180    };
181    quote::quote! {
182        #[::renox::tokio::test(#extra crate = "::renox::tokio")]
183        #item
184    }
185    .into()
186}
187
188/// Embeds `resources/views`, `resources/lang` and `public` in the binary, so
189/// a release build runs from a single file:
190///
191/// ```
192/// # use renox::prelude::*;
193/// # let _ =
194/// App::new().embed(renox::embedded!())
195/// # ;
196/// ```
197///
198/// Files are read from disk while `APP_DEBUG` is on (templates reload), and
199/// from the binary otherwise. Add `cargo:rerun-if-changed=resources` and
200/// `=public` to `build.rs` so new files are picked up (`rnx new` does).
201#[proc_macro]
202pub fn embedded(input: TokenStream) -> TokenStream {
203    embedded::expand(input.into())
204        .unwrap_or_else(|err| err.to_compile_error())
205        .into()
206}