- Statically Typed - 100% statically typed using code generation
- Developer Friendly API - explicit API with method chaining support
- Feature Rich - Default/Sequence/SubFactory/PostHook/Trait
- Ent Support - ent: An Entity Framework For Go
A snippet show how carrier works:
- You have a model
type User struct {
Name string
Email string
Group *Group
}
- Add carrier schema
Schemas := []carrier.Schema{
&carrier.StructSchema{
To: model.User{},
},
}
- Generate Structs 🎉
userMetaFactory := carrier.UserMetaFactory()
userFactory := userMetaFactory.
SetNameDefault("carrier").
SetEmailLazy(func(ctx context.Context, i *model.User) (string, error) {
return fmt.Sprintf("%[email protected]", i.Name), nil
}).
SetGroupFactory(groupFactory.Create).
Build()
user, err := userFactory.Create(ctx)
users, err := userFactory.CreateBatch(ctx, 5)
go get github.com/Yiling-J/carrier/cmd
After installing carrier
codegen, go to the root directory(or the directory you think carrier should stay) of your project, and run:
go run github.com/Yiling-J/carrier/cmd init
The command above will generate carrier
directory under current directory:
└── carrier
└── schema
└── schema.go
It's up to you where the carrier directory should be, just remember to use the right directory in MetaFactory Generation step.
Edit schema.go
and add some schemas:
> struct
package schema
import (
"github.com/Yiling-J/carrier"
)
var (
Schemas = []carrier.Schema{
&carrier.StructSchema{
To: model.User{},
},
}
)
> ent
To support ent, you need to provide the ent.{Name}Create
struct to schema, so carrier can get enough information.
package schema
import (
"github.com/Yiling-J/carrier"
"your/ent"
)
var (
Schemas = []carrier.Schema{
&carrier.EntSchema{
To: &ent.UserCreate{},
},
}
)
The To
field only accept struct/struct pointer, carrier will valid that on generation step. Schema definition reference
Run code generation from the root directory of the project as follows:
# this will use default schema path ./carrier/schema
go run github.com/Yiling-J/carrier/cmd generate
Or can use custom schema path:
go run github.com/Yiling-J/carrier/cmd generate ./your/carrier/schema
This produces the following files:
└── carrier
├── factory
│ ├── base.go
│ ├── ent_user.go
│ └── user.go
├── schema
│ └── schema.go
└── factory.go
Here factory.go
include all meta factories you need.
Also all ent files and meta factories will have ent
prefix to avoid name conflict.
If you update schemas, just run generate
again.
To construct a real factory for testing:
Create MetaFactory struct
userMetaFactory := carrier.UserMetaFactory()
Build factory from meta factory
userFactory := userMetaFactory.SetNameDefault("carrier").Build()
MetaFactory provide several methods to help you initial field values automatically.
Create structs
> struct
user, err := userFactory.Create(context.TODO())
users, err := userFactory.CreateBatch(context.TODO(), 3)
> ent
// need ent client
user, err := userFactory.Client(entClient).Create(context.TODO())
user, err := userFactory.Client(entClient).CreateBatch(context.TODO(), 3)
Use factory wrapper
Carrier also include a wrapper where you can put all your factories in:
> struct
factory := carrier.NewFactory()
factory.SetUserFactory(userFactory)
factory.UserFactory().Create(context.TODO())
> ent
factory := carrier.NewEntFactory(client)
// this step will assign factory client to userFactory also
factory.SetUserFactory(userFactory)
factory.UserFactory().Create(context.TODO())
// access ent client
client := factory.Client()
There are 2 kinds of schemas StructSchema
and EntSchema
,
both of them implement carrier.Schema
interface so you can put them in the schema slice.
Each schema has 4 fields:
-
Alias: Optional. If you have 2 struct type from different package, but have same name, add alias for them. Carrier will use alias directly as factory name.
-
To: Required. For
StructSchema
, this is the struct factory should generate. Carrier will get struct type from it and used in code generation, Only public fields are concerned. ForEntSchema
, this field should be the{SchemaName}Create
struct whichent
generated. Carrier will look up allSet{Field}
methods and generate factory based on them. Both struct and pointer of struct are OK. -
Traits: Optional. String slice of trait names. Traits allow you to group attributes together and override them at once.
-
Posts: Optional. Slice of
carrier.PostField
. Eachcarrier.PostField
requireName
(string) andInput
(any interface{}), and map to a post function after code generation. Post function will run after struct created, with input value as param.
MetaFactory API can be categorized into 8 types of method:
-
Each field in
To
struct has 4 types:- Default:
Set{Field}Default
- Sequence:
Set{Field}Sequence
- Lazy:
Set{Field}Lazy
- Factory:
Set{Field}Factory
- Default:
-
Each field in
[]Posts
has 1 type:- Post:
Set{PostField}PostFunc
- Post:
-
Each name in
[]Traits
has 1 type:- Trait:
Set{TraitName}Trait
- Trait:
-
Each
MetaFactory
has 2 type:- BeforeCreate:
SetBeforeCreateFunc
- AfterCreate:
SetAfterCreateFunc
- BeforeCreate:
The evaluation order of these methods are:
Trait -> Default/Sequence/Factory -> Lazy -> BeforeCreate -> Create -> AfterCreate -> Post
Create
only exists in ent factory, will call ent builder Save
method.
Put Trait
first because Trait
can override other types.
All methods except Default
and Trait
use function as input and it's fine to set it to nil
. This is very useful in Trait
override.
Set a fixed default value for field.
userMetaFactory.SetNameDefault("carrier")
If a field should be unique, and thus different for all built structs, use a sequence. Sequence counter is shared by all fields in a factory, not a single field.
// i is the current sequence counter
userMetaFactory.SetNameSequence(
func(ctx context.Context, i int) (string, error) {
return fmt.Sprintf("user_%d", i), nil
},
),
The sequence counter is concurrent safe and increase by 1 each time factory's Create
method called.
For fields whose value is computed from other fields, use lazy attribute. Only Default/Sequence/Factory values are accessible in the struct.
userMetaFactory.SetEmailLazy(
func(ctx context.Context, i *model.User) (string, error) {
return fmt.Sprintf("%[email protected]", i.Name), nil
},
)
> ent
Ent is a little different because the struct is created after Save
. And carrier call ent's Set{Field}
method to set values.
So the input param here is not *model.User
, but a temp containter struct created by carrier, hold all fields you can set.
entUserMetaFactory.SetEmailLazy(
func(ctx context.Context, i *factory.EntUserMutator) (string, error) {
return fmt.Sprintf("%[email protected]", i.Name), nil
},
)
You can get original ent mutation using i.EntCreator()
:
entUserMetaFactory.SetEmailLazy(
func(ctx context.Context, i *factory.EntUserMutator) (string, error) {
i.EntCreator().SetName("apple")
return "[email protected]", nil
},
)
If a field's value has related factory, use relatedFactory.Create
method as param here. You can also set the function manually.
// User struct has a Group field, type is Group
userMetaFactory.SetGroupFactory(groupFactory.Create)
> ent
Make sure related factory's ent client is set. By using factory wrapper or set it explicitly.
For struct factory, before create function is called after all lazy functions done. For ent factory, before create function is called right before to ent's Save
method.
groupMetaFactory.SetBeforeCreateFunc(func(ctx context.Context, i *model.User) error {
return nil
})
> ent
entUserMetaFactory.SetBeforeCreateFunc(func(ctx context.Context, i *factory.EntUserMutator) error {
return nil
})
For struct factory, after create function is called after all lazy functions done. For ent factory, after create function is called next to ent's Save
method.
userMetaFactory.SetAfterCreateFunc(func(ctx context.Context, i *model.User) error {
fmt.Printf("user: %d saved", i.Name)
return nil
})
Post functions will run once AfterCreate
step done.
// user MetaFactory
userMetaFactory.SetWelcomePostFunc(
func(ctx context.Context, set bool, obj *model.User, i string) error {
if set {
message.SendTo(obj, i)
}
return nil
},
)
// user Factory, send welcome message
userFactory.SetWelcomePost("welcome to carrier").Create(context.TODO())
// user Factory, no welcome message
userFactory.Create(context.TODO())
Trait is used to override some fields at once, activated by With{Name}Trait
method.
// override name
userMetaFactory.SetGopherTrait(factory.UserTrait().SetNameDefault("gopher"))
// user Factory
userFactory.WithGopherTrait().Create(context.TODO())
The Trait
struct share same API with MetaFactory
except Set{Name}Trait
one, that means you can override 6 methods within a trait.
Trait
only override methods you explicitly set, the exampe above will only override name field. So you can combine multiple traits together,
each change some parts of the struct. If multiple traits override same field, the last one will win:
userMetaFactory.SetGopherTrait(factory.UserTrait().SetNameDefault("gopher")).
SetFooTrait(factory.UserTrait().SetNameDefault("foo"))
// user name is foo
userFactory.WithGopherTrait().WithFooTrait().Create(context.TODO())
// user name is gopher
userFactory.WithFooTrait().WithGopherTrait().Create(context.TODO())
This is the final step for MetaFactory
definition, call this method will return a Factory
which you can use to create structs.
Factory API provide 3 types of method, Set{Field}
to override some field, Set{Field}Post
to call post function and With{Name}Trait
to enable trait.
Override field value. This method has the highest priority and will override your field method in MetaFactory
.
userFactory.SetName("foo").Create(context.TODO())
Call post function defined in MetaFactory
with param.
// create a user with 3 friends
userFactory.SetFriendsPost(3).Create(context.TODO())
Enable a named trait. If you enable multi traits, and traits have overlapping, the latter one will override the former.
userFactory.WithFooTrait().WithBarTrait().Create(context.TODO())
Create pointer of struct.
Create struct.
Create slice of struct pointer.
Create slice of struct.