Skip to main content

actix_cloud_extra_macros/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{Ident, ImplItem, ItemImpl, parse_macro_input, parse_quote};
6
7/// Implement default repo.
8///
9/// Generates default CRUD methods (`find`, `find_by_id`, `delete_all`,
10/// `delete`, `count`) on the repo struct; existing methods with the same
11/// name are kept untouched. The attribute argument is the entity module path,
12/// whose `Entity`/`Model` types are used.
13///
14/// # Examples
15/// ```ignore
16/// pub struct UserRepo;
17///
18/// #[default_repo(users)]
19/// impl UserRepo {}
20/// ```
21#[proc_macro_attribute]
22pub fn default_repo(attr: TokenStream, input: TokenStream) -> TokenStream {
23    let attr = parse_macro_input!(attr as Ident);
24    let mut input = parse_macro_input!(input as ItemImpl);
25    let func: Vec<String> = input
26        .items
27        .iter()
28        .map(|x| {
29            if let ImplItem::Fn(x) = x {
30                x.sig.ident.to_string()
31            } else {
32                String::new()
33            }
34        })
35        .collect();
36    let contains = |name: &str| func.iter().any(|x| x == name);
37    if !contains("find") {
38        input.items.push(parse_quote! {
39            pub async fn find<C>(
40                db: &C,
41                cond: actix_cloud_extra::api::Condition,
42            ) -> anyhow::Result<(Vec<#attr::Model>, u64)>
43            where
44                C: sea_orm::ConnectionTrait
45            {
46                cond.select_page(#attr::Entity::find(), db).await
47            }
48        });
49    }
50    if !contains("find_by_id") {
51        input.items.push(parse_quote! {
52            pub async fn find_by_id<C>(db: &C, id: &actix_cloud_extra::HyUuid) -> anyhow::Result<Option<#attr::Model>>
53            where
54                C: sea_orm::ConnectionTrait,
55            {
56                #attr::Entity::find_by_id(id.to_owned())
57                    .one(db)
58                    .await
59                    .map_err(Into::into)
60            }
61        });
62    }
63    if !contains("delete_all") {
64        input.items.push(parse_quote! {
65            pub async fn delete_all<C>(db: &C) -> anyhow::Result<u64>
66            where
67                C: sea_orm::ConnectionTrait,
68            {
69                #attr::Entity::delete_many()
70                    .exec(db)
71                    .await
72                    .map(|x| x.rows_affected)
73                    .map_err(Into::into)
74            }
75        });
76    }
77    if !contains("delete") {
78        input.items.push(parse_quote! {
79            pub async fn delete<C>(db: &C, id: &actix_cloud_extra::HyUuid) -> anyhow::Result<u64>
80            where
81                C: sea_orm::ConnectionTrait,
82            {
83                #attr::Entity::delete_by_id(*id)
84                    .exec(db)
85                    .await
86                    .map(|x| x.rows_affected)
87                    .map_err(Into::into)
88            }
89        });
90    }
91    if !contains("count") {
92        input.items.push(parse_quote! {
93            pub async fn count<C>(
94                db: &C,
95                cond: actix_cloud_extra::api::Condition,
96            ) -> anyhow::Result<u64>
97            where
98                C: sea_orm::ConnectionTrait,
99            {
100                Ok(cond.build(#attr::Entity::find()).0.count(db).await?)
101            }
102        });
103    }
104
105    quote! {
106        #input
107    }
108    .into()
109}
110
111#[cfg(feature = "seaorm")]
112/// Default timestamp generator.
113///
114/// Automatically generate `created_at` and `updated_at` on create and update.
115/// On insert both fields are set to the current UTC time; on update only
116/// `updated_at` is refreshed. Also implements
117/// `actix_cloud_extra::entity::DefaultColumnTrait` for `Column`.
118///
119/// # Examples
120/// ```ignore
121/// pub struct Model {
122///     ...
123///     pub created_at: DateTime,
124///     pub updated_at: DateTime,
125/// }
126///
127/// #[entity_timestamp]
128/// impl ActiveModel {}
129/// ```
130#[proc_macro_attribute]
131pub fn entity_timestamp(_: TokenStream, input: TokenStream) -> TokenStream {
132    let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);
133    entity.items.push(syn::parse_quote!(
134        fn entity_timestamp(&self, e: &mut Self, insert: bool) {
135            let tm: sea_orm::ActiveValue<DateTime> =
136                sea_orm::ActiveValue::set(chrono::Utc::now().naive_utc());
137            if insert {
138                e.created_at = tm.clone();
139                e.updated_at = tm.clone();
140            } else {
141                e.updated_at = tm.clone();
142            }
143        }
144    ));
145    quote! {
146        #entity
147
148        impl actix_cloud_extra::entity::DefaultColumnTrait for Column {
149            fn get_created_at() -> impl sea_orm::ColumnTrait {
150                Self::CreatedAt
151            }
152
153            fn get_updated_at() -> impl sea_orm::ColumnTrait {
154                Self::UpdatedAt
155            }
156        }
157    }
158    .into()
159}
160
161#[cfg(feature = "seaorm")]
162/// Default id generator.
163///
164/// Automatically generate `id` on create, only when the field is not already
165/// set.
166///
167/// # Examples
168/// ```ignore
169/// pub struct Model {
170///     id: i64,
171///     ...
172/// }
173///
174/// #[entity_id(rand_i64())]
175/// impl ActiveModel {}
176/// ```
177#[proc_macro_attribute]
178pub fn entity_id(attr: TokenStream, input: TokenStream) -> TokenStream {
179    let attr = syn::parse_macro_input!(attr as syn::ExprCall);
180    let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);
181    entity.items.push(syn::parse_quote!(
182        fn entity_id(&self, e: &mut Self, insert: bool) {
183            if insert && e.id.is_not_set() {
184                e.id = sea_orm::ActiveValue::set(#attr);
185            }
186        }
187    ));
188    quote! {
189        #entity
190    }
191    .into()
192}
193
194#[cfg(feature = "seaorm")]
195/// Default entity behavior:
196/// - `entity_id`
197/// - `entity_timestamp`
198///
199/// The two helpers must already be generated on the same `impl ActiveModel`
200/// block (see the example), and `async_trait` must be available as a
201/// dependency of the crate using this macro.
202///
203/// # Examples
204/// ```ignore
205/// #[entity_id(rand_i64())]
206/// #[entity_timestamp]
207/// impl ActiveModel {}
208///
209/// #[entity_behavior]
210/// impl ActiveModelBehavior for ActiveModel {}
211/// ```
212#[proc_macro_attribute]
213pub fn entity_behavior(_: TokenStream, input: TokenStream) -> TokenStream {
214    let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);
215
216    entity.items.push(syn::parse_quote!(
217        async fn before_save<C>(self, _: &C, insert: bool) -> Result<Self, sea_orm::DbErr>
218        where
219            C: sea_orm::ConnectionTrait,
220        {
221            let mut new = self.clone();
222            self.entity_id(&mut new, insert);
223            self.entity_timestamp(&mut new, insert);
224            Ok(new)
225        }
226    ));
227    quote! {
228        #[async_trait::async_trait]
229        #entity
230    }
231    .into()
232}
233
234#[cfg(feature = "seaorm")]
235/// Implement `From` for entity to partial entity.
236/// The fields should be exactly the same.
237///
238/// # Examples
239/// ```ignore
240/// #[partial_entity(users::Model)]
241/// #[derive(Serialize)]
242/// struct Rsp {
243///     pub id: i64,
244/// }
245///
246/// let y = users::Model {
247///     id: ...,
248///     name: ...,
249///     ...
250/// };
251/// let x: Rsp = y.into();
252/// ```
253#[proc_macro_attribute]
254pub fn partial_entity(attr: TokenStream, input: TokenStream) -> TokenStream {
255    let attr = syn::parse_macro_input!(attr as syn::ExprPath);
256    let input = syn::parse_macro_input!(input as syn::ItemStruct);
257    let name = &input.ident;
258    let mut fields = Vec::new();
259    for i in &input.fields {
260        let field_name = &i.ident;
261        fields.push(quote!(#field_name: val.#field_name,));
262    }
263
264    quote! {
265        #input
266        impl From<#attr> for #name {
267            fn from(val: #attr) -> Self {
268                #name {
269                    #(#fields)*
270                }
271            }
272        }
273    }
274    .into()
275}