Code Generation =============== Juice provides a code-generation tool to simplify development. This page introduces the basic workflow. Installing juicecli ------------------- .. code-block:: shell go install github.com/go-juicedev/juicecli@latest After installation, run ``juicecli`` in the terminal to verify that it is available. Quick Start ----------- Prepare a ``user`` table. The example below uses MySQL: .. code-block:: sql CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; In most cases, you define an interface first: .. code-block:: go package main import ( "context" "database/sql" "github.com/go-juicedev/juice" _ "github.com/go-sql-driver/mysql" ) type User struct { ID int64 `column:"id" autoincr:"true"` Name string `param:"name" column:"name"` } type UserRepository interface { CreateUser(ctx context.Context, user *User) (sql.Result, error) DeleteUserByID(ctx context.Context, id int64) (sql.Result, error) UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error) GetUserByID(ctx context.Context, id int64) (*User, error) } Next, write the mapper. Create ``config.xml``: .. code-block:: xml root:qwe123@tcp(localhost:3306)/database?charset=utf8mb4&parseTime=true mysql insert into user (name) values (#{name}) delete from user where id = #{param} update user set name = #{name} where id = #{id} Then run: .. code-block:: shell juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go After the command finishes, a new ``user_repo.go`` file will be generated in the current directory. Generated example: .. code-block:: go // Code generated by "juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go"; DO NOT EDIT. package main import ( "context" "database/sql" "github.com/go-juicedev/juice" ) type UserRepositoryImpl struct{} func (u UserRepositoryImpl) CreateUser(ctx context.Context, user *User) (result0 sql.Result, result1 error) { manager, err := juice.ManagerFromContext(ctx) if err != nil { return nil, err } var iface UserRepository = u executor := juice.NewGenericManager[any](manager).Object(iface.CreateUser) return executor.ExecContext(ctx, user) } func (u UserRepositoryImpl) DeleteUserByID(ctx context.Context, id int64) (result0 sql.Result, result1 error) { manager, err := juice.ManagerFromContext(ctx) if err != nil { return nil, err } var iface UserRepository = u executor := juice.NewGenericManager[any](manager).Object(iface.DeleteUserByID) return executor.ExecContext(ctx, id) } func (u UserRepositoryImpl) UpdateUserNameByID(ctx context.Context, id int64, name string) (result0 sql.Result, result1 error) { manager, err := juice.ManagerFromContext(ctx) if err != nil { return nil, err } var iface UserRepository = u executor := juice.NewGenericManager[any](manager).Object(iface.UpdateUserNameByID) return executor.ExecContext(ctx, juice.H{"id": id, "name": name}) } func (u UserRepositoryImpl) GetUserByID(ctx context.Context, id int64) (result0 *User, result1 error) { manager, err := juice.ManagerFromContext(ctx) if err != nil { return nil, err } var iface UserRepository = u executor := juice.NewGenericManager[User](manager).Object(iface.GetUserByID) ret, err := executor.QueryContext(ctx, id) return &ret, err } func NewUserRepository() UserRepository { return &UserRepositoryImpl{} } The tool automatically generates the concrete implementation for the interface you defined. Using the Generated Code ------------------------ Now complete the earlier example: .. code-block:: go package main import ( "context" "database/sql" "fmt" "github.com/go-juicedev/juice" _ "github.com/go-sql-driver/mysql" ) type User struct { ID int64 `column:"id" autoincr:"true"` Name string `param:"name" column:"name"` } type UserRepository interface { CreateUser(ctx context.Context, user *User) (sql.Result, error) DeleteUserByID(ctx context.Context, id int64) (sql.Result, error) UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error) GetUserByID(ctx context.Context, id int64) (*User, error) } func main() { cfg, err := juice.NewXMLConfiguration("config.xml") if err != nil { panic(err) } engine, err := juice.Default(cfg) if err != nil { panic(err) } ctx := juice.ContextWithManager(context.Background(), engine) userRepo := NewUserRepository() user := &User{Name: "eatmoreapple"} result, err := userRepo.CreateUser(ctx, user) if err != nil { panic(err) } id, err := result.LastInsertId() if err != nil { panic(err) } user, err = userRepo.GetUserByID(ctx, id) if err != nil { panic(err) } fmt.Println(user) } Run the code: .. code-block:: shell go run . Do not use ``go run main.go`` here. Console output: .. code-block:: shell [juice] 2023/06/13 14:41:05 [main.UserRepository.CreateUser] insert into user (name) values (?) [eatmoreapple] 6.745625ms [juice] 2023/06/13 14:41:05 [main.UserRepository.GetUserByID] select * from user where id = ? [1] 483.166µs &{1 eatmoreapple} Command Breakdown ----------------- The command: .. code-block:: shell juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go means: - ``impl``: generate an implementation for an interface - ``type``: the interface name to implement - ``config``: the path to the configuration file - ``namespace``: the namespace where Juice should look for actions - ``output``: the generated file name .. attention:: The interface method names must match the action IDs under the specified namespace. The command can often be simplified. ``config`` can be omitted if ``juicecli`` can automatically find ``config.xml`` or ``config/config.xml`` near the execution path. ``namespace`` can also be omitted. Juice can infer it from the relative path between ``go.mod`` and the Go file containing the interface. So you can shorten the command to: .. code-block:: shell juicecli impl --type=UserRepository --output=user_repo.go or: .. code-block:: shell juicecli impl -t UserRepository -o user_repo.go If ``output`` is omitted, the generated code is written to standard output. Interface Constraints --------------------- ``juicecli`` can parse interface signatures and generate implementations automatically, but the interface must follow a few rules. Context Parameter ~~~~~~~~~~~~~~~~~ Every method must take ``context.Context`` as the first argument: .. code-block:: go type UserRepository interface { GetUser(ctx context.Context, id int64) (*User, error) GetUserWithoutContext(id int64) (*User, error) } The first method is valid; the second is not. Error Return Value ~~~~~~~~~~~~~~~~~~ Following Go conventions, ``error`` must be the last return value: .. code-block:: go type UserRepository interface { CreateUser(ctx context.Context, user *User) error UpdateUser(ctx context.Context, user *User) (sql.Result, error) DeleteUser(ctx context.Context, id int64) (error, bool) } The first two signatures are valid; the third is not. Query Operations ~~~~~~~~~~~~~~~~ For ``select`` actions, the method must return data before ``error``: .. code-block:: go type UserRepository interface { GetUser(ctx context.Context, id int64) (*User, error) ListUsers(ctx context.Context) ([]*User, error) FindUser(ctx context.Context, id int64) error } The first two signatures are valid. The last one is not, because a query action must return data. Non-Query Operations ~~~~~~~~~~~~~~~~~~~~ For ``INSERT``, ``UPDATE``, and ``DELETE`` actions, the method may return only ``error`` or ``sql.Result`` plus ``error``: .. code-block:: go type UserRepository interface { DeleteUser(ctx context.Context, id int64) error UpdateUser(ctx context.Context, user *User) (sql.Result, error) CreateUser(ctx context.Context, user *User) (int64, error) } The first two are valid. The last one is not. Parameter Handling ~~~~~~~~~~~~~~~~~~ When a method has more than two parameters, all parameters except ``context`` are packaged into a map: .. code-block:: go type UserRepository interface { SearchUsers(ctx context.Context, name string, age int, city string) ([]*User, error) } // Internally: params := map[string]any{ "name": name, "age": age, "city": city, } Context Requirements ~~~~~~~~~~~~~~~~~~~~ Calls must use a context that carries a manager implementation: .. code-block:: go ctx := juice.ContextWithManager(context.Background(), manager) users, err := repo.SearchUsers(ctx, "John", 25, "New York") Using a plain ``context.Background()`` directly is not sufficient. Notes ----- - ``juicecli`` generates implementations according to these rules automatically. - Invalid interface definitions may cause generation failures or runtime errors. - Parameter names affect SQL parameter mapping, so name them carefully. go generate ----------- You can also integrate generation with ``go generate``: .. code-block:: go //go:generate juicecli impl -t UserRepository -o user_repo.go type UserRepository interface { CreateUser(ctx context.Context, user *User) (sql.Result, error) DeleteUserByID(ctx context.Context, id int64) (sql.Result, error) UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error) GetUserByID(ctx context.Context, id int64) (*User, error) } Add that line next to your interface definition and run ``go generate`` to produce the implementation.