Code Generation

Juice provides a code-generation tool to simplify development.

This page introduces the basic workflow.

Installing juicecli

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:

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:

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:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <environments default="prod">
        <environment id="prod">
            <dataSource>root:qwe123@tcp(localhost:3306)/database?charset=utf8mb4&amp;parseTime=true</dataSource>
            <driver>mysql</driver>
        </environment>
    </environments>

    <mappers>
        <mapper namespace="main.UserRepository">

            <insert id="CreateUser">
                insert into user (name) values (#{name})
            </insert>

            <delete id="DeleteUserByID">
                delete from user where id = #{param}
            </delete>

            <update id="UpdateUserNameByID">
                update user set name = #{name} where id = #{id}
            </update>

            <select id="GetUserByID">
                select * from user where id = #{param}
            </select>
        </mapper>
    </mappers>
</configuration>

Then run:

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 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:

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:

go run .

Do not use go run main.go here.

Console output:

[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:

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:

juicecli impl --type=UserRepository --output=user_repo.go

or:

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:

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:

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:

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:

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:

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:

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:

//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.