Skip to content

Commit

Permalink
Feat/update readme and example (#41)
Browse files Browse the repository at this point in the history
* feat: update readme

* feat: update code example

* feat: add best practice

* refactor: correct format

---------

Co-authored-by: Eyo Chen <[email protected]>
  • Loading branch information
eyo-chen and Eyo Chen authored Sep 29, 2024
1 parent 0521de6 commit a07937a
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 83 deletions.
177 changes: 118 additions & 59 deletions examples/association_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,106 +14,165 @@ import (
"github.com/eyo-chen/gofacto/typeconv"
)

// order has a foreign key field `CustomerID` to customer
type order struct {
// expense has two foreign key fields `UserID` and `CategoryID` to `User` and `Category` structs
type expense struct {
ID int
CustomerID int `gofacto:"struct:customer"` // set the correct tag
Amount int
UserID int `gofacto:"foreignKey,struct:User"`
CategoryID int `gofacto:"foreignKey,struct:Category,table:categories"`
}

// Example_association demonstrates how to build associations value in an easy way.
// category has a foreign key field `UserID` to `User` struct
type category struct {
ID int
UserID int `gofacto:"foreignKey,struct:User"`
}

type user struct {
ID int
}

// Example_association_basic demonstrates how to build basic associations value in an easy way.
// First, we need to set the correct tag on the foreign key field to tell gofacto which struct to associate with.
// Then, we can use `WithOne` or `WithMany` to create the association value and set the connection between the two structs.
func Example_association() {
// init a oreder factory
f := gofacto.New(order{}).
func Example_association_basic() {
f := gofacto.New(category{}).
WithDB(mysqlf.NewConfig(nil)) // you should pass db connection

// build one category with one user
user1 := user{}
category1, err := f.Build(ctx).
WithOne(&user1). // must pass the struct pointer to WithOne or WithMany
Insert()
if err != nil {
panic(err)
}
fmt.Println(category1.UserID == user1.ID) // true

// build two categories with two users
user2 := user{}
user3 := user{}
categories1, err := f.BuildList(ctx, 2).
WithMany([]interface{}{&user2, &user3}). // must pass the struct pointer to WithOne or WithMany
Insert()
if err != nil {
panic(err)
}
fmt.Println(categories1[0].UserID == user2.ID) // true
fmt.Println(categories1[1].UserID == user3.ID) // true

// build two categories with one user
user4 := user{}
categories2, err := f.BuildList(ctx, 2).
WithOne(&user4). // must pass the struct pointer to WithOne or WithMany
Insert()
if err != nil {
panic(err)
}
fmt.Println(categories2[0].UserID == user4.ID) // true
fmt.Println(categories2[1].UserID == user4.ID) // true
}

// Example_association_advanced demonstrates how to build advanced associations value in an easy way.
// In this example, we will build the expense with user and category, and the category is associated with user.
func Example_association_advanced() {
f := gofacto.New(expense{}).
WithDB(mysqlf.NewConfig(nil)) // you should pass db connection

// build one order with one customer
customer1 := customer{}
order1, err := f.Build(ctx).
WithOne(&customer1). // must pass the struct pointer to WithOne or WithMany
// build one expense with one user and one category
user1 := user{}
category1 := category{}
expense1, err := f.Build(ctx).
WithOne(&user1).
WithOne(&category1).
Insert()
if err != nil {
panic(err)
}
fmt.Println(order1) // {ID: 1, CustomerID: 1, Amount: {{non-zero value}}}
fmt.Println(customer1) // {ID: 1, Gender: "", Name: {{non-zero value}}, Age: {{non-zero value}}}
fmt.Println(order1.CustomerID == customer1.ID) // true

// build two orders with two customers
customer2 := customer{}
customer3 := customer{}
orders1, err := f.BuildList(ctx, 2).
WithMany([]interface{}{&customer2, &customer3}). // must pass the struct pointer to WithOne or WithMany
fmt.Println(expense1.UserID == user1.ID) // true
fmt.Println(expense1.CategoryID == category1.ID) // true
fmt.Println(category1.UserID == user1.ID) // true
// You can also use .WithOne(&user1, &category1) to pass multiple structs to WithOne

// build two expenses with two users and two categories
user2 := user{}
user3 := user{}
category2 := category{}
category3 := category{}
expenses1, err := f.BuildList(ctx, 2).
WithMany([]interface{}{&user2, &user3}). // must pass same type of structs to WithMany
WithMany([]interface{}{&category2, &category3}).
Insert()
if err != nil {
panic(err)
}
fmt.Println(orders1[0]) // {ID: 2, CustomerID: 2, Amount: {{non-zero value}}}
fmt.Println(orders1[1]) // {ID: 3, CustomerID: 3, Amount: {{non-zero value}}}
fmt.Println(customer2) // {ID: 2, Gender: "", Name: {{non-zero value}}, Age: {{non-zero value}}}
fmt.Println(customer3) // {ID: 3, Gender: "", Name: {{non-zero value}}, Age: {{non-zero value}}}
fmt.Println(orders1[0].CustomerID == customer2.ID) // true
fmt.Println(orders1[1].CustomerID == customer3.ID) // true

// build two orders with one customer
customer4 := customer{}
orders2, err := f.BuildList(ctx, 2).
WithOne(&customer4). // must pass the struct pointer to WithOne or WithMany
fmt.Println(expenses1[0].UserID == user2.ID) // true
fmt.Println(expenses1[1].UserID == user3.ID) // true
fmt.Println(expenses1[0].CategoryID == category2.ID) // true
fmt.Println(expenses1[1].CategoryID == category3.ID) // true
fmt.Println(category2.UserID == user2.ID) // true
fmt.Println(category3.UserID == user3.ID) // true

// build two expenses with one user and two categories
user5 := user{}
category4 := category{}
category5 := category{}
expenses2, err := f.BuildList(ctx, 2).
WithOne(&user5).
WithMany([]interface{}{&category4, &category5}).
Insert()
if err != nil {
panic(err)
}
fmt.Println(orders2[0]) // {ID: 4, CustomerID: 4, Amount: {{non-zero value}}}
fmt.Println(orders2[1]) // {ID: 5, CustomerID: 4, Amount: {{non-zero value}}}
fmt.Println(customer4) // {ID: 4, Gender: "", Name: {{non-zero value}}, Age: {{non-zero value}}}
fmt.Println(orders2[0].CustomerID == customer4.ID) // true
fmt.Println(orders2[1].CustomerID == customer4.ID) // true
fmt.Println(expenses2[0].UserID == user5.ID) // true
fmt.Println(expenses2[1].UserID == user5.ID) // true
fmt.Println(expenses2[0].CategoryID == category4.ID) // true
fmt.Println(expenses2[1].CategoryID == category5.ID) // true
fmt.Println(category4.UserID == user5.ID) // true
fmt.Println(category5.UserID == user5.ID) // true
}

// InsertOrders demonstrates how to use the functionality of `typeconv` package to simplify the code.
// InsertCategories demonstrates how to use the functionality of `typeconv` package to simplify the code.
// In some cases, we might want to wrap the insert logic into a function.
// In this case, we define the `InsertOrders` function to insert `n` orders with `n` customers.
func InsertOrders(ctx context.Context, f *gofacto.Factory[order], n int) ([]order, []customer, error) {
// use `ToAnysWithOW` to generate `n` customers with any type
// The first parameter is the number of customers to generate
// In this case, we define the `InsertCategories` function to insert `n` categories with `n` users.
func InsertCategories(ctx context.Context, f *gofacto.Factory[category], n int) ([]category, []user, error) {
// use `ToAnysWithOW` to generate `n` users with any type
// The first parameter is the number of users to generate
// The second parameter is the value to override the default value(we pass nil because we don't want to override the default value)
customersAny := typeconv.ToAnysWithOW[customer](n, nil)
usersAny := typeconv.ToAnysWithOW[user](n, nil)

orders, err := f.BuildList(ctx, n).
WithMany(customersAny).
categories, err := f.BuildList(ctx, n).
WithMany(usersAny).
Insert()
if err != nil {
return nil, nil, err
}

// convert the `[]any` to `[]customer` using `ToT`
customers := typeconv.ToT[customer](customersAny)
// convert the `[]any` to `[]user` using `ToT`
users := typeconv.ToT[user](usersAny)

return orders, customers, nil
return categories, users, nil
}

// Without the `typeconv` package, we would need to manually convert the `[]any` to `[]customer` using `ToT`
func InsertOrdersWithoutTypeconv(ctx context.Context, f *gofacto.Factory[order], n int) ([]order, []customer, error) {
// manually create `n` customers with any type
customersAny := make([]interface{}, n)
// Without the `typeconv` package, we would need to manually convert the `[]any` to `[]user` using `ToT`
func InsertCategoriesWithoutTypeconv(ctx context.Context, f *gofacto.Factory[category], n int) ([]category, []user, error) {
// manually create `n` users with any type
usersAny := make([]interface{}, n)
for i := 0; i < n; i++ {
customersAny[i] = &customer{}
usersAny[i] = &user{}
}

orders, err := f.BuildList(ctx, n).
WithMany(customersAny).
categories, err := f.BuildList(ctx, n).
WithMany(usersAny).
Insert()
if err != nil {
return nil, nil, err
}

// manually convert the `[]any` to `[]customer`
customers := make([]customer, n)
// manually convert the `[]any` to `[]user`
users := make([]user, n)
for i := 0; i < n; i++ {
customers[i] = *customersAny[i].(*customer)
users[i] = *usersAny[i].(*user)
}

return orders, customers, nil
return categories, users, nil
}
92 changes: 68 additions & 24 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ gofacto is a strongly-typed and user-friendly factory library for Go, designed t

- Intuitive and straightforward usage
- Strong typing and type safety
- Flexible data customization
- Support for various databases and ORMs
- Basic association relationship support
- Basic and multi-level association relationship support

&nbsp;

Expand Down Expand Up @@ -210,7 +211,7 @@ order, err := factory.Build(ctx).WithOne(&c).Insert()
// build two orders with two customers
c1 := Customer{}
c2 := Customer{}
orders, err := factory.BuildList(ctx, 2).WithMany(&c1, &c2).Insert()
orders, err := factory.BuildList(ctx, 2).WithMany([]interface{}{&c1, &c2}).Insert()
// orders[0].CustomerID == c1.ID
// orders[1].CustomerID == c2.ID

Expand All @@ -221,8 +222,73 @@ orders, err := factory.BuildList(ctx, 2).WithOne(&c1).Insert()
// orders[1].CustomerID == c1.ID
```

If there are multiple level association relationships, both `WithOne` and `WithMany` methods can also come in handy.<br>
Suppose we have a following schema:
```go
type Expense struct {
ID int
UserID int `gofacto:"foreignKey,struct:User"`
CategoryID int `gofacto:"foreignKey,struct:Category,table:categories"`
}

type Category struct {
ID int
UserID int `gofacto:"foreignKey,struct:User"`
}

type User struct {
ID int
}
```

We can build the `Expense` struct with the associated `User` and `Category` structs by using `WithOne` and `WithMany` methods.
```go
// build one expense with one user and one category
user := User{}
category := Category{}
expense, err := factory.Build(ctx).WithOne(&user).WithOne(&category).Insert()
// expense.UserID == user.ID
// expense.CategoryID == category.ID
// category.UserID == user.ID

// build two expenses with two users and two categories
user1 := User{}
user2 := User{}
category1 := Category{}
category2 := Category{}
expenses, err := factory.BuildList(ctx, 2).WithMany([]interface{}{&user1, &user2}).WithMany([]interface{}{&category1, &category2}).Insert()
// expenses[0].UserID == user1.ID
// expenses[0].CategoryID == category1.ID
// expenses[1].UserID == user2.ID
// expenses[1].CategoryID == category2.ID
// category1.UserID == user1.ID
// category2.UserID == user2.ID
```

This is one of the most powerful features of gofacto, it helps us easily build the structs with the complex associations relationships as long as setting the correct tags in the struct.<br>

Find out more [examples](https://github.com/eyo-chen/gofacto/blob/main/examples/association_test.go).


<details>
<summary>Best Practice to use <code>WithOne</code> & <code>WithMany</code></summary>
<ul>
<li>Must pass the struct pointer to <code>WithOne</code> or <code>WithMany</code></li>
<li>Must pass same type of struct pointer to <code>WithMany</code></li>
<li>Do not pass struct with cyclic dependency</li>
</ul>

// Do not do this:
type A struct {
B_ID int `gofacto:"foreignKey,struct:B"`
}
type B struct {
A_ID int `gofacto:"foreignKey,struct:A"`
}
</details>



### Reset
Use `Reset` method to reset the factory.
```go
Expand Down Expand Up @@ -403,28 +469,6 @@ type Order struct {
If the struct has a custom type, gofacto will ignore the field, and leave it as zero value.<br>
The clients need to set the values manually by using blueprint or overwrite if they don't want the zero value.<br>

3. gofacto only supports basic associations relationship.
If your database schema has a complex associations relationship, you might need to set the associations manually.<br>
Suppose you have a following schema:
```go
type Expense struct {
ID int
CategoryID int
}

type Category struct {
ID int
UserID int
}

type User struct {
ID int
}
```
`Expense` struct has a foreign key `CategoryID` to `Category` struct, and `Category` struct has a foreign key `UserID` to `User` struct.<br>
When building `Expense` data with `WithOne(&Category{})`, `Category` struct will be built, but `User` struct will not be built. The clients need to build `User` struct manually and set `UserID` to `Category` struct.


&nbsp;

# Acknowledgements
Expand Down

0 comments on commit a07937a

Please sign in to comment.