libsql_orm_macros/lib.rs
1//! Procedural macros for libsql-orm
2//!
3//! This crate provides derive macros and attribute macros for the libsql-orm library,
4//! enabling automatic implementation of ORM traits and convenient model definitions.
5//!
6//! # Derive Macros
7//!
8//! ## `#[derive(Model)]`
9//!
10//! Automatically implements the `Model` trait for a struct, providing all CRUD operations
11//! and ORM functionality.
12//!
13//! ```rust
14//! use libsql_orm::Model;
15//! use serde::{Serialize, Deserialize};
16//!
17//! #[derive(Model, Serialize, Deserialize)]
18//! struct User {
19//! pub id: Option<i64>,
20//! pub name: String,
21//! pub email: String,
22//! }
23//! ```
24//!
25//! # Attribute Macros
26//!
27//! ## `#[table_name("custom_name")]`
28//!
29//! Specifies a custom table name for the model. By default, the table name is derived
30//! from the struct name converted to lowercase.
31//!
32//! ```rust
33//! use libsql_orm::Model;
34//! use serde::{Serialize, Deserialize};
35//!
36//! #[derive(Model, Serialize, Deserialize)]
37//! #[table_name("custom_users")]
38//! struct User {
39//! pub id: Option<i64>,
40//! pub name: String,
41//! }
42//! ```
43//!
44//! ## `#[orm_column(...)]`
45//!
46//! Specifies custom column properties for database fields.
47//!
48//! ```rust
49//! use libsql_orm::Model;
50//! use serde::{Serialize, Deserialize};
51//!
52//! #[derive(Model, Serialize, Deserialize)]
53//! struct User {
54//! #[orm_column(type = "INTEGER PRIMARY KEY AUTOINCREMENT")]
55//! pub id: Option<i64>,
56//!
57//! #[orm_column(not_null, unique)]
58//! pub email: String,
59//!
60//! #[orm_column(type = "TEXT DEFAULT 'active'")]
61//! pub status: String,
62//! }
63//! ```
64//!
65//! # Function-like Macros
66//!
67//! ## `generate_migration!(Model)`
68//!
69//! Generates a database migration from a model definition.
70//!
71//! ```rust
72//! use libsql_orm::{generate_migration, MigrationManager};
73//!
74//! let migration = generate_migration!(User);
75//! let manager = MigrationManager::new(db);
76//! manager.execute_migration(&migration).await?;
77//! ```
78
79use proc_macro::TokenStream;
80use quote::quote;
81use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields, Lit, Type};
82
83/// Column attribute macro for defining SQL column properties
84///
85/// This attribute allows you to specify custom SQL column properties for struct fields.
86///
87/// # Supported attributes:
88/// - `type = "SQL_TYPE"` - Custom SQL type definition
89/// - `not_null` - Add NOT NULL constraint
90/// - `unique` - Add UNIQUE constraint
91/// - `primary_key` - Mark as PRIMARY KEY
92/// - `auto_increment` - Add AUTOINCREMENT (for INTEGER PRIMARY KEY)
93///
94/// # Examples:
95///
96/// ```rust
97/// #[derive(Model)]
98/// struct User {
99/// #[orm_column(type = "INTEGER PRIMARY KEY AUTOINCREMENT")]
100/// pub id: Option<i64>,
101///
102/// #[orm_column(not_null, unique)]
103/// pub email: String,
104///
105/// #[orm_column(type = "TEXT DEFAULT 'active'")]
106/// pub status: String,
107/// }
108/// ```
109#[proc_macro_attribute]
110pub fn orm_column(_args: TokenStream, input: TokenStream) -> TokenStream {
111 // For now, just return the input unchanged
112 // We'll parse the column attributes in the Model macro
113 input
114}
115
116/// Derive macro for the Model trait
117///
118/// Automatically implements the `Model` trait for a struct, providing CRUD operations
119/// and ORM functionality. The macro analyzes the struct fields to generate appropriate
120/// SQL schema and conversion methods.
121///
122/// # Attributes:
123/// - `#[table_name("custom_name")]` - Specify custom table name
124/// - `#[orm_column(...)]` - Configure column properties
125///
126/// # Examples:
127///
128/// ```rust
129/// use libsql_orm::Model;
130/// use serde::{Serialize, Deserialize};
131///
132/// #[derive(Model, Serialize, Deserialize)]
133/// #[table_name("users")]
134/// struct User {
135/// pub id: Option<i64>,
136/// pub name: String,
137/// pub email: String,
138/// }
139/// ```
140#[proc_macro_derive(Model, attributes(table_name, orm_column))]
141pub fn derive_model(input: TokenStream) -> TokenStream {
142 let input = parse_macro_input!(input as DeriveInput);
143 let name = input.ident;
144
145 // Extract table name from attributes or use default
146 let table_name =
147 extract_table_name(&input.attrs).unwrap_or_else(|| name.to_string().to_lowercase());
148
149 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
150
151 // Extract field names and column metadata for columns
152 let (field_names, column_definitions, boolean_field_names, boolean_flags) =
153 if let Data::Struct(data) = &input.data {
154 if let Fields::Named(fields) = &data.fields {
155 let mut field_names = Vec::new();
156 let mut column_defs = Vec::new();
157 let mut bool_field_names = Vec::new();
158 let mut bool_flags = Vec::new();
159
160 for field in &fields.named {
161 let field_name = &field.ident;
162 let field_name_str = quote! { stringify!(#field_name) };
163 field_names.push(field_name_str);
164
165 // Parse column attributes to get SQL definition
166 let column_def = parse_column_definition(field);
167 column_defs.push(column_def);
168
169 // Extract field type information for conversion
170 let field_type = &field.ty;
171 let is_bool = is_boolean_type(field_type);
172 bool_field_names.push(quote! { stringify!(#field_name) });
173 bool_flags.push(is_bool);
174 }
175
176 (field_names, column_defs, bool_field_names, bool_flags)
177 } else {
178 (vec![], vec![], vec![], vec![])
179 }
180 } else {
181 (vec![], vec![], vec![], vec![])
182 };
183
184 let expanded = quote! {
185 impl #impl_generics libsql_orm::Model for #name #ty_generics #where_clause {
186 fn table_name() -> &'static str {
187 #table_name
188 }
189
190 fn get_primary_key(&self) -> Option<i64> {
191 self.id
192 }
193
194 fn set_primary_key(&mut self, id: i64) {
195 self.id = Some(id);
196 }
197
198 fn columns() -> Vec<&'static str> {
199 vec![#(#field_names),*]
200 }
201
202 /// Generate SQL for creating the table
203 fn migration_sql() -> String {
204 let columns = vec![#(#column_definitions),*];
205 format!(
206 "CREATE TABLE IF NOT EXISTS {} (\n {}\n)",
207 Self::table_name(),
208 columns.join(",\n ")
209 )
210 }
211
212 fn to_map(&self) -> libsql_orm::Result<std::collections::HashMap<String, libsql_orm::Value>> {
213 use serde_json;
214 let json = serde_json::to_value(self)?;
215 let map: std::collections::HashMap<String, serde_json::Value> = serde_json::from_value(json)?;
216
217 let mut result = std::collections::HashMap::new();
218 for (k, v) in map {
219 let value = match v {
220 serde_json::Value::Null => libsql_orm::Value::Null,
221 serde_json::Value::Bool(b) => libsql_orm::Value::Boolean(b),
222 serde_json::Value::Number(n) => {
223 if let Some(i) = n.as_i64() {
224 libsql_orm::Value::Integer(i)
225 } else if let Some(f) = n.as_f64() {
226 libsql_orm::Value::Real(f)
227 } else {
228 libsql_orm::Value::Text(n.to_string())
229 }
230 }
231 serde_json::Value::String(s) => libsql_orm::Value::Text(s),
232 serde_json::Value::Array(_) => libsql_orm::Value::Text(serde_json::to_string(&v)?),
233 serde_json::Value::Object(_) => libsql_orm::Value::Text(serde_json::to_string(&v)?),
234 };
235 result.insert(k, value);
236 }
237 Ok(result)
238 }
239
240 fn from_map(map: std::collections::HashMap<String, libsql_orm::Value>) -> libsql_orm::Result<Self> {
241 use serde_json;
242 let mut json_map = serde_json::Map::new();
243
244 for (k, v) in map {
245 let json_value = match v {
246 libsql_orm::Value::Null => serde_json::Value::Null,
247 libsql_orm::Value::Boolean(b) => serde_json::Value::Bool(b),
248 libsql_orm::Value::Integer(i) => {
249 // Convert integers to booleans for known boolean fields
250 let field_name = k.as_str();
251 let mut is_boolean_field = false;
252 #(
253 if field_name == #boolean_field_names {
254 is_boolean_field = #boolean_flags;
255 }
256 )*
257
258 if is_boolean_field {
259 serde_json::Value::Bool(i != 0)
260 } else {
261 serde_json::Value::Number(serde_json::Number::from(i))
262 }
263 }
264 libsql_orm::Value::Real(f) => {
265 if let Some(n) = serde_json::Number::from_f64(f) {
266 serde_json::Value::Number(n)
267 } else {
268 serde_json::Value::String(f.to_string())
269 }
270 }
271 libsql_orm::Value::Text(s) => serde_json::Value::String(s),
272 libsql_orm::Value::Blob(b) => {
273 serde_json::Value::Array(b.into_iter().map(|byte| serde_json::Value::Number(serde_json::Number::from(byte))).collect())
274 }
275 };
276 json_map.insert(k, json_value);
277 }
278
279 let json_value = serde_json::Value::Object(json_map);
280 let result: Self = serde_json::from_value(json_value)?;
281 Ok(result)
282 }
283 }
284
285 // Note: Clone is already derived in the struct definition
286 };
287
288 TokenStream::from(expanded)
289}
290
291/// Parse column definition from field attributes
292fn parse_column_definition(field: &Field) -> proc_macro2::TokenStream {
293 let field_name = &field.ident;
294 let field_name_str = field_name.as_ref().unwrap().to_string();
295
296 // Default column definitions based on field type
297 let default_def = match &field.ty {
298 Type::Path(type_path) => {
299 let type_name = &type_path.path.segments.last().unwrap().ident;
300 match type_name.to_string().as_str() {
301 "i64" => format!("{field_name_str} INTEGER"),
302 "i32" => format!("{field_name_str} INTEGER"),
303 "f64" => format!("{field_name_str} REAL"),
304 "f32" => format!("{field_name_str} REAL"),
305 "bool" => format!("{field_name_str} BOOLEAN"),
306 "String" => format!("{field_name_str} TEXT"),
307 _ => format!("{field_name_str} TEXT"),
308 }
309 }
310 _ => format!("{field_name_str} TEXT"),
311 };
312
313 // Check for orm_column attributes
314 for attr in &field.attrs {
315 if attr.path().is_ident("orm_column") {
316 let mut column_type = None;
317 let mut not_null = false;
318 let mut unique = false;
319 let mut primary_key = false;
320 let mut auto_increment = false;
321
322 // Parse the nested meta items
323 let _ = attr.parse_nested_meta(|meta| {
324 if meta.path.is_ident("type") {
325 if let Ok(value) = meta.value() {
326 let lit: Lit = value.parse()?;
327 if let Lit::Str(lit_str) = lit {
328 column_type = Some(lit_str.value());
329 }
330 }
331 } else if meta.path.is_ident("not_null") {
332 not_null = true;
333 } else if meta.path.is_ident("unique") {
334 unique = true;
335 } else if meta.path.is_ident("primary_key") {
336 primary_key = true;
337 } else if meta.path.is_ident("auto_increment") {
338 auto_increment = true;
339 }
340 Ok(())
341 });
342
343 let mut column_def = column_type.unwrap_or_else(|| default_def.clone());
344 if primary_key {
345 column_def = format!("{column_def} PRIMARY KEY");
346 }
347 if auto_increment {
348 column_def = format!("{column_def} AUTOINCREMENT");
349 }
350 if not_null {
351 column_def = format!("{column_def} NOT NULL");
352 }
353 if unique {
354 column_def = format!("{column_def} UNIQUE");
355 }
356 return quote! { #column_def };
357 }
358 }
359 // Return default definition
360 quote! { #default_def }
361}
362
363/// Extract table name from struct attributes
364fn extract_table_name(attrs: &[Attribute]) -> Option<String> {
365 for attr in attrs {
366 if attr.path().is_ident("table_name") {
367 if let Ok(Lit::Str(lit_str)) = attr.parse_args::<Lit>() {
368 return Some(lit_str.value());
369 }
370 }
371 }
372 None
373}
374
375/// Check if a type is a boolean type
376fn is_boolean_type(ty: &Type) -> bool {
377 if let Type::Path(type_path) = ty {
378 if let Some(segment) = type_path.path.segments.last() {
379 let type_name = &segment.ident;
380 return type_name == "bool";
381 }
382 }
383 false
384}
385
386/// Macro to generate migration from a model
387///
388/// Creates a migration instance from a model's schema definition. The migration
389/// will contain the SQL necessary to create the table for the model.
390///
391/// # Examples:
392///
393/// ```rust
394/// use libsql_orm::{generate_migration, MigrationManager};
395///
396/// // Generate migration for User model
397/// let user_migration = generate_migration!(User);
398///
399/// // Execute the migration
400/// let manager = MigrationManager::new(db);
401/// manager.execute_migration(&user_migration).await?;
402/// ```
403#[proc_macro]
404pub fn generate_migration(input: TokenStream) -> TokenStream {
405 let input = parse_macro_input!(input as syn::Ident);
406
407 let expanded = quote! {
408 {
409 let sql = #input::migration_sql();
410 libsql_orm::MigrationManager::create_migration(
411 &format!("create_table_{}", #input::table_name()),
412 &sql
413 )
414 }
415 };
416
417 TokenStream::from(expanded)
418}