feat: entities with defaultable fields
This commit is contained in:
parent
91d7651eca
commit
cf7660c505
108
georm-macros/src/georm/defaultable_struct.rs
Normal file
108
georm-macros/src/georm/defaultable_struct.rs
Normal file
@ -0,0 +1,108 @@
|
||||
//! This module creates the defaultable version of a structured derived with
|
||||
//! Georm. It creates a new struct named `<StructName>Default` where the fields
|
||||
//! marked as defaultable become an `Option<T>`, where `T` is the initial type
|
||||
//! of the field.
|
||||
//!
|
||||
//! The user does not have to mark a field defaultable if the field already has
|
||||
//! a type `Option<T>`. It is intended only for fields marked as `NOT NULL` in
|
||||
//! the database, but not required when creating the entity due to a `DEFAULT`
|
||||
//! or something similar. The type `<StructName>Default` implements the
|
||||
//! `Defaultable` trait.
|
||||
|
||||
use quote::quote;
|
||||
use super::ir::{GeormField, GeormStructAttributes};
|
||||
|
||||
fn create_defaultable_field(field: &GeormField) -> proc_macro2::TokenStream {
|
||||
let ident = &field.ident;
|
||||
let ty = &field.ty;
|
||||
let vis = &field.field.vis;
|
||||
|
||||
// If the field is marked as defaultable, wrap it in Option<T>
|
||||
// Otherwise, keep the original type
|
||||
let field_type = if field.defaultable {
|
||||
quote! { Option<#ty> }
|
||||
} else {
|
||||
quote! { #ty }
|
||||
};
|
||||
|
||||
quote! {
|
||||
#vis #ident: #field_type
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_defaultable_trait_impl(
|
||||
struct_name: &syn::Ident,
|
||||
defaultable_struct_name: &syn::Ident,
|
||||
struct_attrs: &GeormStructAttributes,
|
||||
fields: &[GeormField],
|
||||
) -> proc_macro2::TokenStream {
|
||||
let table = &struct_attrs.table;
|
||||
|
||||
// Find the ID field
|
||||
let id_field = fields.iter().find(|field| field.id).expect("Must have an ID field");
|
||||
let id_type = &id_field.ty;
|
||||
|
||||
// Generate the INSERT query for defaultable struct
|
||||
let field_names: Vec<String> = fields.iter().map(|f| f.ident.to_string()).collect();
|
||||
let field_placeholders: Vec<String> = (1..=field_names.len()).map(|i| format!("${}", i)).collect();
|
||||
|
||||
let insert_query = format!(
|
||||
"INSERT INTO {} ({}) VALUES ({}) RETURNING *",
|
||||
table,
|
||||
field_names.join(", "),
|
||||
field_placeholders.join(", ")
|
||||
);
|
||||
|
||||
// Generate field identifiers for binding
|
||||
let field_idents: Vec<&syn::Ident> = fields.iter().map(|field| &field.ident).collect();
|
||||
|
||||
quote! {
|
||||
impl ::georm::Defaultable<#id_type, #struct_name> for #defaultable_struct_name {
|
||||
async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#struct_name> {
|
||||
::sqlx::query_as!(
|
||||
#struct_name,
|
||||
#insert_query,
|
||||
#(self.#field_idents),*
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_defaultable_struct(
|
||||
ast: &syn::DeriveInput,
|
||||
struct_attrs: &GeormStructAttributes,
|
||||
fields: &[GeormField],
|
||||
) -> proc_macro2::TokenStream {
|
||||
// Only generate if there are defaultable fields
|
||||
if fields.iter().all(|field| !field.defaultable) {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
let struct_name = &ast.ident;
|
||||
let vis = &ast.vis;
|
||||
let defaultable_struct_name = quote::format_ident!("{}Default", struct_name);
|
||||
|
||||
let defaultable_fields: Vec<proc_macro2::TokenStream> = fields
|
||||
.iter()
|
||||
.map(create_defaultable_field)
|
||||
.collect();
|
||||
|
||||
let trait_impl = generate_defaultable_trait_impl(
|
||||
struct_name,
|
||||
&defaultable_struct_name,
|
||||
struct_attrs,
|
||||
fields,
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone)]
|
||||
#vis struct #defaultable_struct_name {
|
||||
#(#defaultable_fields),*
|
||||
}
|
||||
|
||||
#trait_impl
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ struct GeormFieldAttributes {
|
||||
pub id: bool,
|
||||
#[deluxe(default = None)]
|
||||
pub relation: Option<O2ORelationship>,
|
||||
#[deluxe(default = false)]
|
||||
pub defaultable: bool,
|
||||
}
|
||||
|
||||
#[derive(deluxe::ParseMetaItem, Clone, Debug)]
|
||||
@ -45,6 +47,7 @@ pub struct GeormField {
|
||||
pub ty: syn::Type,
|
||||
pub id: bool,
|
||||
pub relation: Option<O2ORelationship>,
|
||||
pub defaultable: bool,
|
||||
}
|
||||
|
||||
impl GeormField {
|
||||
@ -53,13 +56,42 @@ impl GeormField {
|
||||
let ty = field.clone().ty;
|
||||
let attrs: GeormFieldAttributes =
|
||||
deluxe::extract_attributes(field).expect("Could not extract attributes from field");
|
||||
let GeormFieldAttributes { id, relation } = attrs;
|
||||
let GeormFieldAttributes {
|
||||
id,
|
||||
relation,
|
||||
defaultable,
|
||||
} = attrs;
|
||||
|
||||
// Validate that defaultable is not used on Option<T> fields
|
||||
if defaultable && Self::is_option_type(&ty) {
|
||||
panic!(
|
||||
"Field '{}' is already an Option<T> and cannot be marked as defaultable. \
|
||||
Remove the #[georm(defaultable)] attribute.",
|
||||
ident
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
ident,
|
||||
field: field.to_owned(),
|
||||
id,
|
||||
ty,
|
||||
relation,
|
||||
defaultable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a type is Option<T>
|
||||
fn is_option_type(ty: &syn::Type) -> bool {
|
||||
match ty {
|
||||
syn::Type::Path(type_path) => {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
segment.ident == "Option"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use ir::GeormField;
|
||||
use quote::quote;
|
||||
|
||||
mod defaultable_struct;
|
||||
mod ir;
|
||||
mod relationships;
|
||||
mod trait_implementation;
|
||||
@ -51,9 +52,12 @@ pub fn georm_derive_macro2(
|
||||
let (fields, id) = extract_georm_field_attrs(&mut ast)?;
|
||||
let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &id);
|
||||
let trait_impl = trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &id);
|
||||
let defaultable_struct =
|
||||
defaultable_struct::derive_defaultable_struct(&ast, &struct_attrs, &fields);
|
||||
let code = quote! {
|
||||
#relationships
|
||||
#trait_impl
|
||||
#defaultable_struct
|
||||
};
|
||||
Ok(code)
|
||||
}
|
||||
|
10
src/defaultable.rs
Normal file
10
src/defaultable.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub trait Defaultable<Id, Entity> {
|
||||
/// Creates an entity in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error the database may have encountered
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Entity>> + Send;
|
||||
}
|
92
src/entity.rs
Normal file
92
src/entity.rs
Normal file
@ -0,0 +1,92 @@
|
||||
pub trait Georm<Id> {
|
||||
/// Find all the entities in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find_all(
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Find the entiy in the database based on its identifier.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Create the entity in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database if
|
||||
/// it exists, create it otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create_or_update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = sqlx::Result<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
async {
|
||||
if Self::find(pool, self.get_id()).await?.is_some() {
|
||||
self.update(pool).await
|
||||
} else {
|
||||
self.create(pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the entity from the database if it exists.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Delete any entity with the identifier `id`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete_by_id(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Returns the identifier of the entity.
|
||||
fn get_id(&self) -> &Id;
|
||||
}
|
92
src/georm.rs
Normal file
92
src/georm.rs
Normal file
@ -0,0 +1,92 @@
|
||||
pub trait Georm<Id> {
|
||||
/// Find all the entities in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find_all(
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Find the entiy in the database based on its identifier.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Create the entity in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database if
|
||||
/// it exists, create it otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create_or_update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = sqlx::Result<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
async {
|
||||
if Self::find(pool, self.get_id()).await?.is_some() {
|
||||
self.update(pool).await
|
||||
} else {
|
||||
self.create(pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the entity from the database if it exists.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Delete any entity with the identifier `id`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete_by_id(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Returns the identifier of the entity.
|
||||
fn get_id(&self) -> &Id;
|
||||
}
|
166
src/lib.rs
166
src/lib.rs
@ -267,6 +267,76 @@
|
||||
//! | link.from | Column of the linking table referring to this entity | N/A |
|
||||
//! | link.to | Column of the linking table referring to the remote entity | N/A |
|
||||
//!
|
||||
//! ## Defaultable Fields
|
||||
//!
|
||||
//! Georm supports defaultable fields for entities where some fields have database
|
||||
//! defaults or are auto-generated (like serial IDs). When you mark fields as
|
||||
//! `defaultable`, Georm generates a companion struct that makes these fields
|
||||
//! optional during entity creation.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! pub struct Post {
|
||||
//! #[georm(id, defaultable)]
|
||||
//! id: i32, // Auto-generated serial
|
||||
//! title: String, // Required field
|
||||
//! #[georm(defaultable)]
|
||||
//! published: bool, // Has database default
|
||||
//! #[georm(defaultable)]
|
||||
//! created_at: chrono::DateTime<chrono::Utc>, // Has database default
|
||||
//! author_id: i32, // Required field
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This generates a `PostDefault` struct where defaultable fields become `Option<T>`:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Generated automatically by the macro
|
||||
//! pub struct PostDefault {
|
||||
//! pub id: Option<i32>, // Can be None for auto-generation
|
||||
//! pub title: String, // Required field stays the same
|
||||
//! pub published: Option<bool>, // Can be None to use database default
|
||||
//! pub created_at: Option<chrono::DateTime<chrono::Utc>>, // Can be None
|
||||
//! pub author_id: i32, // Required field stays the same
|
||||
//! }
|
||||
//!
|
||||
//! impl Defaultable<i32, Post> for PostDefault {
|
||||
//! async fn create(&self, pool: &sqlx::PgPool) -> sqlx::Result<Post>;
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Usage Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use georm::{Georm, Defaultable};
|
||||
//!
|
||||
//! // Create a post with some fields using database defaults
|
||||
//! let post_default = PostDefault {
|
||||
//! id: None, // Let database auto-generate
|
||||
//! title: "My Blog Post".to_string(),
|
||||
//! published: None, // Use database default (e.g., false)
|
||||
//! created_at: None, // Use database default (e.g., NOW())
|
||||
//! author_id: 42,
|
||||
//! };
|
||||
//!
|
||||
//! // Create the entity in the database
|
||||
//! let created_post = post_default.create(&pool).await?;
|
||||
//! println!("Created post with ID: {}", created_post.id);
|
||||
//! ```
|
||||
//!
|
||||
//! ### Rules and Limitations
|
||||
//!
|
||||
//! - **Option fields cannot be marked as defaultable**: If a field is already
|
||||
//! `Option<T>`, you cannot mark it with `#[georm(defaultable)]`. This prevents
|
||||
//! `Option<Option<T>>` types.
|
||||
//! - **Field visibility is preserved**: The generated defaultable struct maintains
|
||||
//! the same field visibility (`pub`, `pub(crate)`, private) as the original struct.
|
||||
//! - **ID fields can be defaultable**: It's common to mark ID fields as defaultable
|
||||
//! when they are auto-generated serials in PostgreSQL.
|
||||
//! - **Only generates when needed**: The defaultable struct is only generated if
|
||||
//! at least one field is marked as defaultable.
|
||||
//!
|
||||
//! ## Limitations
|
||||
//! ### Database
|
||||
//!
|
||||
@ -282,95 +352,7 @@
|
||||
|
||||
pub use georm_macros::Georm;
|
||||
|
||||
pub trait Georm<Id> {
|
||||
/// Find all the entities in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find_all(
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Find the entiy in the database based on its identifier.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn find(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Create the entity in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database if
|
||||
/// it exists, create it otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn create_or_update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = sqlx::Result<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
async {
|
||||
if Self::find(pool, self.get_id()).await?.is_some() {
|
||||
self.update(pool).await
|
||||
} else {
|
||||
self.create(pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the entity from the database if it exists.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Delete any entity with the identifier `id`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
fn delete_by_id(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Returns the identifier of the entity.
|
||||
fn get_id(&self) -> &Id;
|
||||
}
|
||||
mod georm;
|
||||
pub use georm::Georm;
|
||||
mod defaultable;
|
||||
pub use defaultable::Defaultable;
|
||||
|
61
tests/defaultable_struct.rs
Normal file
61
tests/defaultable_struct.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use georm::Georm;
|
||||
|
||||
// Test struct with defaultable fields using existing table structure
|
||||
#[derive(Georm)]
|
||||
#[georm(table = "authors")]
|
||||
struct TestAuthor {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub biography_id: Option<i32>, // Don't mark Option fields as defaultable
|
||||
}
|
||||
|
||||
// Test struct with only ID defaultable
|
||||
#[derive(Georm)]
|
||||
#[georm(table = "authors")]
|
||||
struct MinimalDefaultable {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub biography_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaultable_struct_should_exist() {
|
||||
// This test will compile only if TestAuthorDefault struct exists
|
||||
let _author_default = TestAuthorDefault {
|
||||
id: Some(1), // Should be Option<i32> since ID is defaultable
|
||||
name: "Test Author".to_string(), // Should remain String
|
||||
biography_id: None, // Should remain Option<i32>
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_defaultable_struct_should_exist() {
|
||||
// MinimalDefaultableDefault should exist because ID is marked as defaultable
|
||||
let _minimal_default = MinimalDefaultableDefault {
|
||||
id: None, // Should be Option<i32>
|
||||
name: "testuser".to_string(), // Should remain String
|
||||
biography_id: None, // Should remain Option<i32>
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaultable_fields_can_be_none() {
|
||||
let _author_default = TestAuthorDefault {
|
||||
id: None, // Can be None since it's defaultable (auto-generated)
|
||||
name: "Test Author".to_string(),
|
||||
biography_id: None, // Can remain None
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_visibility_is_preserved() {
|
||||
let _author_default = TestAuthorDefault {
|
||||
id: Some(1), // pub
|
||||
name: "Test".to_string(), // pub
|
||||
biography_id: Some(1), // pub, Option<i32>
|
||||
};
|
||||
|
||||
// This test ensures field visibility is preserved in generated struct
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user