kanna

Generated Go,
not magic Go.

Seven code generators for what every Go service repeats: validation, value types, the database, mapping, messages, wiring, and test data. Each reads what you have already written, from struct tags to your schema, and writes the plain Go you would otherwise write by hand.

kanna-validateRead the README

user.goyou write

//go:generate go tool kanna-validate

//kanna:validate
type CreateUserRequest struct {
	Name string `json:"name" validate:"required,max=50"`
	Age  int    `json:"age"  validate:"gte=18"`
}

validate_gen.gokanna-validate writes

func (x CreateUserRequest) Validate() error {
	var errs validate.Errors

	if x.Name == "" {
		errs.Add("name", validate.Blank())
	} else {
		if utf8.RuneCountInString(x.Name) > 50 {
			errs.Add("name", validate.TooLong(50))
		}
	}

	if x.Age < 18 {
		errs.Add("age", validate.GreaterThanOrEqualTo(18))
	}

	return errs.Err()
}

kanna-validate writes a Validate method from the validate tags of a struct, and reports what breaks under Rails' error codes, keyed by the fields' JSON names.

Read the README GitHub v0.0.11Go 1.27

go get -tool github.com/go-kanna/validate/cmd/kanna-validate
kanna-valueRead the README

model.goyou write

//go:generate go tool kanna-value

//kanna:value
type Email struct{ s string }

func (Email) validate(s string) error { return rule.Email(s) }

value_gen.gokanna-value writes

func NewEmail(v string) (Email, error) {
	if err := (Email{}).validate(v); err != nil {
		return Email{}, err
	}
	return Email{s: v}, nil
}

func (x *Email) UnmarshalJSON(data []byte) error {
	if string(data) == "null" {
		return nil
	}
	var v string
	if err := json.Unmarshal(data, &v); err != nil {
		return err
	}
	y, err := NewEmail(v)
	if err != nil {
		return err
	}
	*x = y
	return nil
}

// MustEmail, String, IsZero, MarshalJSON, Value, and Scan, which checks too.

kanna-value turns a struct holding one value and a validate method into a value type: constructors, an accessor, and JSON and SQL methods that hold every value to the rule, wherever it comes from. A string type with constants, marked //kanna:enum, becomes an enum that reads in only those.

Read the README GitHub v0.0.8Go 1.26

go get -tool github.com/go-kanna/value/cmd/kanna-value
kanna-ormRead the README

repository/order.goyou write

//go:generate go tool kanna-orm -destination ../postgres

//kanna:repository
type OrderRepository interface {
	List(ctx context.Context, s ...scopes.Order) ([]model.Order, error)
	Insert(ctx context.Context, o model.Order) (model.Order, error)
}

postgres/orm_gen.gokanna-orm writes

func (r OrderRepository) Insert(ctx context.Context, o model.Order) (model.Order, error) {
	const q = `INSERT INTO orders (id, customer_id, status, placed_at, note)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, customer_id, status, placed_at, note`
	var row model.Order
	err := r.db.QueryRow(ctx, q, o.ID, o.CustomerID, o.Status, o.PlacedAt, o.Memo).
		Scan(&row.ID, &row.CustomerID, &row.Status, &row.PlacedAt, &row.Memo)
	if err != nil {
		return model.Order{}, fmt.Errorf("insert into orders: %w", err)
	}
	return row, nil
}

scopes/orm_gen.gokanna-orm writes

// StatusNe narrows to the rows whose status is not v.
func (s Order) StatusNe(v model.OrderStatus) Order {
	return Order{s.q.Where(`status <> `, v)}
}

// ...a method for each column and relation, and Limit, After, and Cursor.

handler.goyou call

orders, err := repo.List(ctx, scopes.Orders().
	StatusNe(model.OrderCanceled).
	PlacedAtDesc().
	Limit(20))

kanna-orm writes the implementation of your repository interfaces, with the SQL in it as literals, and a scope type for each table whose methods narrow, order, page, and load relations.

Read the README GitHub v0.0.7Go 1.26

go get -tool github.com/go-kanna/orm/cmd/kanna-orm
kanna-mapperRead the README

mapper.goyou write

//go:generate go tool kanna-mapper

//kanna:mapper
type _ interface {
	EmployeeToProto(model.Employee) *employeev1.Employee
	EmployeeFromProto(*employeev1.Employee) (model.Employee, error)
}

//kanna:converter
var _ = []any{
	uuid.UUID.String,
	uuid.Parse,
	timestamppb.New,
	(*timestamppb.Timestamp).AsTime,
}

mapper_gen.gokanna-mapper writes

func EmployeeFromProto(src *employeev1.Employee) (model.Employee, error) {
	if src == nil {
		return model.Employee{}, nil
	}
	v, err := uuid.Parse(src.GetId())
	if err != nil {
		return model.Employee{}, fmt.Errorf("map model.Employee.ID: %w", err)
	}
	return model.Employee{
		ID:       v,
		FullName: src.GetName(),
		HiredAt:  src.GetHiredAt().AsTime(),
		Address:  addressToModel(src.GetAddress()),
	}, nil
}

// EmployeeToProto, and addressToModel and addressToEmployeev1, which they call.

kanna-mapper writes the functions between your domain types and the wire types you do not control. You declare the signatures; it writes the bodies, field by field, and handles protobuf oneofs, enums, and sum types.

Read the README GitHub v0.0.13Go 1.26

go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper
kanna-i18nRead the README

locales/you write

# en.yaml
hello: "Hello, {name}!"
items_count:
  plural:
    one: "You have {count} item."
    other: "You have {count} items."

# ja.yaml
hello: "こんにちは、{name}さん!"
items_count:
  plural:
    other: "アイテムが{count}個あります。"

messages/i18n_gen.gokanna-i18n writes

// Hello returns the message "hello":
//
//	Hello, {name}!
func Hello(name string) i18n.Message {
	return i18n.Message{Key: "hello", Args: []i18n.Arg{
		{Name: "name", Value: name},
	}}
}

// ItemsCount returns the message "items_count", in the form count selects:
//
//	one:   You have {count} item.
//	other: You have {count} items.
func ItemsCount(count int) i18n.Message {
	return i18n.Message{Key: "items_count", Args: []i18n.Arg{
		{Name: "count", Value: count},
	}}
}

handler.goyou call

loc := messages.Localizer(r.Header.Get("Accept-Language"))

// You have 1 item. or アイテムが1個あります。
fmt.Fprintln(w, loc.Localize(messages.ItemsCount(1)))

kanna-i18n compiles a directory of locale files into a typed constructor for every message, the translations compiled in: CLDR plurals, locale-formatted numbers, and a Localizer that takes an Accept-Language value.

Read the README GitHub v0.0.10Go 1.26

go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n
kanna-diRead the README

server.goyou write

//go:generate go tool kanna-di

//kanna:container
type server struct {
	_ user.Repository `di:"with=postgres.NewUserRepository"`

	users *handler.User
}

func openDB(cfg config.Config) (*sql.DB, func(), error)
func newLogger() *slog.Logger

di_gen.gokanna-di writes

func newServer() (*server, func(), error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, nil, err
	}
	db, cleanup, err := openDB(cfg)
	if err != nil {
		return nil, nil, err
	}
	repo := postgres.NewUserRepository(db)
	log := newLogger()
	registerUser := usecase.NewRegisterUser(repo, log)
	getUser := usecase.NewGetUser(repo)
	users := handler.NewUser(registerUser, getUser)

	return &server{
		users: users,
	}, cleanup, nil
}

kanna-di writes the constructor of a container struct from the functions next to your types, in the order they need each other. What a provider opens, it closes, through the one cleanup the constructor returns.

Read the README GitHub v0.0.7Go 1.26

go get -tool github.com/go-kanna/di/cmd/kanna-di
kanna-fixtureRead the README

model.goyou write

//go:generate go tool kanna-fixture

type User struct {
	ID        int64
	Name      string `fake:"{firstname} {lastname}"`
	Email     string `validate:"required,email"`
	Age       int    `validate:"gte=18,lte=130"`
	Role      Role
	CreatedAt time.Time
}

fixture/fixture_gen.gokanna-fixture writes

func User(setters ...func(*model.User)) model.User {
	n := seqUser.Add(1)
	f := gofakeit.NewFaker(rand.NewPCG(uint64(n), 0x5f2c8e0a9d4b7136), false)
	x := model.User{
		ID:        n,
		Name:      mustGenerate(f, "{firstname} {lastname}"),
		Email:     fmt.Sprintf("email-%d@example.com", n),
		Age:       int(18 + (n-1)%113),
		Role:      model.RoleAdmin,
		CreatedAt: time.Date(2000, time.January, 1, 0, 0, int(n), 0, time.UTC),
	}
	for _, set := range setters {
		set(&x)
	}
	return x
}

user_test.goyou call

u := fixture.User(func(u *model.User) { u.Age = 17 })
// Validating u reports age, and nothing else.

kanna-fixture writes a constructor for every struct of a package, every field filled: numbered so that no two collide, within the rules its validate tags declare, or from a gofakeit template where you ask for one.

Read the README GitHub v0.0.10Go 1.26

go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture

What each one writes

Plain Go declarations, in files headed DO NOT EDIT. Here is what the example in each repository gets, listed as go doc lists it.

kanna-ormscopes/orm_gen.go, postgres/orm_gen.go

type Order struct{ q orm.Of[model.Order] }
func Orders() Order
func (s Order) StatusEq(v model.OrderStatus) Order
func (s Order) PlacedAtDesc() Order
func (s Order) WithLines(sub ...OrderLine) Order
func (s Order) After(row model.Order) Order
func (s Order) AfterCursor(cursor string) (Order, error)
// ...79 methods on Order in all, for its columns, its relations, and paging

type OrderRepository struct{ db DBTX }
func NewOrderRepository(db DBTX) OrderRepository
func (r OrderRepository) Get(ctx context.Context, id model.OrderID, s ...scopes.Order) (model.Order, error)
func (r OrderRepository) List(ctx context.Context, s ...scopes.Order) ([]model.Order, error)
func (r OrderRepository) Insert(ctx context.Context, o model.Order) (model.Order, error)
// ...one for each method of the interface

kanna-validatevalidate_gen.go

func (x CreateUserRequest) Validate() error
func (x Address) Validate() error

kanna-valuevalue_gen.go

func NewEmail(v string) (Email, error)
func MustEmail(v string) Email
func (x Email) String() string
func (x Email) IsZero() bool
func (x Email) MarshalJSON() ([]byte, error)
func (x *Email) UnmarshalJSON(data []byte) error
func (x Email) Value() (driver.Value, error)
func (x *Email) Scan(src any) error

// and for the enum OrderStatus
func NewOrderStatus(v string) (OrderStatus, error)
func OrderStatusValues() []OrderStatus
func (x *OrderStatus) UnmarshalText(text []byte) error
func (x *OrderStatus) Scan(src any) error

kanna-mappermapper_gen.go

func EmployeeToProto(emp model.Employee) *employeev1.Employee
func EmployeeFromProto(emp *employeev1.Employee) (model.Employee, error)
func addressToEmployeev1(src model.Address) *employeev1.Address
func addressToModel(src *employeev1.Address) model.Address
// ...and the same two for Contact

kanna-i18nmessages/i18n_gen.go

func Hello(name string) i18n.Message
func ItemsCount(count int) i18n.Message
func TotalPrice(price float64) i18n.Message
func UserDeleted(name string) i18n.Message
func Localizer(langs ...string) i18n.Localizer

kanna-didi_gen.go

func newServer(logs io.Writer) (*server, func(), error)
func mustNewServer(logs io.Writer) (*server, func())

kanna-fixturefixture/fixture_gen.go

func Article(setters ...func(*blog.Article)) blog.Article
func Author(setters ...func(*blog.Author)) blog.Author
func SignUp(setters ...func(*blog.SignUp)) blog.SignUp

Learn one, and you know all seven

Every tool is installed, run, and checked in CI the same way. Nothing is set up but the line that runs it.

  1. Install it into your module

    go get -tool github.com/go-kanna/validate/cmd/kanna-validate

    The version is in your go.mod, so everyone on the project runs the same one.

  2. Mark what it reads

    //go:generate go tool kanna-validate
    
    //kanna:validate
    type CreateUserRequest struct {

    A comment next to the struct, the interface, or the directory it is about. Without kanna, your code reads the same.

  3. Generate

    go generate ./...

    Each tool writes its _gen.go files and prints nothing when it succeeds. A mistake stops it instead, at the file and line, with what you probably meant.

  4. Check in CI

    KANNA_CHECK=1 go generate ./...

    Fails when a committed file is not what its generator would write now, and writes nothing.

All seven, in the order they need

Tools read what other tools write, so with more than one, which runs first matters. kanna gen works that out from what each one reads and writes, and runs every //go:generate go tool kanna-* line once. There is nothing to configure, and no list of packages to keep.

The lines in your code, as go generate finds themyou write

  handler/handler.go:11   //go:generate go tool kanna-di
  mapper/todo.go:5        //go:generate go tool kanna-mapper
  model/todo.go:5         //go:generate go tool kanna-value
  model/todo.go:6         //go:generate go tool kanna-fixture
  usecase/put_todo.go:9   //go:generate go tool kanna-validate
  usecase/put_todo.go:10  //go:generate go tool kanna-di

go tool kanna genkanna writes

  model/todo.go:5         kanna-value     wrote model/value_gen.go
  model/todo.go:6         kanna-fixture   wrote model/fixture/fixture_gen.go
  usecase/put_todo.go:9   kanna-validate  wrote usecase/validate_gen.go
  usecase/put_todo.go:10  kanna-di        wrote usecase/di_gen.go
  mapper/todo.go:5        kanna-mapper    wrote mapper/mapper_gen.go
  handler/handler.go:11   kanna-di        wrote handler/di_gen.go
6 tasks, 6 files changed
  • See why

    go tool kanna gen --plan

    Prints what would run and why it comes where it does, such as code in usecase calls what kanna-validate generates, and runs nothing.

  • Check in CI

    go tool kanna gen --check

    Fails when a committed file is not what would be generated, and writes nothing.

  • Install it

    go get -tool github.com/go-kanna/kanna

    It runs the lines already in your code, so every tool goes on working under go generate without it. Read the README

Three Go proverbs every generator keeps

The tools do different jobs, and hold to the same three rules.

  • Clear is better than clever.

    What a generator writes is the code you would have written: if statements, function calls, and SQL in string literals. Step through it in a debugger, and see every change of a rule in a diff.

  • Reflection is never clear.

    Struct tags, directives, and locale files are read once, by go generate, not by reflection on every call. What runs is what the generated file shows.

  • A little copying is better than a little dependency.

    The generated code is yours, in your module. kanna-di and kanna-mapper import nothing at run time; a generator that needs a runtime ships a small one in its own module, so the two never drift apart.

From Rob Pike's Go Proverbs.

Seven tools, one service

go-kanna/showcase is a to-do service, gRPC over PostgreSQL, with every tool in its place. What the generators write is committed, so each file can be read beside what it came from: here, on wood.

showcase/├── config/│   ├── api_config.go│   └── validate_gen.go                 kanna-validate├── internal/│   ├── di/│   │   ├── infra_di.go│   │   └── di_gen.go                   kanna-di│   ├── feature/todo/│   │   ├── model/│   │   │   ├── todo_model.go│   │   │   ├── value_gen.go            kanna-value│   │   │   └── fixture/fixture_gen.go  kanna-fixture│   │   ├── scopes/│   │   │   ├── scopes.go│   │   │   └── orm_gen.go              kanna-orm│   │   ├── repository/│   │   │   └── todo_repository.go│   │   ├── postgres/│   │   │   ├── postgres_todo_repository.go│   │   │   ├── todo_queries.sql│   │   │   └── orm_gen.go              kanna-orm│   │   ├── usecase/│   │   │   ├── *_use_case.go│   │   │   ├── di_gen.go               kanna-di│   │   │   └── validate_gen.go         kanna-validate│   │   ├── mapper/│   │   │   ├── todo_mapper.go│   │   │   └── mapper_gen.go           kanna-mapper│   │   └── handler/│   │       ├── todo_handler.go│   │       └── di_gen.go               kanna-di│   └── messages/│       ├── locales/│       └── i18n_gen.go                 kanna-i18n└── kanna-orm.snapshot.json
  • kanna-dibuilds the infrastructure once, and the handler from it, choosing the repository its use cases are given
  • kanna-validatechecks what the use cases are given, and the settings read from the environment
  • kanna-valueholds a to-do's completion time between 2000 and 2100, wherever it goes, and its priority to the three it declares
  • kanna-ormimplements the repository the use cases depend on, and the scopes it reads through, as scopes.Todos().CreatedAtAsc()
  • kanna-mappermaps a to-do to its protobuf message, the input of PutTodo back from one, priorities by name, and the filter of ListTodo, a oneof, into a sum type
  • kanna-fixturemakes the to-dos the tests start from
  • kanna-i18ncompiles the service's messages, and the texts of validation errors, in English and Japanese
  • kanna genruns all of them behind make gen, and checks what is committed in CI

Read the code on GitHub

Every tool

Each is a module of its own: install the ones you need. Each page here is the tool's README.

  • Generates a Validate method from the validate tags of a struct, and reports what breaks under Rails' error codes, keyed by the fields' JSON names. Decodes a request and reports everything wrong with it at once.

    go get -tool github.com/go-kanna/validate/cmd/kanna-validate

    v0.0.11 Go 1.27

  • Turns a struct holding one value and a validate method into a value type, with constructors, an accessor, and JSON and SQL methods that hold every value to the rule wherever it comes from. A type of your own, so it takes your methods; the email, url, and uuid tags of kanna-validate call the same rules. A string type with constants becomes an enum that reads in only those, from JSON, text, and a database.

    go get -tool github.com/go-kanna/value/cmd/kanna-value

    v0.0.8 Go 1.26

  • Writes the implementation of your repository interfaces, with the SQL in it as literals, and a scope type for each table whose methods narrow, order, page, and load relations. It reads the schema from a snapshot of your migrated database, so a column or a type that does not match stops generation. PostgreSQL through pgx or database/sql, and MySQL 8.4 or later.

    go get -tool github.com/go-kanna/orm/cmd/kanna-orm

    v0.0.7 Go 1.26

  • Writes the functions between your domain types and the wire types you do not control. You declare the signatures; it writes the bodies, field by field, calling the conversions you list, uuid.Parse and timestamppb.New as they are, and handles protobuf oneofs. An enum of kanna-value maps to a protobuf enum by the names of their constants, and a sum type of the domain to a oneof; //kanna:mapper strict fills only the fields named for it.

    go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper

    v0.0.13 Go 1.26

  • Compiles a directory of locale files into a typed constructor for every message, with the translations themselves compiled in, so nothing is read or parsed at run time. CLDR plurals, locale-formatted numbers, and a Localizer that takes an Accept-Language value.

    go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n

    v0.0.10 Go 1.26

  • Writes the constructor of a container struct from the functions next to your types and next to the container: nothing registered, an interface bound to its implementation with one tag, cleanups returned as one, and nothing to import at run time.

    go get -tool github.com/go-kanna/di/cmd/kanna-di

    v0.0.7 Go 1.26

  • Writes a constructor for every struct of a package with every field filled: numbered so that no two collide, within the rules its validate tags declare, or from a gofakeit template, which is checked while generating.

    go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture

    v0.0.10 Go 1.26

  • Runs the generators of a module in the order they need, worked out from what each reads and writes. It adds no configuration: what it runs are the //go:generate lines already there, and it can show why each comes where it does, or check them in CI.

    go get -tool github.com/go-kanna/kanna

    v0.3.0 Go 1.26

Why a plane

A kanna (鉋) is the Japanese hand plane, pulled toward you across a board. A good one lifts a shaving thin enough to read through, and leaves a surface that needs nothing more.

That is the job we hold a generator to: take off exactly the code you would have written, thin enough to read, and leave the rest of your program alone. The shaving is the mark, and its wood is the color of everything a generator writes on this site.