- 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("%s@carrier.go", 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/cmdAfter 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 initThe command above will generate carrier directory under current directory:
βββ carrier
βββ schema
βββ schema.goIt'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 generateOr can use custom schema path:
go run github.com/Yiling-J/carrier/cmd generate ./your/carrier/schemaThis produces the following files:
βββ carrier
Β Β βββ factory
β βββ base.go
β βββ ent_user.go
Β Β βΒ Β βββ user.go
βββ schema
βΒ Β βββ schema.go
βββ factory.goHere 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}Createstruct whichentgenerated. 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.PostFieldrequireName(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
Tostruct has 4 types:- Default:
Set{Field}Default - Sequence:
Set{Field}Sequence - Lazy:
Set{Field}Lazy - Factory:
Set{Field}Factory
- Default:
-
Each field in
[]Postshas 1 type:- Post:
Set{PostField}PostFunc
- Post:
-
Each name in
[]Traitshas 1 type:- Trait:
Set{TraitName}Trait
- Trait:
-
Each
MetaFactoryhas 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("%s@carrier.go", 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("%s@carrier.com", 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 "apple@carrier.com", 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.