rusticx_derive/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 parse_macro_input, Attribute, Data, DeriveInput, Expr, Ident, Meta, MetaNameValue, Type,
5 TypePath,
6};
7
8/// Derives the `SQLModel` trait for a struct, allowing it to be used as a database model.
9///
10/// This macro automatically generates the necessary implementations for the `SQLModel`
11/// trait based on the struct's fields and the attributes applied to them.
12///
13/// # Usage
14///
15/// Apply `#[derive(Model)]` to your struct definition. You can also use `#[model(...)]`
16/// attributes on the struct itself and on individual fields to configure the model
17/// mapping and behavior.
18///
19/// ```rust
20/// use rusticx_derive::Model; // Assuming the macro is in a crate named rusticx_derive
21/// use uuid::Uuid; // Assuming you use the 'uuid' crate
22/// use chrono::NaiveDateTime; // Assuming you use the 'chrono' crate
23///
24/// #[derive(Model, Debug, serde::Serialize, serde::Deserialize)]
25/// #[model(table = "my_users")] // Optional: specify a custom table name
26/// struct User {
27/// #[model(primary_key, auto_increment)] // Marks 'id' as primary key with auto-increment
28/// // #[model(primary_key, uuid)] // Alternatively, for UUID primary keys
29/// id: Option<i32>, // Use Option<i32> for auto-increment, Uuid for uuid
30///
31/// name: String, // Maps to a text/varchar column
32///
33/// #[model(column = "user_age")] // Optional: specify a custom column name
34/// age: i32, // Maps to an integer column
35///
36/// #[model(nullable)] // Marks the 'email' column as nullable
37/// email: Option<String>,
38///
39/// #[model(default = "'active'")] // Sets a default value for the 'status' column
40/// status: String,
41///
42/// #[model(sql_type = "JSONB")] // Specify a custom SQL type
43/// metadata: serde_json::Value,
44///
45/// #[model(skip)] // This field will be ignored by the ORM
46/// temp_data: String,
47///
48/// #[model(auto_increment)] // Only valid on primary_key fields, will be ignored otherwise
49/// another_id: i32,
50///
51/// #[model(uuid)] // Can be used on non-primary key UUID fields if needed
52/// unique_id: Uuid,
53///
54/// created_at: NaiveDateTime, // Maps to a datetime column
55/// }
56/// ```
57///
58/// # Struct Attributes (`#[model(...)]` on the struct)
59///
60/// * `#[model(table = "custom_name")]`: Specifies the database table name for this model.
61/// Defaults to the struct name (e.g., `User` -> `User`).
62///
63/// # Field Attributes (`#[model(...)]` on fields)
64///
65/// * `#[model(primary_key)]`: Designates this field as the primary key for the table.
66/// Exactly one field should be marked as the primary key.
67/// * `#[model(column = "custom_name")]`: Specifies the database column name for this field.
68/// Defaults to the field name converted to lowercase.
69/// * `#[model(default = "SQL_DEFAULT_VALUE")]`: Sets a SQL default value for the column.
70/// The value is inserted directly into the SQL `CREATE TABLE` statement. Use
71/// appropriate quoting for string literals (e.g., `"'active'"`).
72/// * `#[model(nullable)]`: Explicitly marks the column as nullable (`NULL` in SQL).
73/// Fields with `Option<T>` type are automatically treated as nullable. This attribute
74/// is useful for non-Option types that should still allow `NULL`.
75/// * `#[model(sql_type = "SQL_TYPE_STRING")]`: Specifies a custom SQL data type for the column.
76/// This overrides the default type mapping based on the Rust type.
77/// * `#[model(skip)]`: Excludes this field from the generated SQL model definition (CREATE TABLE,
78/// INSERT, UPDATE) and from deserialization (`from_row`).
79/// * `#[model(auto_increment)]`: Applicable only to `primary_key` fields. Adds the
80/// database-specific syntax for auto-incrementing integer primary keys (`SERIAL` or
81/// `GENERATED ALWAYS AS IDENTITY` for PostgreSQL, `AUTO_INCREMENT` for MySQL,
82/// `AUTOINCREMENT` for SQLite). The field type *must* be an integer type, usually `Option<i32>`.
83/// * `#[model(uuid)]`: Applicable only to `primary_key` fields. Adds database-specific
84/// default value generation for UUID primary keys (`gen_random_uuid()` for PostgreSQL,
85/// `UUID()` for MySQL, and a standard UUID generation expression for SQLite). The field
86/// type *must* be `uuid::Uuid` or `Option<uuid::Uuid>`.
87///
88/// # Generated SQL Types Mapping
89///
90/// The macro attempts to infer SQL types based on common Rust types:
91/// * `i8`, `i16`, `i32`, `u8`, `u16`, `u32`: `INTEGER`
92/// * `i64`, `u64`: `BIGINT`
93/// * `f32`, `f64`: `FLOAT`
94/// * `bool`: `BOOLEAN`
95/// * `String`, `str`: `TEXT`
96/// * `Uuid` (from `uuid` crate): `TEXT` (UUIDs are typically stored as text or byte arrays)
97/// * `NaiveDate` (from `chrono` crate): `DATE`
98/// * `NaiveTime` (from `chrono` crate): `TIME`
99/// * `NaiveDateTime`, `DateTime` (from `chrono` crate): `DATETIME` or `TIMESTAMP` depending on DB
100/// * `Vec<u8>`: `BLOB`
101/// * `Option<T>`: The underlying type `T`'s mapping is used, and the column is marked nullable.
102///
103/// You can override this mapping using `#[model(sql_type = "...")]`.
104///
105/// # Requirements
106///
107/// * The derived struct must have named fields.
108/// * The struct must derive `serde::Deserialize` for `from_row` to work.
109/// * If using `Uuid` or `chrono` types, ensure the respective crates are in your `Cargo.toml`.
110/// * If using the `uuid` attribute on a primary key, the field type must be `Uuid` or `Option<Uuid>`.
111/// * If using the `auto_increment` attribute on a primary key, the field type must be an integer type, typically `Option<i32>`.
112#[proc_macro_derive(Model, attributes(model))]
113pub fn derive_model(input: TokenStream) -> TokenStream {
114 // Parse the input token stream into a DeriveInput syntax tree
115 let input = parse_macro_input!(input as DeriveInput);
116 // Get the name of the struct
117 let name = &input.ident;
118
119 // Extract the table name from the struct attributes. If not found,
120 // default to the struct name as is (without pluralizing or lowercasing).
121 let table_name = extract_table_name(&input.attrs)
122 .unwrap_or_else(|| name.to_string());
123
124 // Ensure the derived item is a struct with named fields.
125 // Panic otherwise with a descriptive error message.
126 let fields = match &input.data {
127 Data::Struct(data) => match &data.fields {
128 syn::Fields::Named(fields) => &fields.named,
129 _ => panic!("#[derive(Model)] can only be applied to structs with named fields"),
130 },
131 _ => panic!("#[derive(Model)] can only be applied to structs"),
132 };
133
134 // Variables to collect information about fields for code generation
135 let mut primary_key_field: Option<Ident> = None;
136 let mut primary_key_type: Option<Type> = None;
137 let mut _pk_is_auto_increment = false; // Track if PK is auto-increment (for generated code logic if needed)
138 let mut pk_is_uuid = false; // Track if PK is UUID (for generated code logic if needed)
139 let mut field_sql_defs = Vec::new(); // Collect SQL column definitions (name, type, constraints)
140 let mut field_names = Vec::new(); // Collect database column names
141 let mut field_to_sql_values = Vec::new(); // Collect code snippets for extracting field values for SQL binding
142 let mut field_from_row = Vec::new(); // Collect code snippets for deserializing fields from a row (JSON value)
143 let mut field_idents = Vec::new(); // Collect original field idents
144 let mut field_str_names = Vec::new(); // Collect original field names as strings
145
146 // Iterate over each field in the struct
147 for field in fields {
148 let field_ident = field.ident.clone().unwrap(); // Get the field identifier
149 let field_name = field_ident.to_string(); // Get the field name as a string
150 let mut column_name = field_name.clone(); // Initialize column name, defaults to field name
151 let mut is_primary_key = false;
152 let mut has_default = false;
153 let mut default_value = String::new();
154 let mut is_nullable = false; // Explicit #[model(nullable)]
155 let mut custom_type = None; // #[model(sql_type = "...")]
156 let mut skip = false; // #[model(skip)]
157 let mut auto_increment = false; // #[model(auto_increment)]
158 let mut uuid_pk = false; // #[model(uuid)] for primary key
159
160 // Process attributes on the current field
161 for attr in &field.attrs {
162 // Check if the attribute is our custom #[model(...)] attribute
163 if !attr.path().is_ident("model") {
164 continue;
165 }
166
167 // Parse the attribute's arguments (e.g., primary_key, column="...")
168 let parsed = attr.parse_args_with(
169 syn::punctuated::Punctuated::<Meta, syn::token::Comma>::parse_terminated,
170 );
171
172 // Process the parsed meta items within the attribute
173 if let Ok(items) = parsed {
174 for meta in items {
175 match meta {
176 // Handle flag attributes like `primary_key` or `nullable`
177 Meta::Path(path) => {
178 if path.is_ident("primary_key") {
179 is_primary_key = true;
180 primary_key_field = Some(field_ident.clone());
181 primary_key_type = Some(field.ty.clone());
182 } else if path.is_ident("nullable") {
183 is_nullable = true;
184 } else if path.is_ident("skip") {
185 skip = true;
186 } else if path.is_ident("auto_increment") {
187 auto_increment = true;
188 _pk_is_auto_increment = true; // Mark PK as auto-incrementing globally
189 } else if path.is_ident("uuid") {
190 uuid_pk = true;
191 pk_is_uuid = true; // Mark PK as UUID globally
192 }
193 }
194 // Handle name-value attributes like `column = "..."` or `default = "..."`
195 Meta::NameValue(MetaNameValue { path, value, .. }) => {
196 if path.is_ident("column") {
197 if let Expr::Lit(expr_lit) = value {
198 if let syn::Lit::Str(lit_str) = expr_lit.lit {
199 column_name = lit_str.value(); // Set custom column name
200 }
201 }
202 } else if path.is_ident("default") {
203 if let Expr::Lit(expr_lit) = value {
204 if let syn::Lit::Str(lit_str) = expr_lit.lit {
205 has_default = true;
206 default_value = lit_str.value(); // Set default value string
207 }
208 }
209 } else if path.is_ident("sql_type") {
210 if let Expr::Lit(expr_lit) = value {
211 if let syn::Lit::Str(lit_str) = expr_lit.lit {
212 custom_type = Some(lit_str.value()); // Set custom SQL type string
213 }
214 }
215 }
216 }
217 _ => {
218 // Ignore other meta types for forward compatibility
219 }
220 }
221 }
222 } else {
223 // Handle parsing errors for the attribute arguments
224 let err = parsed.unwrap_err();
225 return TokenStream::from(err.to_compile_error());
226 }
227 }
228
229 // If the field is marked to be skipped, continue to the next field
230 if skip {
231 continue;
232 }
233
234 // Store field information for later use in generated code
235 field_idents.push(field_ident.clone());
236 field_str_names.push(field_name.clone());
237 field_names.push(column_name.clone());
238
239 // Generate code snippet to extract the field's value.
240 // Assumes the field type implements `Clone` and can be converted to `Box<dyn rusticx::ToSqlConvert>`.
241 // The `rusticx::ToSqlConvert` trait would need to handle the actual type-specific conversion.
242 let field_to_sql_value = quote! {
243 // Clone the field value and box it as a trait object.
244 // The `rusticx::ToSqlConvert` trait should provide a method
245 // to convert the underlying type to database-specific parameters.
246 Box::new(self.#field_ident.clone()) as Box<dyn rusticx::ToSqlConvert>
247 };
248 field_to_sql_values.push(field_to_sql_value);
249
250 // Determine if the field is semantically optional (either Option<T> or explicitly nullable)
251 let is_option = is_nullable || is_option_type(&field.ty);
252 // Generate code snippet to deserialize the field from a JSON value (representing a database row)
253 let field_from_json = generate_from_json(&field_ident, &column_name, &field.ty, is_option);
254 field_from_row.push(field_from_json);
255
256 // Determine the SQL type definition based on custom type or Rust type mapping
257 let sql_type = if let Some(custom) = custom_type {
258 // If a custom SQL type is specified, use it
259 quote! { rusticx::SqlType::Custom(#custom.to_string()) }
260 } else {
261 // Otherwise, map the Rust type to a generic SqlType enum variant
262 let rust_type = &field.ty;
263 generate_sql_type(rust_type) // Calls helper function for mapping
264 };
265
266 // Generate the SQL column definition string part (e.g., "name TEXT NOT NULL")
267 let sql_def = quote! {
268 {
269 // Start with column name and its determined SQL type based on DB type
270 let mut part = format!("\"{}\" {}", #column_name, match db_type {
271 rusticx::DatabaseType::PostgreSQL => #sql_type.pg_type().to_string(),
272 rusticx::DatabaseType::MySQL => #sql_type.mysql_type().to_string(),
273 rusticx::DatabaseType::SQLite => #sql_type.sqlite_type().to_string(),
274 });
275
276 // Add PRIMARY KEY constraint if applicable
277 if #is_primary_key {
278 part.push_str(" PRIMARY KEY");
279
280 // Add auto-increment or UUID default based on database type
281 if #auto_increment {
282 // Auto-increment specific syntax per database
283 match db_type {
284 rusticx::DatabaseType::PostgreSQL => part.push_str(" GENERATED ALWAYS AS IDENTITY"),
285 rusticx::DatabaseType::MySQL => part.push_str(" AUTO_INCREMENT"),
286 rusticx::DatabaseType::SQLite => part.push_str(" AUTOINCREMENT"),
287 }
288 } else if #uuid_pk {
289 // UUID default function specific syntax per database
290 match db_type {
291 rusticx::DatabaseType::PostgreSQL => part.push_str(" DEFAULT gen_random_uuid()"),
292 // MySQL's UUID() includes hyphens, stored as TEXT
293 rusticx::DatabaseType::MySQL => part.push_str(" DEFAULT (UUID())"),
294 // SQLite requires a custom expression for UUID generation
295 // This is a common pattern, might need a dedicated function in the crate
296 rusticx::DatabaseType::SQLite => part.push_str(" DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(6))))"),
297 };
298 }
299 }
300
301 // Add NOT NULL constraint if not nullable and not primary key
302 // Primary keys are implicitly NOT NULL unless explicitly nullable
303 if !#is_option && !#is_primary_key { // Check against is_option which considers Option<T> and #[model(nullable)]
304 part.push_str(" NOT NULL");
305 }
306
307 // Add DEFAULT value constraint if specified
308 if #has_default {
309 part.push_str(&format!(" DEFAULT {}", #default_value));
310 }
311
312 part // Return the generated SQL part for this field
313 }
314 };
315
316 field_sql_defs.push(sql_def); // Add the generated SQL definition to the list
317 }
318
319 // Determine the identifier for the primary key field for use in `primary_key_value` and `set_primary_key`.
320 // Defaults to an identifier "id" if no field was marked as primary key (though this should ideally be a user error).
321 let pk_ident = primary_key_field.unwrap_or_else(|| Ident::new("id", name.span()));
322
323 // Collect column names as string literals for the `field_names` method
324 let field_name_literals: Vec<_> = field_names.iter().map(|name| quote! { #name }).collect();
325
326 // Generate the implementation for `primary_key_value`.
327 // This needs to handle `Option<T>` and different primary key types (int vs UUID).
328 let get_primary_key_code = match primary_key_type {
329 Some(ref pk_type) => {
330 if is_option_type(pk_type) {
331 // Handle Option<T> primary keys
332 if pk_is_uuid {
333 // If Option<Uuid>, clone the Uuid reference
334 quote! {
335 // Access the Option<Uuid> field and map to clone the Uuid if Some
336 self.#pk_ident.as_ref().map(|val| val.clone())
337 }
338 } else {
339 // If Option<i32> or other Option<Integer>
340 // Assumes primary keys are returned as i32 by the ORM's fetch logic.
341 // This might need adjustment based on the actual ORM implementation's return type.
342 quote! {
343 // Access the Option<i32> field and map to the i32 if Some
344 self.#pk_ident.as_ref().map(|val| *val) // Changed to return the actual i32
345 }
346 }
347 } else {
348 // Handle non-Option primary keys
349 if pk_is_uuid {
350 // If Uuid (non-Option), just clone it and wrap in Some
351 quote! { Some(self.#pk_ident.clone()) }
352 } else {
353 // If i32 or other Integer (non-Option)
354 // Assumes primary keys are returned as i32 by the ORM's fetch logic.
355 quote! { Some(self.#pk_ident) } // Changed to return the actual i32
356 }
357 }
358 },
359 // Default case if no primary key was explicitly marked. Assumes an 'id' field exists.
360 // This case should ideally be an error or handled more robustly if PK is mandatory.
361 None => {
362 // Fallback logic assuming an `id` field of type `Option<i32>`
363 // This is less ideal; enforcing a #[model(primary_key)] is better.
364 // If `id` field doesn't exist or isn't Option<i32>, this will cause compilation errors.
365 quote! {
366 // Access the Option<i32> field (assuming `id`)
367 self.#pk_ident.as_ref().map(|val| *val) // Assuming id is Option<i32>
368 }
369 }
370 };
371
372 // Generate the implementation for `set_primary_key`.
373 // This assumes the primary key field is an `Option<i32>`.
374 // This needs refinement if UUID or non-Option primary keys are supported by `set_primary_key`.
375 // Currently, `set_primary_key` takes `i32`, which fits `Option<i32>` PKs set after insert.
376 // If PK is Uuid, this method signature might need to change in the trait.
377 // Assuming for now that `set_primary_key` is only used for auto-generated *integer* IDs.
378 let set_primary_key_code = quote! {
379 // Set the primary key field value, assuming it's Option<i32>
380 self.#pk_ident = Some(id);
381 };
382
383
384 // Construct the final generated code for the SQLModel implementation
385 let expanded = quote! {
386 // Implement the SQLModel trait for the target struct
387 impl rusticx::SQLModel for #name {
388 /// Returns the database table name for this model.
389 ///
390 /// This is derived from the struct name or specified using
391 /// the `#[model(table = "...")]` attribute.
392 fn table_name() -> String {
393 #table_name.to_string()
394 }
395
396 /// Returns the database column name of the primary key field.
397 ///
398 /// This is the field marked with `#[model(primary_key)]`.
399 /// Defaults to "id" if no primary key is explicitly marked (less ideal).
400 fn primary_key_field() -> String {
401 // Stringify the ident of the primary key field
402 stringify!(#pk_ident).to_string()
403 }
404
405 /// Returns the value of the primary key field, if present.
406 ///
407 /// Returns `Some(value)` if the primary key is set, `None` otherwise.
408 ///
409 /// # Note
410 /// The return type `Option<i32>` is assumed for auto-increment integer keys.
411 /// If using UUIDs or other primary key types, the trait method signature
412 /// might need adjustment in the `rusticx` crate.
413 fn primary_key_value(&self) -> Option<i32> {
414 // Execute the generated code snippet to get the PK value
415 // The generated code handles Option<T> and type conversion (assumed i32 for now)
416 // TODO: Refine signature to Option<Self::PkType> if PkType is added to trait
417 #get_primary_key_code
418 }
419
420 /// Sets the value of the primary key field.
421 ///
422 /// This is typically used after an INSERT operation with an auto-generated ID.
423 ///
424 /// # Arguments
425 ///
426 /// * `id`: The integer value of the primary key.
427 ///
428 /// # Note
429 /// This method assumes the primary key field is of type `Option<i32>`.
430 /// It will cause a compilation error if the primary key field has a different type.
431 /// A more generic trait method or separate methods for different PK types might be needed.
432 fn set_primary_key(&mut self, id: i32) {
433 // Execute the generated code snippet to set the PK value
434 #set_primary_key_code
435 }
436
437 /// Generates the SQL `CREATE TABLE` statement for this model.
438 ///
439 /// The statement is tailored to the specified database type (`db_type`).
440 /// Includes column definitions, primary key constraints, nullability,
441 /// defaults, and auto-increment/UUID syntax.
442 ///
443 /// # Arguments
444 ///
445 /// * `db_type`: The type of the database (PostgreSQL, MySQL, SQLite).
446 ///
447 /// # Returns
448 ///
449 /// A string containing the `CREATE TABLE` SQL statement.
450 fn create_table_sql(db_type: &rusticx::DatabaseType) -> String {
451 // Start the CREATE TABLE statement
452 let mut sql = format!("CREATE TABLE IF NOT EXISTS \"{}\" (", Self::table_name());
453 // Collect the generated SQL definitions for each field
454 let fields = vec![#(#field_sql_defs),*];
455 // Join field definitions with commas and close the statement
456 sql.push_str(&fields.join(", "));
457 sql.push(')');
458 sql
459 }
460
461 /// Returns a vector of static strings representing the database column names
462 /// for all non-skipped fields in the model.
463 ///
464 /// Used for constructing SELECT or INSERT statements.
465 fn field_names() -> Vec<&'static str> {
466 // Return a vector of string literals for column names
467 vec![#(#field_name_literals),*]
468 }
469
470 /// Returns a vector of boxed trait objects (`ToSqlConvert`) representing
471 /// the values of all non-skipped fields in the model.
472 ///
473 /// Used for binding values in INSERT or UPDATE statements.
474 /// Assumes field types implement `Clone` and can be converted to `ToSqlConvert`.
475 fn to_sql_field_values(&self) -> Vec<Box<dyn rusticx::ToSqlConvert>> {
476 // Return a vector of boxed trait objects for field values
477 vec![#(#field_to_sql_values),*]
478 }
479
480 /// Deserializes a database row (represented as a `serde_json::Value::Object`)
481 /// into an instance of the model struct.
482 ///
483 /// # Arguments
484 ///
485 /// * `row`: A reference to a `serde_json::Value`, expected to be a JSON object
486 /// where keys are column names and values are column data.
487 ///
488 /// # Returns
489 ///
490 /// Returns `Ok(Self)` on successful deserialization, or a `RusticxError`
491 /// if the input is not a JSON object or if field deserialization fails.
492 fn from_row(row: &serde_json::Value) -> Result<Self, rusticx::RusticxError> {
493 // Ensure the input value is a JSON object
494 if !row.is_object() {
495 return Err(rusticx::RusticxError::DeserializationError(
496 "Input for from_row is not a JSON object".to_string()
497 ));
498 }
499
500 // Get a reference to the JSON object
501 let obj = row.as_object().unwrap(); // Safe to unwrap because we checked is_object()
502
503 // Construct the struct instance by deserializing each field
504 Ok(Self {
505 #(#field_from_row),* // Execute the generated code snippets for each field
506 })
507 }
508 }
509 };
510
511 // Return the generated code as a TokenStream
512 TokenStream::from(expanded)
513}
514
515/// Helper function to check if a given Rust type is an `Option<T>`.
516fn is_option_type(ty: &Type) -> bool {
517 // Check if the type is a path (like `std::option::Option`)
518 if let Type::Path(TypePath { path, .. }) = ty {
519 // Get the last segment of the path (e.g., `Option`)
520 if let Some(segment) = path.segments.last() {
521 // Check if the identifier of the last segment is "Option"
522 return segment.ident == "Option";
523 }
524 }
525 false // Not an Option type
526}
527
528/// Helper function to generate the code snippet for deserializing a single field
529/// from a `serde_json::Value` object (representing a database row).
530///
531/// Handles both optional (`Option<T>`) and required fields.
532///
533/// # Arguments
534///
535/// * `field_ident`: The identifier of the struct field.
536/// * `column_name`: The database column name corresponding to the field.
537/// * `_field_type`: The Rust type of the field (used implicitly by `serde_json::from_value`).
538/// * `is_optional`: Boolean indicating if the field is `Option<T>` or marked nullable.
539///
540/// # Returns
541///
542/// A `proc_macro2::TokenStream` containing the code to deserialize the field.
543fn generate_from_json(field_ident: &Ident, column_name: &str, _field_type: &Type, is_optional: bool) -> proc_macro2::TokenStream {
544 // Use the column name as the key to look up the value in the JSON object
545 let column_literal = column_name;
546
547 if is_optional {
548 // Code for optional fields (Option<T> or #[model(nullable)])
549 quote! {
550 #field_ident: if let Some(val) = obj.get(#column_literal) {
551 // If the key exists, check if the value is null
552 if val.is_null() {
553 None // If null, set field to None
554 } else {
555 // If not null, attempt to deserialize the value
556 match serde_json::from_value(val.clone()) {
557 Ok(v) => Some(v), // If successful, wrap in Some
558 Err(e) => return Err(rusticx::RusticxError::DeserializationError(
559 format!("Failed to deserialize field `{}`: {}", #column_literal, e)
560 )), // If deserialization fails, return an error
561 }
562 }
563 } else {
564 // If the key does not exist in the JSON object, treat as None
565 // This handles cases where a nullable column is not included in the query result
566 None
567 }
568 }
569 } else {
570 // Code for required fields (non-Option and not #[model(nullable)])
571 quote! {
572 #field_ident: if let Some(val) = obj.get(#column_literal) {
573 // If the key exists, attempt to deserialize the value
574 match serde_json::from_value(val.clone()) {
575 Ok(v) => v, // If successful, use the value
576 Err(e) => return Err(rusticx::RusticxError::DeserializationError(
577 format!("Failed to deserialize field `{}`: {}", #column_literal, e)
578 )), // If deserialization fails, return an error
579 }
580 } else {
581 // If the key does not exist for a required field, return an error
582 return Err(rusticx::RusticxError::DeserializationError(
583 format!("Missing required field: `{}`", #column_literal)
584 ));
585 }
586 }
587 }
588}
589
590/// Helper function to map a Rust type to a generic `SqlType` enum variant.
591///
592/// This mapping is used to determine the database column type in the `CREATE TABLE` statement,
593/// which is then translated to the database-specific syntax by the `SqlType` methods.
594///
595/// # Arguments
596///
597/// * `rust_type`: The `syn::Type` of the Rust field.
598///
599/// # Returns
600///
601/// A `proc_macro2::TokenStream` representing the corresponding `rusticx::SqlType` variant.
602///
603/// # Panics
604///
605/// Panics if the Rust type is not recognized or supported by the mapping.
606fn generate_sql_type(rust_type: &Type) -> proc_macro2::TokenStream {
607 // Only support path types (like `i32`, `String`, `Option<T>`, etc.)
608 match rust_type {
609 Type::Path(TypePath { path, .. }) => {
610 // Get the last segment of the path
611 let segment = path.segments.last().unwrap();
612 let ident = &segment.ident;
613 let type_name = ident.to_string();
614
615 // Handle Option<T> recursively: get the inner type's mapping
616 if type_name == "Option" {
617 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
618 if let Some(arg) = args.args.first() {
619 if let syn::GenericArgument::Type(inner_type) = arg {
620 // Recursively call for the inner type
621 return generate_sql_type(inner_type);
622 }
623 }
624 }
625 // Panic if Option type has invalid arguments
626 panic!("Invalid Option<T> type specification for field: {}", quote!{#rust_type});
627 }
628
629 // Map common Rust types to SqlType variants
630 match type_name.as_str() {
631 "i8" | "i16" | "i32" | "u8" | "u16" | "u32" => quote! { rusticx::SqlType::Integer },
632 "i64" | "u64" => quote! { rusticx::SqlType::BigInt },
633 "f32" | "f64" => quote! { rusticx::SqlType::Float },
634 "bool" => quote! { rusticx::SqlType::Boolean },
635 // Map String/str to Text
636 "String" | "str" => quote! { rusticx::SqlType::Text },
637 // Map Uuid (from `uuid` crate) to Text (common storage, can be overridden)
638 "Uuid" => quote! { rusticx::SqlType::Text },
639 // Map chrono date/time types
640 "NaiveDate" => quote! { rusticx::SqlType::Date },
641 "NaiveTime" => quote! { rusticx::SqlType::Time },
642 "NaiveDateTime" | "DateTime" => quote! { rusticx::SqlType::DateTime },
643 // Map Vec<u8> to Blob
644 "Vec" => {
645 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
646 if let Some(arg) = args.args.first() {
647 if let syn::GenericArgument::Type(Type::Path(TypePath { path, .. })) = arg {
648 if let Some(seg) = path.segments.last() {
649 if seg.ident == "u8" {
650 return quote! { rusticx::SqlType::Blob };
651 }
652 }
653 }
654 }
655 }
656 // Fallback for other Vec types, treat as Blob (might need refinement)
657 quote! { rusticx::SqlType::Blob }
658 }
659 // Panic for unknown types
660 _ => panic!("Unknown or unsupported Rust type for SQL mapping: `{}`. Consider using #[model(sql_type = \"...\")]", quote!{#rust_type}),
661 }
662 }
663 // Panic for other complex types (arrays, tuples, pointers, etc.)
664 _ => panic!("Unsupported complex type for SQL mapping: `{}`. Only simple path types and Option<T> are automatically mapped. Consider using #[model(sql_type = \"...\")]", quote!{#rust_type}),
665 }
666}
667
668/// Helper function to extract the custom table name from the struct-level `#[model(table = "...")]` attribute.
669///
670/// # Arguments
671///
672/// * `attrs`: A slice of `syn::Attribute` applied to the struct.
673///
674/// # Returns
675///
676/// An `Option<String>` containing the custom table name if found, otherwise `None`.
677fn extract_table_name(attrs: &[Attribute]) -> Option<String> {
678 // Iterate through all attributes on the struct
679 for attr in attrs {
680 // Check if the attribute is our custom #[model(...)] attribute
681 if !attr.path().is_ident("model") {
682 continue;
683 }
684
685 // Parse the attribute's arguments
686 let parsed = attr.parse_args_with(
687 syn::punctuated::Punctuated::<Meta, syn::token::Comma>::parse_terminated,
688 );
689
690 // Process the parsed meta items
691 if let Ok(items) = parsed {
692 for meta in items {
693 // Check for the `table = "..."` name-value pair
694 if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
695 if path.is_ident("table") {
696 // If found, extract the string literal value
697 if let Expr::Lit(expr_lit) = value {
698 if let syn::Lit::Str(lit_str) = expr_lit.lit {
699 return Some(lit_str.value()); // Return the extracted table name
700 }
701 }
702 }
703 }
704 }
705 } else {
706 // Log or handle parsing errors for struct attributes if necessary
707 // For simplicity, we ignore errors here and let subsequent logic handle missing name
708 let _ = parsed.unwrap_err(); // Consume the error
709 }
710 }
711 None // No custom table name found
712}