Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/src/concepts/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,36 @@ assert_eq!(product.name, "Anvil 3000");

Fields you don't set are filled with generated values automatically.

## Building Without Persisting

Use `make()` instead of `create()` to build a model instance
in memory without hitting the database:

```rust
# extern crate fabrique;
# extern crate sqlx;
# extern crate uuid;
# use fabrique::prelude::*;
# use uuid::Uuid;
#
# #[derive(Model, Factory)]
# pub struct Product {
# id: Uuid,
# name: String,
# price_cents: i32,
# }
# fn main() {
let product: Product = Product::factory::<sqlx::Sqlite>()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for other factory() calls without turbofish to see the pattern
rg -n 'factory\(\)' --type=md docs/ -A1 -B1

Repository: robinstraub/fabrique

Length of output: 19004


🏁 Script executed:

sed -n '60,75p' docs/src/concepts/factories.md

Repository: robinstraub/fabrique

Length of output: 439


🏁 Script executed:

rg -n 'factory.*\).*\.make\(\)' --type=md docs/ -B2 -A2

Repository: robinstraub/fabrique

Length of output: 629


🏁 Script executed:

fd -e rs 'factory' src/ | head -20

Repository: robinstraub/fabrique

Length of output: 156


🏁 Script executed:

ls -la && find . -name "*.rs" -path "*/factory*" | head -10

Repository: robinstraub/fabrique

Length of output: 1842


🏁 Script executed:

cat -n fabrique/src/factory.rs | head -100

Repository: robinstraub/fabrique

Length of output: 3543


🏁 Script executed:

rg -n 'fn make\(' fabrique/src/factory.rs -A 5

Repository: robinstraub/fabrique

Length of output: 46


🏁 Script executed:

rg -n 'fn make\(' fabrique-core/src/factory.rs -A 5

Repository: robinstraub/fabrique

Length of output: 46


🏁 Script executed:

rg -n 'make' fabrique/src/factory.rs | head -20

Repository: robinstraub/fabrique

Length of output: 46


🏁 Script executed:

wc -l fabrique/src/factory.rs && tail -100 fabrique/src/factory.rs

Repository: robinstraub/fabrique

Length of output: 2680


🏁 Script executed:

cat -n fabrique-core/src/factory.rs | head -150

Repository: robinstraub/fabrique

Length of output: 3086


🏁 Script executed:

cat -n fabrique-derive/src/codegen/factory.rs | head -200

Repository: robinstraub/fabrique

Length of output: 8993


🏁 Script executed:

rg -n 'fn generate_factory_method_make' fabrique-derive/src/codegen/factory.rs -A 30

Repository: robinstraub/fabrique

Length of output: 1146


🏁 Script executed:

rg -n 'fn generate_factory_method_make' fabrique-derive/src/codegen/factory.rs -A 50 | head -70

Repository: robinstraub/fabrique

Length of output: 1955


Remove the unnecessary turbofish type parameter.

The example uses Product::factory::<sqlx::Sqlite>() but this explicit type specification is redundant. Rust can infer the database type from context. Other .make() examples in the docs (e.g., docs/src/cookbook/bulk-update-and-upsert.md lines 217–218) omit the turbofish and compile successfully. Update to match the consistent pattern elsewhere:

let product: Product = Product::factory()
    .name("Anvil 3000".to_string())
    .make();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/concepts/factories.md` at line 65, Update the example to remove the
unnecessary turbofish type parameter on Product::factory — Rust will infer the
DB type from context, so replace the explicit Product::factory::<sqlx::Sqlite>()
call with a plain Product::factory() and then chain the same builder methods
(e.g., .name(...) and .make()) to match other examples like the .make() usages
in the cookbook.

.name("Anvil 3000".to_string())
.make();

assert_eq!(product.name, "Anvil 3000");
# }
```

This is useful for preparing data for bulk operations like
[`upsert()`](../cookbook/bulk-update-and-upsert.md#bulk-upsert-a-collection).

## Random Value Generation

By default, factories generate random values for all fields using
Expand Down
6 changes: 5 additions & 1 deletion docs/src/concepts/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,12 @@ let anvil: Product = Product::find(&pool, anvil.id).await?;
// Upsert (insert or update on PK conflict)
let anvil: Product = anvil.save(&pool).await?;

// Bulk upsert a collection
let products = vec![anvil];
products.upsert(&pool, Product::ID).await?;

// Delete by primary key, no instance needed
Product::destroy(&pool, anvil.id).await?;
Product::destroy(&pool, Uuid::new_v4()).await?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are similar destroy patterns in other docs
rg -n -C3 '::destroy\(' --type=md docs/

Repository: robinstraub/fabrique

Length of output: 2577


🏁 Script executed:

# Get the full context around line 263 in models.md
sed -n '240,280p' docs/src/concepts/models.md

Repository: robinstraub/fabrique

Length of output: 920


Update destroy example to delete the created record for proper lifecycle demonstration.

The example creates an anvil record and demonstrates multiple operations on it, but then deletes a random Uuid::new_v4() instead of the anvil.id. This breaks the expected pattern of creating, using, and cleaning up the same resource. While the comment "no instance needed" suggests this was intentional to show the API capability, the same point can be demonstrated by using anvil.id—which actually destroys the record that was created—rather than a non-existent UUID. This provides a complete and realistic lifecycle example.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/concepts/models.md` at line 263, Replace the call that destroys a
random UUID with one that destroys the actual record created earlier: use the
created variable's id (anvil.id) when calling Product::destroy instead of
Uuid::new_v4(), so the example demonstrates creating, operating on, and then
cleaning up the same resource (reference symbols: Product::destroy, anvil,
anvil.id, Uuid::new_v4()).

# Ok(())
# }
```
Expand Down
67 changes: 67 additions & 0 deletions docs/src/cookbook/bulk-update-and-upsert.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,73 @@ let saved: Product = Product::insert()
values from the INSERT — the SQL equivalent of
`SET col = EXCLUDED.col` for each column.

## Bulk Upsert a Collection

When you have a `Vec` of models to upsert — e.g. syncing an
external data source — use `.upsert()` to insert or update
them all in a single statement:

```rust
# extern crate fabrique;
# extern crate sqlx;
# extern crate tokio;
# extern crate uuid;
# use fabrique::prelude::*;
# use uuid::Uuid;
#
# #[derive(Clone, Debug, Factory, Model)]
# pub struct Product {
# pub id: Uuid,
# pub name: String,
# pub price_cents: i32,
# pub in_stock: bool,
# }
#
# #[fabrique::doctest]
# async fn main(pool: Pool<Sqlite>) -> Result<(), fabrique::Error> {
# let products = vec![
# Product::factory().name("Anvil 3000".to_string()).make(),
# Product::factory().name("Rocket Skates".to_string()).make(),
# ];
// Insert all products, or update on ID conflict
products.upsert(&pool, Product::ID).await?;
# Ok(())
# }
```

The second argument specifies the conflict target — the column
(or columns) that identify a unique row. All other columns are
updated when a conflict is detected.

For a composite unique key, pass a tuple:

```rust,no_run
# extern crate fabrique;
# extern crate sqlx;
# extern crate uuid;
# use fabrique::prelude::*;
# use uuid::Uuid;
# #[derive(Clone, Debug, Factory, Model)]
# pub struct Product {
# pub id: Uuid,
# pub name: String,
# pub price_cents: i32,
# pub in_stock: bool,
# }
# async fn example(
# products: Vec<Product>,
# pool: Pool<Sqlite>,
# ) -> Result<(), fabrique::Error> {
products
.upsert(&pool, (Product::NAME, Product::PRICE_CENTS))
.await?;
# Ok(())
# }
```

> **Note:** The conflict target columns must have a UNIQUE
> constraint or be the primary key in your database schema.

If you only want to skip duplicates without updating, use
`.do_nothing()`:

Expand Down
3 changes: 3 additions & 0 deletions fabrique-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod factory;
pub mod model;
pub mod relation;
pub mod sql;
pub mod upsert;

// Re-export for use in generated code
pub use database::Nil;
Expand All @@ -24,3 +25,5 @@ pub use factory::SetForeignKey;
pub use relation::Alias;
pub use relation::BelongsTo;
pub use relation::Joinable;
pub use upsert::UniqueBy;
pub use upsert::Upsert;
4 changes: 4 additions & 0 deletions fabrique-core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ pub trait Persist<DB: Dialect>: Model {
) -> impl Future<Output = Result<Self, crate::Error>> + Send + 'e
where
A: sqlx::Acquire<'e, Database = DB> + Send + 'e;

/// Pushes all field values as bind parameters into a separated
/// query builder row.
fn push_bind_values(self, separated: sqlx::query_builder::Separated<'_, DB, &'static str>);
}

/// Delete operations
Expand Down
83 changes: 83 additions & 0 deletions fabrique-core/src/upsert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::{
database::Column,
dialect::Dialect,
model::{Model, Persist},
};

pub trait UniqueBy<M> {
fn column_names() -> &'static [&'static str];
}

macro_rules! impl_unique_by {
($($C:ident),+) => {
impl<M, $($C),+> UniqueBy<M> for ($($C,)+)
where
$($C: Column<M>,)+
{
fn column_names() -> &'static [&'static str] {
&[$($C::NAME),+]
}
}
};
}
impl_unique_by!(C0, C1);
impl_unique_by!(C0, C1, C2);
impl_unique_by!(C0, C1, C2, C3);
Comment on lines +11 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Tuple arity capped at 4 — document or extend.

impl_unique_by! only covers 2-, 3-, and 4-tuples. A user attempting a 5+-column composite conflict target (rare but valid for natural keys) will hit a confusing trait-bound error rather than a clear message. Single-column targets are covered by the per-column codegen, so that gap is intentional.

Either extend the macro to a higher arity (e.g., up to 8 or 12, mirroring std's tuple-trait conventions) or note the limit in the trait's rustdoc and in the cookbook section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fabrique-core/src/upsert.rs` around lines 11 - 25, The impl_unique_by! macro
currently implements UniqueBy for 2–4 tuples (C0..C3) causing confusing errors
for 5+ column composite keys; extend support by adding additional macro
invocations for higher arities (e.g., impl_unique_by!(C0, C1, C2, C3, C4); ...
up to C7 for 8-tuples or further as desired) so UniqueBy and its column_names()
cover 5–8 element tuples, and also add/update rustdoc on the UniqueBy trait to
state the maximum supported tuple arity (mention Column, UniqueBy,
impl_unique_by!, and column_names for clarity).


pub trait Upsert<DB: Dialect> {
type Model: Model;

fn upsert<'e, A, U>(
self,
executor: A,
unique_by: U,
) -> impl Future<Output = Result<(), crate::Error>>
where
A: sqlx::Acquire<'e, Database = DB> + Send + 'e,
U: UniqueBy<Self::Model>;
}

impl<M, DB> Upsert<DB> for Vec<M>
where
M: Persist<DB> + Send,
DB: Dialect,
<DB as sqlx::Database>::Arguments: sqlx::IntoArguments<DB>,
for<'c> &'c mut <DB as sqlx::Database>::Connection: sqlx::Executor<'c, Database = DB>,
{
type Model = M;

async fn upsert<'e, A, U>(self, executor: A, _unique_by: U) -> Result<(), crate::Error>
where
A: sqlx::Acquire<'e, Database = DB> + Send + 'e,
U: UniqueBy<M>,
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if self.is_empty() {
return Ok(());
}

let mut conn = executor.acquire().await.map_err(crate::Error::from)?;

let mut qb = sqlx::QueryBuilder::new("INSERT INTO ");
qb.push(M::table_name());
qb.push(" (");
qb.push(M::columns().join(", "));
qb.push(") ");

qb.push_values(self, |separated, model| {
model.push_bind_values(separated);
});

let unique_cols = U::column_names();
qb.push(DB::on_conflict_sql(unique_cols));

let update_cols: Vec<&str> = M::columns()
.iter()
.copied()
.filter(|c| !unique_cols.contains(c))
.collect();
qb.push(DB::do_update_sql(&update_cols));

qb.build().execute(&mut *conn).await?;
Comment on lines +71 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Dialect trait definition and impls for on_conflict_sql / do_update_sql
# to verify behavior when an empty slice is passed.
fd -e rs | xargs rg -nP -C5 '\bdo_update_sql\s*\(' 
echo "---"
fd -e rs | xargs rg -nP -C5 '\bon_conflict_sql\s*\('
echo "---"
# Look for any explicit empty-slice guard
fd -e rs | xargs rg -nP -C2 'is_empty\(\)|len\(\)\s*==\s*0' | rg -i 'do_update|conflict'

Repository: robinstraub/fabrique

Length of output: 13107


🏁 Script executed:

sed -n '40,50p' fabrique-core/src/dialect.rs

Repository: robinstraub/fabrique

Length of output: 438


🏁 Script executed:

sed -n '68,78p' fabrique-core/src/dialect.rs

Repository: robinstraub/fabrique

Length of output: 408


🏁 Script executed:

sed -n '96,106p' fabrique-core/src/dialect.rs

Repository: robinstraub/fabrique

Length of output: 408


🏁 Script executed:

# Also check the complete upsert function to see context
sed -n '60,85p' fabrique-core/src/upsert.rs

Repository: robinstraub/fabrique

Length of output: 743


Guard against empty update_cols slice.

When all columns are part of the conflict target, update_cols becomes empty. The current dialect implementations generate incomplete SQL for this case:

  • Postgres/Sqlite: " DO UPDATE SET " (incomplete SET clause)
  • MySql: " ON DUPLICATE KEY UPDATE " (incomplete UPDATE clause)

All three produce syntax errors. The semantically correct behavior is DO NOTHING.

Add a guard at line 78 to call DB::do_nothing_sql() when update_cols.is_empty():

if update_cols.is_empty() {
    qb.push(DB::do_nothing_sql());
} else {
    qb.push(DB::do_update_sql(&update_cols));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fabrique-core/src/upsert.rs` around lines 70 - 80, The code builds an empty
update clause when all columns are unique; guard against this by checking the
computed update_cols (from M::columns() filtered against U::column_names()) and,
if it is empty, push DB::do_nothing_sql() to qb instead of
DB::do_update_sql(&update_cols); otherwise keep the existing
qb.push(DB::do_update_sql(&update_cols)). Ensure this change is made where qb is
assembled before qb.build().execute(&mut *conn).await so the query uses DO
NOTHING for empty update sets.

Ok(())
}
}
32 changes: 32 additions & 0 deletions fabrique-derive/src/codegen/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ impl<'a> ColumnsCodegen<'a> {
#into_db_body
}
}

impl ::fabrique::UniqueBy<#base_struct_ident> for #type_name {
fn column_names() -> &'static [&'static str] {
&[#column_name]
}
}
}
});

Expand Down Expand Up @@ -127,6 +133,13 @@ mod tests {
value
}
}

impl ::fabrique::UniqueBy<Anvil> for AnvilIdColumn {
fn column_names() -> &'static [&'static str] {
&["id"]
}
}

impl ::fabrique::Column<Anvil> for AnvilNameColumn {
type Type = String;
type DbType = String;
Expand All @@ -139,6 +152,12 @@ mod tests {
}
}

impl ::fabrique::UniqueBy<Anvil> for AnvilNameColumn {
fn column_names() -> &'static [&'static str] {
&["name"]
}
}

impl Anvil {
pub const ID: AnvilIdColumn = AnvilIdColumn;
pub const NAME: AnvilNameColumn = AnvilNameColumn;
Expand Down Expand Up @@ -182,6 +201,13 @@ mod tests {
value
}
}

impl ::fabrique::UniqueBy<Account> for AccountIdColumn {
fn column_names() -> &'static [&'static str] {
&["id"]
}
}

impl ::fabrique::Column<Account> for AccountStatusColumn {
type Type = Status;
type DbType = String;
Expand All @@ -194,6 +220,12 @@ mod tests {
}
}

impl ::fabrique::UniqueBy<Account> for AccountStatusColumn {
fn column_names() -> &'static [&'static str] {
&["status"]
}
}

impl Account {
pub const ID: AccountIdColumn = AccountIdColumn;
pub const STATUS: AccountStatusColumn = AccountStatusColumn;
Expand Down
53 changes: 53 additions & 0 deletions fabrique-derive/src/codegen/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl<'a> FactoryCodegen<'a> {
let factory_ident = &self.ident;
let factory_fields = self.generate_factory_fields();
let factory_method_new = self.generate_factory_method_new();
let factory_method_make = self.generate_factory_method_make();
let factory_method_fields = self.generate_factory_method_fields();
let factory_methods_for_relation = self.generate_factory_methods_for_relation();
let factory_relation_fields = self.generate_factory_relation_fields();
Expand Down Expand Up @@ -87,6 +88,8 @@ impl<'a> FactoryCodegen<'a> {
impl<DB: ::fabrique::Dialect> #factory_ident<DB> {
#factory_method_new

#factory_method_make

#(#factory_method_fields)*

#(#factory_methods_for_relation)*
Expand Down Expand Up @@ -369,6 +372,47 @@ impl<'a> FactoryCodegen<'a> {
}
}

/// Generates the `make()` method that builds a model instance
/// without persisting it.
fn generate_factory_method_make(&self) -> TokenStream {
let struct_ident = &self.analysis.ident;

let has_custom_faker = self
.analysis
.column_fields
.iter()
.any(|f| f.faker.is_some());

let fake_import = if has_custom_faker {
quote! { use ::fabrique::fake::Fake; }
} else {
quote! {}
};

let column_fields = self.analysis.column_fields.iter().map(|field| {
let name = &field.ident;
let ty = &field.ty;

match &field.faker {
Some(faker_expr) => quote! {
#name: self.#name.unwrap_or_else(|| #faker_expr.fake())
},
None => quote! {
#name: self.#name.unwrap_or_else(::fabrique::seeded_value::<#ty>)
},
}
});

quote! {
pub fn make(self) -> #struct_ident {
#fake_import
#struct_ident {
#(#column_fields,)*
}
Comment on lines +407 to +411
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

make() ignores for_<relation>() inputs, causing inconsistent factory behavior.

make() only materializes column_fields and never consumes *_relation; so relation setters become no-ops on this path, and FK fields may be seeded instead of using caller-provided relations. This is a functional gap versus the fluent API surface.

Suggested direction
 pub fn make(mut self) -> `#struct_ident` {
+    // Resolve belongs_to relation fields into FK columns for in-memory build.
+    // - PrimaryKey variant: assign FK directly.
+    // - Factory variant: return an error or panic with a clear message since no DB executor exists.
+    //   (Preferably adjust make() signature to Result<..., fabrique::Error> if you want non-panicking behavior.)
+
     `#fake_import`
     `#struct_ident` {
         #(`#column_fields`,)*
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fabrique-derive/src/codegen/factory.rs` around lines 407 - 411, The make()
implementation currently only constructs `#struct_ident` from `#column_fields` and
ignores any stored relation values (fields named like *_relation), causing
for_<relation>() setters to be no-ops; update make() to consume each relation
field (e.g., fields matching /_relation$/) and apply them to the resulting
struct by converting the relation into its foreign key columns (or using the
relation's primary key) so FK fields are set from the provided relation instead
of seeded defaults; locate the make() method and add logic that checks each
*_relation field, extracts the needed key(s) (or calls the relation's
make/into_key helper), and override the corresponding FK column entries in the
built `#struct_ident` accordingly.

}
}
}
Comment on lines +375 to +414
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Deduplicate field-construction logic shared with generate_factory_method_create.

has_custom_faker, fake_import, and the per-column column_fields mapping at lines 380-404 are a verbatim copy of lines 286-314 in generate_factory_method_create. Any future change to the defaulting strategy (new field attributes, alternate seed source, etc.) will need to be applied in both places, and these two paths will silently drift on the first miss.

Extract a small helper that returns the shared (fake_import, column_fields) token streams and call it from both methods.

♻️ Sketch of a shared helper
+    /// Shared field-construction tokens used by both `create()` and
+    /// `make()`. Returns `(fake_import, column_field_initializers)`.
+    fn generate_field_initializers(
+        &self,
+    ) -> (TokenStream, Vec<TokenStream>) {
+        let has_custom_faker = self
+            .analysis
+            .column_fields
+            .iter()
+            .any(|f| f.faker.is_some());
+
+        let fake_import = if has_custom_faker {
+            quote! { use ::fabrique::fake::Fake; }
+        } else {
+            quote! {}
+        };
+
+        let column_fields = self
+            .analysis
+            .column_fields
+            .iter()
+            .map(|field| {
+                let name = &field.ident;
+                let ty = &field.ty;
+                match &field.faker {
+                    Some(faker_expr) => quote! {
+                        `#name`: self.#name.unwrap_or_else(|| `#faker_expr.fake`())
+                    },
+                    None => quote! {
+                        `#name`: self.#name.unwrap_or_else(::fabrique::seeded_value::<#ty>)
+                    },
+                }
+            })
+            .collect();
+
+        (fake_import, column_fields)
+    }
+
     fn generate_factory_method_make(&self) -> TokenStream {
         let struct_ident = &self.analysis.ident;
-
-        let has_custom_faker = self
-            .analysis
-            .column_fields
-            .iter()
-            .any(|f| f.faker.is_some());
-
-        let fake_import = if has_custom_faker {
-            quote! { use ::fabrique::fake::Fake; }
-        } else {
-            quote! {}
-        };
-
-        let column_fields = self.analysis.column_fields.iter().map(|field| { /* ... */ });
+        let (fake_import, column_fields) = self.generate_field_initializers();

         quote! {
             pub fn make(self) -> `#struct_ident` {
                 `#fake_import`
                 `#struct_ident` {
                     #(`#column_fields`,)*
                 }
             }
         }
     }

Then mirror the same call inside generate_factory_method_create in place of lines 286-314.

As per coding guidelines on essential refactors: code smells such as duplicate code (copy/paste, similar logic) should be removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fabrique-derive/src/codegen/factory.rs` around lines 375 - 414, The duplicate
logic that computes has_custom_faker, fake_import, and the per-column
column_fields in generate_factory_method_make is identical to
generate_factory_method_create; extract a private helper (e.g.,
build_fake_import_and_column_fields or prepare_field_defaults) that takes &self
and returns a tuple of TokenStreams (fake_import, column_fields) or a small
struct, implement the shared loop that inspects self.analysis.column_fields and
builds the per-field tokens and fake import there, and then call that helper
from both generate_factory_method_make and generate_factory_method_create to
replace the duplicated code paths (refer to generate_factory_method_make,
generate_factory_method_create, and the new helper name when updating callers).


/// Generates setter methods for each field in the factory struct.
///
/// Each setter method takes a value and stores it in the factory's optional
Expand Down Expand Up @@ -661,6 +705,15 @@ mod tests {
}
}

pub fn make(self) -> Anvil {
Anvil {
id: self.id.unwrap_or_else(::fabrique::seeded_value::<u32>),
hammer_id: self.hammer_id.unwrap_or_else(::fabrique::seeded_value::<u32>),
hardness: self.hardness.unwrap_or_else(::fabrique::seeded_value::<u32>),
weight: self.weight.unwrap_or_else(::fabrique::seeded_value::<u32>),
}
}

pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
Expand Down
Loading
Loading