Skip to main content

benzina_derive/
lib.rs

1use proc_macro2::Span;
2use quote::ToTokens;
3use syn::{
4    DeriveInput, Ident, Path, PathArguments, PathSegment, parse_macro_input,
5    punctuated::Punctuated, token::PathSep,
6};
7
8use self::enum_derive::Enum;
9use crate::join::Join;
10
11mod enum_derive;
12mod join;
13mod rename_rule;
14
15/// Derive [`FromSql`] and [`ToSql`] for a Rust enum.
16/// Represents a PostgreSQL enum as a Rust enum.
17///
18/// ## Example
19///
20/// ### migration
21///
22/// ```sql
23/// CREATE TYPE animal AS ENUM (
24///     'chicken',
25///     'duck',
26///     'oca',
27///     'rabbit
28/// );
29/// ```
30///
31/// ### Rust enum
32///
33/// ```rust
34/// # use benzina_derive as benzina;
35/// # fn main() {}
36///
37/// #[derive(Debug, Copy, Clone, benzina::Enum)]
38/// #[benzina(
39///     sql_type = crate::schema::sql_types::Animal,
40///     rename_all = "snake_case"
41/// )]
42/// # #[benzina(crate = fake_benzina)]
43/// pub enum Animal {
44///     Chicken,
45///     Duck,
46///     #[benzina(rename = "oca")]
47///     Goose,
48///     Rabbit,
49/// }
50///
51/// pub mod schema {
52///     // @generated automatically by Diesel CLI.
53///
54///     pub mod sql_types {
55///         #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
56///         #[diesel(postgres_type(name = "animal"))]
57///         pub struct Animal;
58///     }
59/// }
60/// #
61/// # mod fake_benzina {
62/// #     pub mod __private {
63/// #         pub use std;
64/// #         pub use diesel;
65/// #     }
66/// # }
67/// ```
68///
69/// ## Enums with variant-specific data in separate JSONB column
70///
71/// You can also use `benzina::Enum` for enums where each variant holds
72/// associated data. This is useful when you have a PostgreSQL ENUM for the
73/// discriminator and a JSONB column for the variant-specific payload.
74///
75/// ### migration
76///
77/// ```sql
78/// CREATE TYPE animal AS ENUM ('chicken', 'duck', 'oca', 'rabbit');
79///
80/// CREATE TABLE pets (
81///     id SERIAL PRIMARY KEY,
82///     name TEXT NOT NULL,
83///     animal animal NOT NULL,
84///     animal_data JSONB NOT NULL
85/// );
86/// ```
87///
88/// ### Rust enum
89///
90/// ```rust
91/// # use benzina_derive as benzina;
92/// # fn main() {}
93/// use diesel::pg::Pg;
94/// use diesel::{Identifiable, Insertable, Queryable, Selectable};
95/// use serde::{Deserialize, Serialize};
96///
97/// #[derive(Debug, Queryable, Identifiable, Insertable, Selectable)]
98/// #[diesel(table_name = schema::pets, check_for_backend(Pg))]
99/// pub struct Pet {
100///     pub id: i32,
101///     pub name: String,
102///     #[diesel(embed)]
103///     pub animal: Animal,
104/// }
105///
106/// #[derive(Debug, Clone, benzina::Enum)]
107/// #[benzina(
108///     sql_type = schema::sql_types::Animal,
109///     rename_all = "snake_case",
110///     table = schema::pets,
111///     column = animal,
112///     data_column = animal_data
113/// )]
114/// # #[benzina(crate = fake_benzina)]
115/// pub enum Animal {
116///     Chicken(ChickenData),
117///     Duck(DuckData),
118///     #[benzina(rename = "oca")]
119///     Goose(GooseData),
120///     Rabbit(RabbitData),
121/// }
122///
123/// #[derive(Debug, Clone, Serialize, Deserialize)]
124/// pub struct ChickenData {
125///     pub likes_cuddles: bool,
126///     pub breed: String,
127/// }
128///
129/// #[derive(Debug, Clone, Serialize, Deserialize)]
130/// pub struct DuckData {
131///     pub favorite_treat: String,
132///     pub feather_color: String,
133/// }
134///
135/// #[derive(Debug, Clone, Serialize, Deserialize)]
136/// pub struct GooseData {
137///     pub weight_kg: f64,
138///     pub honks_at_strangers: bool,
139/// }
140///
141/// #[derive(Debug, Clone, Serialize, Deserialize)]
142/// pub struct RabbitData {
143///     pub fur_color: String,
144///     pub litter_trained: bool,
145/// }
146///
147/// pub mod schema {
148///     // @generated automatically by Diesel CLI.
149///
150///     pub mod sql_types {
151///         #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
152///         #[diesel(postgres_type(name = "animal"))]
153///         pub struct Animal;
154///     }
155///
156///     diesel::table! {
157///         use diesel::sql_types::*;
158///         use super::sql_types::Animal;
159///
160///         pets (id) {
161///             id -> Int4,
162///             name -> Text,
163///             animal -> Animal,
164///             animal_data -> Jsonb,
165///         }
166///     }
167/// }
168/// #
169/// # mod fake_benzina {
170/// #     pub mod __private {
171/// #         pub use std;
172/// #         pub use diesel;
173/// #
174/// #         pub mod json {
175/// #             use diesel::{
176/// #                 deserialize::{FromSql, FromSqlRow},
177/// #                 expression::AsExpression,
178/// #                 pg::{Pg, PgValue},
179/// #                 serialize::ToSql,
180/// #                 sql_types,
181/// #             };
182/// #             use serde::{Deserialize, Serialize};
183/// #
184/// #             #[derive(Debug, FromSqlRow, AsExpression)]
185/// #             #[diesel(sql_type = sql_types::Jsonb)]
186/// #             pub struct RawJsonb;
187/// #
188/// #             impl RawJsonb {
189/// #                 pub const EMPTY: Self = Self;
190/// #
191/// #                 pub fn serialize(value: &impl Serialize) -> diesel::deserialize::Result<Self> {
192/// #                     unimplemented!()
193/// #                 }
194/// #
195/// #                 pub fn deserialize<T: for<'a> Deserialize<'a>>(&self) -> diesel::deserialize::Result<T> {
196/// #                     unimplemented!()
197/// #                 }
198/// #             }
199/// #
200/// #             impl FromSql<sql_types::Jsonb, Pg> for RawJsonb {
201/// #                 fn from_sql(value: PgValue) -> diesel::deserialize::Result<Self> {
202/// #                     unimplemented!()
203/// #                 }
204/// #             }
205/// #
206/// #             impl ToSql<sql_types::Jsonb, Pg> for RawJsonb {
207/// #                 fn to_sql(&self, out: &mut diesel::serialize::Output<Pg>) -> diesel::serialize::Result {
208/// #                     unimplemented!()
209/// #                 }
210/// #             }
211/// #         }
212/// #     }
213/// # }
214/// ```
215///
216/// [`FromSql`]: https://docs.rs/diesel/latest/diesel/deserialize/trait.FromSql.html
217/// [`ToSql`]: https://docs.rs/diesel/latest/diesel/serialize/trait.ToSql.html
218#[proc_macro_derive(Enum, attributes(benzina))]
219pub fn benzina_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
220    let input = parse_macro_input!(input as DeriveInput);
221
222    Enum::parse(input)
223        .map_or_else(syn::Error::into_compile_error, ToTokens::into_token_stream)
224        .into()
225}
226
227/// Convert the output of a query containing joins into a properly nested structure.
228///
229/// <div class="warning">
230///     This macro is still in the experimental stage and may contain
231///     bugs and unhelpful error diagnostics.
232/// </div>
233///
234/// Enable the `rustc-hash` feature to use a faster but non-DOS-resistant hasher for
235/// the internal maps.
236///
237/// ## Example
238///
239/// ```rust,compile_fail
240/// # fn main() {}
241///
242/// use diesel::{
243///     Identifiable, QueryDsl, QueryResult, Queryable, RunQueryDsl, Selectable, SelectableHelper,
244///     pg::{Pg, PgConnection},
245/// };
246///
247/// #[derive(Debug, Queryable, Identifiable, Selectable)]
248/// #[diesel(table_name = users, check_for_backend(Pg))]
249/// pub struct User {
250///     pub id: i32,
251///     pub name: String,
252/// }
253///
254/// #[derive(Debug)]
255/// pub struct UserWithPosts {
256///     pub user: User,
257///     pub posts: Vec<PostFromUser>,
258/// }
259///
260/// #[derive(Debug, Queryable, Identifiable, Selectable)]
261/// #[diesel(table_name = topics, check_for_backend(Pg))]
262/// pub struct Topic {
263///     pub id: i32,
264///     pub name: String,
265/// }
266///
267/// #[derive(Debug, Queryable, Identifiable, Selectable)]
268/// #[diesel(table_name = posts, check_for_backend(Pg))]
269/// pub struct Post {
270///     pub id: i32,
271///     pub user_id: i32,
272///     pub topic_id: i32,
273///     pub message: String,
274/// }
275///
276/// #[derive(Debug)]
277/// pub struct PostFromUser {
278///     pub post: Post,
279///     pub topic: Topic,
280///     pub comments: Vec<CommentFromPost>,
281/// }
282///
283/// #[derive(Debug, Queryable, Identifiable, Selectable)]
284/// #[diesel(table_name = comments, check_for_backend(Pg))]
285/// pub struct Comment {
286///     pub id: i32,
287///     pub post_id: i32,
288///     pub user_id: i32,
289///     pub message: String,
290/// }
291///
292/// #[derive(Debug)]
293/// pub struct CommentFromPost {
294///     pub comment: Comment,
295///     pub user: User,
296/// }
297///
298/// impl UserWithPosts {
299///     pub fn get_by_id(conn: &mut PgConnection, user_id: i32) -> QueryResult<Vec<Self>> {
300///         let (users1, users2) = diesel::alias!(users as users1, users as users2);
301///
302///         let records = users1
303///             .find(user_id)
304///             .left_join(
305///                 posts::table
306///                     .left_join(topics::table)
307///                     .left_join(comments::table.left_join(users2)),
308///             )
309///             .select((
310///                 users1.fields(<User as Selectable<Pg>>::construct_selection()),
311///                 Option::<Post>::as_select(),
312///                 Option::<Topic>::as_select(),
313///                 Option::<Comment>::as_select(),
314///                 users2.fields(<Option<User> as Selectable<Pg>>::construct_selection()),
315///             ))
316///             .get_results::<(
317///                 User,
318///                 Option<Post>,
319///                 Option<Topic>,
320///                 Option<Comment>,
321///                 Option<User>,
322///             )>(conn)?;
323///
324///         let joined = benzina::join! {
325///             records,
326///             Vec<UserWithPosts {
327///                 user: One<0>,
328///                 posts: Vec0<PostFromUser {
329///                     post: One<1>,
330///                     topic: AssumeOne<2>,
331///                     comments: Vec0<CommentFromPost {
332///                         comment: One<3>,
333///                         user: AssumeOne<4>,
334///                     }>,
335///                 }>,
336///             }>,
337///         };
338///         Ok(joined)
339///     }
340/// }
341///
342/// diesel::table! {
343///     users {
344///         id -> Integer,
345///         name -> Text,
346///     }
347/// }
348///
349/// diesel::table! {
350///     topics {
351///         id -> Integer,
352///         name -> Text,
353///     }
354/// }
355///
356/// diesel::table! {
357///     posts {
358///         id -> Integer,
359///         user_id -> Integer,
360///         topic_id -> Integer,
361///         message -> Text,
362///     }
363/// }
364///
365/// diesel::table! {
366///     comments {
367///         id -> Integer,
368///         post_id -> Integer,
369///         user_id -> Integer,
370///         message -> Text,
371///     }
372/// }
373///
374/// diesel::joinable!(posts -> users (user_id));
375/// diesel::joinable!(posts -> topics (topic_id));
376/// diesel::joinable!(comments -> posts (post_id));
377/// diesel::joinable!(comments -> users (user_id));
378///
379/// diesel::allow_tables_to_appear_in_same_query!(users, topics, posts, comments);
380/// ```
381#[proc_macro]
382pub fn join(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
383    let input = parse_macro_input!(input as Join);
384    input.into_token_stream().into()
385}
386
387#[expect(clippy::ref_option, reason = "it's easier to use")]
388fn crate_name(crate_name: &Option<Path>) -> Path {
389    crate_name.clone().unwrap_or_else(|| {
390        let mut segments = Punctuated::new();
391        segments.push(PathSegment {
392            ident: Ident::new("benzina", Span::call_site()),
393            arguments: PathArguments::None,
394        });
395        Path {
396            leading_colon: Some(PathSep {
397                spans: [Span::call_site(); 2],
398            }),
399            segments,
400        }
401    })
402}