kanna

module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT

README

kanna

CI codecov Go Reference

Go code generators built on the standard library — DI, struct mapping, test fixtures, ORM, i18n. Each works on its own and emits plain Go with no runtime reflection.

Your structs are the source of truth. Point a generator at a package and it writes the code you would otherwise write by hand, so the output stays readable, debuggable, and free of anything to learn at runtime.

Status

kanna is being assembled from generators that already exist as standalone tools. Each is ported onto a shared scanning layer as it lands.

Generator What it does Status
kanna-di dependency-injection constructors available
kanna-fixture test fixtures from model structs available
kanna-mapper struct-to-struct mapping available
kanna-orm query helpers from model structs available
kanna-i18n typed message constructors from locale files available

Nothing is released yet, so import paths and flags may still move.

kanna-di

Wires a container struct from the providers it can find, and writes a plain constructor.

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

A provider is a top-level function whose first result is a named type, a pointer to one, or an interface, optionally followed by an error. Nothing needs to be registered — kanna-di finds them by scanning the packages you point it at.

A container is a struct whose fields carry a di tag.

package app

//go:generate go tool kanna-di ./...

type DB struct{}

func NewDB() *DB { return &DB{} }

type User struct{ db *DB }

func NewUser(db *DB) User { return User{db: db} }

type Container struct {
	User User `di:""`
}

go generate ./...

writes di_gen.go next to it:

// Code generated by kanna. DO NOT EDIT.

package app

// NewContainer initializes dependencies and constructs Container.
func NewContainer() *Container {
	db := NewDB()
	user := NewUser(db)

	return &Container{
		User: user,
	}
}

When any provider in the chain returns an error, the constructor returns one too and propagates it.

Tags

A field may be named — the resolved value is stored in it — or blank (_), which declares something about the container without keeping a value.

Tag On a named field On a blank field
di:"" resolve from whichever provider returns the field's type
di:"with=<ref>" resolve from the named provider pick that provider for the type everywhere in this container
di:"arg" take it as a constructor parameter, named after its type, and store it take it as a parameter only
di:"arg=<name>" same, with the parameter name spelled out same, with the parameter name spelled out
di:"returns" store it and declare its type as the constructor's return type declare the return type only
di:"embed" take a struct as a parameter and offer its exported fields as resolution sources

<ref> may be a bare function name (NewWriter), a package-qualified one (config.NewWriter), or fully qualified ( github.com/me/config.NewWriter).

Directives

A //kanna:container comment above the struct adjusts what gets generated. It is optional — one di tag is enough to make a struct a container.

Directive Effect
//kanna:container name=<ident> name the constructor (default: New + struct name)
//kanna:container returns=<type> declare the return type (default: pointer to the struct)
//kanna:container must also emit MustNew*, which panics instead of returning the error

Write the tag against the comment marker. Go only treats //kanna:… as a directive when the two are adjacent; with a space, // kanna:container stays part of the doc comment and shows up in go doc and on pkg.go.dev. kanna does not honor that form either, and points out the spelling it expected.

returns= takes the container's own type to construct it by value, or an interface the container satisfies to hide the concrete type.

Flags
Flag Meaning
--must emit MustNew* for every container
--tags <list> comma-separated build tags
-check verify generated files are up to date instead of writing them
-v, --verbose verbose output
Example

examples/di wires a small application covering every directive and every tag except di:"arg=<name>", which needs a name collision before it is worth showing. CI regenerates it and fails if the output would change, so what you read there is what the generator currently produces.

kanna-fixture

Writes one constructor per struct in a package, with every field already filled, so a test states only what it cares about.

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

Point it at the package holding your models and at the directory the fixtures should live in. Every exported struct there gets a function — there is nothing to opt into, which is what keeps fixtures from falling behind the model.

package model

//go:generate go tool kanna-fixture -source ./model -destination ./fixture

type User struct {
	ID    int64
	Name  string
	Email string
	Age   int `fake:"{number:18,65}"`
}
go generate ./...

writes fixture/fixture_gen.go:

// Code generated by kanna. DO NOT EDIT.

package fixture

import (
	"github.com/brianvoe/gofakeit/v7"

	"example.com/app/model"
)

func User(setters ...func(m *model.User)) model.User {
	m := model.User{
		ID:    gofakeit.Int64(),
		Name:  gofakeit.Name(),
		Email: gofakeit.Email(),
		Age:   gofakeit.Number(18, 65),
	}
	for _, s := range setters {
		s(&m)
	}
	return m
}
u := fixture.User(func(m *model.User) { m.Email = "known@example.com" })

The generated code calls gofakeit, plus whatever else the values it builds need — github.com/google/uuid for a uuid.UUID field, for instance. When the destination module does not require one of them yet, the generator says so.

Generation is deterministic; the values are not. Seed the faker when a test needs the same data twice:

func TestMain(m *testing.M) {
	if err := gofakeit.Seed(1); err != nil {
		panic(err)
	}

	os.Exit(m.Run())
}
Inference

Each field takes the first rule that matches.

Rule Applies when Result
fake tag the field carries one see below
field name the name is known and the type agrees Email stringEmail()
field type the type has a faker boolBool()
a struct generated in this run the field is a value of that type Author UserUser()
otherwise zero value

Names matched on a string field: Email, Name, FirstName, LastName, Phone, URL, UUID, Address, City, Country. Any field whose name ends in At and whose type is time.Time gets a date.

Types matched: every string, bool, int, uint, and float kind, plus time.Time and github.com/google/uuid's UUID.

Left at the zero value: pointers, slices, maps, interfaces, channels, funcs, named basic types without a tag (the valid values are not knowable — type Status string could be anything), and any reference that would recurse, including a struct that points back at its own type.

Unexported fields are skipped, since the generated package cannot set them.

Tags

The fake tag follows gofakeit's own template syntax, so there is nothing new to learn.

Tag Effect
fake:"{email}" use that generator — also {firstname}, {name}, {phone}, {url}, and others
fake:"{number:18,65}" a parameterized generator — also {intrange:…}, {uintrange:…}, {price:…}
fake:"???-####" any other template, resolved at run time through gofakeit.Generate
fake:"skip" or "-" leave the field at its zero value

A tag also opts a named basic type back in: a Status field tagged fake:"{word}" emits model.Status(gofakeit.Word()).

A template that cannot produce the field's type is refused rather than forced, and the field stays zero. That covers a range too wide for the field ({number:1,300} on an int8) and a mismatched kind ({email} on an int), so the generated file always compiles.

Directives
Directive Effect
//kanna:ignore do not generate a fixture for this struct

Write it against the comment marker. // kanna:ignore is not a directive to Go, so kanna does not treat it as one either, and says so.

Flags
Flag Meaning
-source <pkg> package to scan (relative path or import path)
-destination <dir> directory to write fixture_gen.go into
-package <name> package name for the generated file (default: what the destination declares)
-exclude <names> comma-separated type names to skip
-check verify the output is up to date instead of writing it

The destination has to be a different package from the source; a fixture that imported its own package would not compile.

Example

examples/fixture generates fixtures for a model covering each inference rule, then builds values from them. CI regenerates it and fails if the output would change.

kanna-mapper

Writes the mapping functions between your domain types and the wire types you do not control, calling the converters you registered for the fields Go cannot convert on its own.

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

Two lines, because this generator has a runtime half. The package declaring your converters imports github.com/go-kanna/kanna/mapper; the generated code does not.

Use

Register the conversions Go cannot do by itself, once, anywhere:

package converters

import "github.com/go-kanna/kanna/mapper"

func init() {
	mapper.Register(UUIDToString) // uuid.UUID → string
	mapper.RegisterE(uuid.Parse)  // string → uuid.UUID, and this one can fail
}

Then name the pairs to map:

//go:generate go tool kanna-mapper -types=model.Employee:*employeev1.Employee -converter-pkg=../lib/converters
package mapper

import (
	_ "example.com/app/gen/employeev1"
	_ "example.com/app/model"
)

The blank imports are what make model and employeev1 resolvable as selectors. If the package already imports those types for real use, the directive stands on its own; full import paths work too.

go generate ./... writes the functions:

// EmployeeToEmployeev1 maps model.Employee to *employeev1.Employee.
func EmployeeToEmployeev1(src model.Employee) *employeev1.Employee {
	return &employeev1.Employee{
		Id:      converters.UUIDToString(src.ID),
		Name:    src.Name,
		Address: AddressToEmployeev1(src.Address),
	}
}

// EmployeeFromEmployeev1 maps *employeev1.Employee to model.Employee.
func EmployeeFromEmployeev1(src *employeev1.Employee) (model.Employee, error) {
	if src == nil {
		return model.Employee{}, nil
	}
	v1, err0 := uuid.Parse(src.GetId())
	if err0 != nil {
		return model.Employee{}, fmt.Errorf("map model.Employee.ID: %w", err0)
	}
	// ...
}

The direction that can fail returns an error naming the field that produced it. The direction that cannot, does not. Nil-safe getters are used when the wire type has them.

mapper.Register is never executed by the generator: the calls are read statically, and the functions they name are called directly. The registry also works at run time through mapper.Convert if you want it.

How fields are matched

Each destination field takes the first rule that matches.

Rule Example
the destination field's own tag a destination tagged map:"EmployeeName" reads that field
a source field tagged with the destination's name a source tagged map:"Name" fills Name
the same name NameName
the same name, any case IDId
a promoted field an embedded struct's field, by exact name

A destination field with no source is an error, not a silent zero value. Exclude it with map:"-" on the source, or -ignore TYPE.FIELD when the type is not yours to tag.

How values are converted
Case Result
identical types assigned as-is
a registered converter exists that function is called
the pair is declared in -types the generated function for it is called
source is a pointer dereferenced; nil leaves the destination zero
destination is a pointer the value's address is taken
both are slices converted element-wise; nil maps to nil
a lossless Go conversion exists dst(v)

"Lossless" is meant strictly: int32int64 converts, int64int32 does not. Neither does intint32 (int is 64 bits on some platforms), intuint (sign), int64float64 (precision), or anything → string. Everything else needs a converter, and the error message shows the mapper.Register line that would satisfy it.

Flags
Flag Meaning
-types <SRC:DST> pairs to map, comma-separated; repeatable. * marks a pointer
-converters <pkg> package holding the mapper.Register calls; repeatable
-exclude <TYPE.FIELD> destination fields to exclude; repeatable
-output <path> output directory, or a file path ending in .go
-direction <dir> both (default), to, or from
-package <name> output package name (default: $GOPACKAGE)
-check verify the output is up to date instead of writing it
Example

examples/mapper maps a domain aggregate onto protobuf-shaped wire types and back, covering each way a field can be handled: renamed with map:"Name", excluded from the domain side with map:"-", excluded from the wire side with -exclude where there is no tag to write, converted through a registered function, and converted through one that can fail. CI regenerates it and fails if the output would change.

kanna-orm

Generates type-safe query code from annotated model structs: a factory returning orm.Query[T] per table, row scanning, relations with eager loading, and automatic timestamps. The generated code is plain Go on top of the orm/ runtime, which brings the query builder, MySQL/PostgreSQL dialects, and transactions.

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

//kanna:table opts a struct in; everything else is inferred from the fields and overridden with orm tags where the inference is not what the schema says.

package model

import "time"

//kanna:table
type User struct {
	ID        int       // primary key by name
	Name      string    // column "name"
	Email     string    `orm:"email_address"` // explicit column name
	CreatedAt time.Time // set automatically on create
	Posts     []Post    `orm:"has_many,foreign_key:user_id"`
}

//kanna:table
type Post struct {
	ID     int
	UserID int
	Title  string
	User   *User `orm:"belongs_to,foreign_key:user_id"`
}
Generate
//go:generate go tool kanna-orm -source ./model -destination ./query

The destination must be a package of its own — generating into the model package would leave it uncompilable whenever the output goes stale.

Use
db := orm.New(sqlDB, orm.MySQL) // or orm.PostgreSQL

users, err := query.Users(db).Where("name LIKE ?", "A%").OrderBy("id").All(ctx)
posts, err := query.Posts(db).Preload("User").All(ctx)

err = db.Transaction(ctx, func(tx orm.Querier) error {
	return query.Users(tx).Create(ctx, &model.User{Name: "Alice"})
})
Tags

The first element of an orm tag is either a relation kind or a column name; everything after it is an option.

Tag Meaning
(no tag) column inferred from the field name (CreatedAtcreated_at)
orm:"col_name" explicit column name
orm:"-" not a column
orm:",primary_key" primary key (default: the field named ID)
orm:",created_at" / orm:",updated_at" timestamp managed on create/update (default: fields named CreatedAt/UpdatedAt)
orm:"has_many,foreign_key:user_id" one-to-many; the field is a slice
orm:"has_one,foreign_key:user_id" one-to-one; the target holds the key
orm:"belongs_to,foreign_key:user_id" the owning side; this struct holds the key
orm:"many_to_many,join_table:user_tags,foreign_key:user_id,references:tag_id" via a join table

Table names are pluralized snake_case (UserProfileuser_profiles); //kanna:table name=people overrides one. Name inference is mechanical — there is deliberately no acronym dictionary, so a mixed-case name like OAuthToken takes its column from the tag. Anything malformed — an unknown option, a relation whose target generates no queries, a missing foreign key column — is a positioned error, not a silent skip.

Flags
Flag Description
-source <pkg> source package to scan
-destination <dir> output directory for orm_gen.go
-package <name> generated package name (defaults to what the destination declares)
-check verify the output is up to date instead of writing it
Example

examples/orm drives the generated queries end to end against real MySQL and PostgreSQL: scopes, joins, the three preload kinds, transactions, batch inserts, and upserts. CI regenerates it, runs it against both databases, and fails if the output would change.

kanna-i18n

Generates typed message constructors from a directory of locale files — and compiles the translations themselves into the output, so nothing is parsed or read at run time.

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

Two lines, because this generator has a runtime half: both the generated code and the code calling Localize import github.com/go-kanna/kanna/i18n, which carries the CLDR plural rules, locale-aware number formatting, and language fallback that depend on run-time values.

Use

One file per language, named by its BCP 47 tag:

# locales/en.yaml
greeting: "Hello!"
hello: "Hello, {name}!"
items_count:
  plural:
    one: "You have {count} item."
    other: "You have {count} items."
total_price: "Total: {price:number}"
user:
  not_found: "User not found."
//go:generate go tool kanna-i18n

With the defaults, that one line reads locales/ and writes messages/i18n_gen.go: a constructor per message, a Localizer accessor, and the translations of every language as an embedded bundle.

Discovery is flat: one file defines one language, and subdirectories are never scanned. Nothing locale-shaped is skipped in silence — a subdirectory or unreadable file named like a locale (locales/en/, en.json) fails the run, and other non-locale files are skipped with a warning.

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

// Localizer renders this package's messages in the compiled locale best
// matching tag.
func Localizer(tag language.Tag) i18n.Localizer {
	return bundle.Localizer(tag)
}

// bundle holds every locale this package was generated from.
var bundle = i18n.NewBundle("en",
	i18n.Catalog{Lang: "en", Entries: map[string]i18n.Entry{
		"hello": {Single: i18n.Template{{Text: "Hello, "}, {Param: "name"}, {Text: "!"}}},
		// ...
	}},
)

Calling it takes no setup at all:

en := messages.Localizer(language.English)
fmt.Println(en.Localize(messages.Hello("World")))

The default language (-default, en unless said otherwise) defines the constructor signatures; every other locale is validated against it at generation time. At run time, a message missing from the requested language falls back through the language's parents (en-GB falls back to en) and finally to the default language, and requesting a language that was never compiled in gets the default outright.

Locale files

The locale directory is flat — one file per language, no recursion — and the filename stem is the language: en.yaml, pt-BR.yaml, ja.toml. YAML (.yaml, .yml) and TOML (.toml) both work. Files whose stem is not a language tag are skipped with a warning, so a stray config.yaml does not fail the run.

Nested mappings become dot-joined keys, and keys become constructor names: user.not_found generates UserNotFound(). Keys and parameter names match [a-z][a-z0-9_]* per segment.

A message declares its plural forms under an explicit plural mapping, keyed by CLDR category (zero, one, two, few, many, other) and always defining other. The marker is explicit so intent is never guessed from shape: keys that merely happen to be named one or other are ordinary nesting, and the only name a locale file cannot use freely is a mapping-valued plural. The generated constructor takes count int first, and the count picks the variant under the rendering language's own plural rules — Japanese has no one form, and that is fine.

A plural group that skips forms its language does use gets a warning, because those counts silently render with other: Russian providing only other is missing one, few, and many, while Japanese providing only other is complete.

Placeholders
Form Meaning
{name} a string parameter
{name:int} a plain integer, rendered as-is — counts, IDs
{name:number} a float64, rendered with the locale's conventions: 1,234.56 in en, 1.234,56 in de
{{ and }} literal braces

A bare placeholder inherits an explicit kind annotated elsewhere in the same message; conflicting annotations are an error.

Validation

Errors fail the generation: a key a translation has but the default language does not, a plural group where the default has a plain message (or the reverse), a parameter the default language never mentions, and conflicting kind annotations across plural variants.

Missing translations are warnings, not errors, because the runtime falls back to the default language. The generated code never breaks when a locale lags behind; it renders the default until the translation lands.

Flags
Flag Meaning
-locales <dir> directory containing locale files (default: locales)
-default <lang> default language defining the generated signatures (default: en)
-destination <dir> output directory for the generated file (default: messages)
-package <name> package name of the generated file (default: base of the output directory)
-check verify the output is up to date instead of writing it
Example

examples/i18n renders the same messages in English and Japanese — plurals, locale-formatted numbers, and a fallback for a missing translation — with zero run-time setup. CI regenerates it and fails if the output would change.

Development

make test        # unit tests
make lint        # golangci-lint
make examples    # regenerate every example, then build and run it

The repository is a Go workspace: go.work points the examples' tool directive at this checkout, so go generate inside an example runs the generator you have locally rather than a published version.

License

MIT

Directories

Path Synopsis
cmd
kanna-di command
Command kanna-di generates type-safe dependency-injection constructors from container struct declarations.
Command kanna-di generates type-safe dependency-injection constructors from container struct declarations.
kanna-fixture command
Command kanna-fixture generates plain fixture functions from the structs a source package declares.
Command kanna-fixture generates plain fixture functions from the structs a source package declares.
kanna-i18n command
Command kanna-i18n generates typed message constructors from a directory of locale files, with the translations themselves compiled into the output.
Command kanna-i18n generates typed message constructors from a directory of locale files, with the translations themselves compiled into the output.
kanna-mapper command
Command kanna-mapper generates struct-to-struct mapping functions, wiring in the converters a project registers for the field types Go cannot convert on its own.
Command kanna-mapper generates struct-to-struct mapping functions, wiring in the converters a project registers for the field types Go cannot convert on its own.
kanna-orm command
Command kanna-orm generates type-safe query code from the annotated model structs a source package declares.
Command kanna-orm generates type-safe query code from the annotated model structs a source package declares.
examples
fixture module
Package i18n renders the localized messages that kanna-i18n compiles into generated code.
Package i18n renders the localized messages that kanna-i18n compiles into generated code.
internal
diag
Package diag provides diagnostic types and helpers for reporting issues detected during code generation.
Package diag provides diagnostic types and helpers for reporting issues detected during code generation.
directive
Package directive parses the //kanna:<key> comment directives that the generators share.
Package directive parses the //kanna:<key> comment directives that the generators share.
gen/di
Package di generates dependency-injection constructors from container struct declarations.
Package di generates dependency-injection constructors from container struct declarations.
gen/fixture
Package fixture generates plain constructor functions that fill a struct with fake data, one per struct in a source package.
Package fixture generates plain constructor functions that fill a struct with fake data, one per struct in a source package.
gen/i18n
Package i18n generates a typed message package from a directory of locale files: one constructor per message, validated across locales, with the translations themselves compiled in.
Package i18n generates a typed message package from a directory of locale files: one constructor per message, validated across locales, with the translations themselves compiled in.
gen/i18n/locale
Package locale parses locale files (YAML or TOML) into flat message catalogs.
Package locale parses locale files (YAML or TOML) into flat message catalogs.
gen/i18n/template
Package template parses message templates such as "Hello, {name}!".
Package template parses message templates such as "Hello, {name}!".
gen/mapper
Package mapper generates mapping functions between two struct types, wiring in the converters a project registers for the field types Go cannot convert on its own.
Package mapper generates mapping functions between two struct types, wiring in the converters a project registers for the field types Go cannot convert on its own.
gen/orm
Package orm generates type-safe query code from annotated model structs.
Package orm generates type-safe query code from annotated model structs.
imports
Package imports assigns the local names a generated file uses for the packages it imports.
Package imports assigns the local names a generated file uses for the packages it imports.
ir
Package ir defines the generator-agnostic model that the scan layer produces and each generator consumes.
Package ir defines the generator-agnostic model that the scan layer produces and each generator consumes.
output
Package output writes what a generator produced, with the guards every generator needs and none should reimplement: never replace a file that is not generated, tell staleness apart from I/O failure, and resolve paths against the directory the tool was invoked in.
Package output writes what a generator produced, with the guards every generator needs and none should reimplement: never replace a file that is not generated, tell staleness apart from I/O failure, and resolve paths against the directory the tool was invoked in.
packages
Package packages wraps golang.org/x/tools/go/packages with the loading mode and build-tag handling used by kanna.
Package packages wraps golang.org/x/tools/go/packages with the loading mode and build-tag handling used by kanna.
pkgtest
Package pkgtest provides helpers for tests that need a loaded package.
Package pkgtest provides helpers for tests that need a loaded package.
scan
Package scan extracts a generator-agnostic model of the struct types declared in a set of loaded packages.
Package scan extracts a generator-agnostic model of the struct types declared in a set of loaded packages.
version
Package version reports the version of the running binary.
Package version reports the version of the running binary.
Package mapper declares the type converters that kanna-mapper wires into generated code.
Package mapper declares the type converters that kanna-mapper wires into generated code.
orm
Package orm is the runtime for code generated by kanna-orm: the Query builder that generated factory functions return, the MySQL and PostgreSQL dialects that shape its SQL, and the transaction, logging, and clock plumbing around it.
Package orm is the runtime for code generated by kanna-orm: the Query builder that generated factory functions return, the MySQL and PostgreSQL dialects that shape its SQL, and the transaction, logging, and clock plumbing around it.
scope
Package scope provides composable, reusable query fragments that Query.Scopes applies to a query.
Package scope provides composable, reusable query fragments that Query.Scopes applies to a query.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL