tommy

module
v0.2.8 Latest Latest
Warning

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

Go to latest
Published: May 24, 2026 License: MIT

README

Tommy

A TOML library for Go that preserves comments, formatting, and whitespace on round-trip.

Most TOML libraries parse into a map or struct and discard everything else. Tommy keeps a concrete syntax tree (CST) so that reading a config, modifying a value, and writing it back produces minimal diffs --- comments stay where they were, blank lines don't move, and whitespace around = signs is untouched.

Features

  • Round-trip fidelity --- parse and serialize without losing comments or formatting
  • Code generator --- annotate a struct with //go:generate tommy generate and get type-safe Decode/Encode methods with undecoded-key detection
  • Reflection-based marshal --- UnmarshalDocument/MarshalDocument for quick use without codegen
  • Document API --- read and write values by dotted key path while preserving the surrounding document
  • Formatter --- tommy fmt normalizes whitespace, comment spacing, and blank lines

Install

go install github.com/amarbel-llc/tommy/cmd/tommy@latest

Or with Nix:

nix build github:amarbel-llc/tommy

How It Works

Tommy parses TOML into a concrete syntax tree where comments, whitespace, and blank lines are first-class nodes --- not discarded during parsing. When you modify a value through any of Tommy's APIs, only the value node in the tree is updated. Everything else --- comments above, inline comments after values, blank line separators between sections --- stays exactly where it was.

input.toml                          after Encode()
─────────────                       ──────────────
# Server configuration              # Server configuration
[server]                            [server]
port = 8080 # default port          port = 9090 # default port
host = "localhost"                  host = "localhost"

This is automatic. There is no flag to enable or annotation to add --- every path through Tommy (codegen, reflection marshal, document API) preserves the full document structure. The only thing that changes in the output is the value you changed.

The recommended way to use Tommy is through its code generator. Add a //go:generate tommy generate directive above your struct and you get type-safe Decode/Encode methods that handle all of this transparently --- you just read and write normal Go struct fields. No CST manipulation, no special APIs, no awareness of the preservation machinery needed.

The code generator is not yet fully type-exhaustive --- support for additional Go type patterns is being added as needed. See the open issues for what's planned and in progress.

This matters for config files that humans maintain: version-controlled TOML with explanatory comments, hand-tuned formatting, or sections separated by blank lines. A programmatic update to one field should not rewrite the entire file.

Quick Start

Annotate your struct and run go generate:

//go:generate tommy generate
type Config struct {
    Title   string `toml:"title"`
    Port    int    `toml:"port"`
    Debug   bool   `toml:"debug,omitempty"`
}

Two things to get right:

  • Directive placement. Put //go:generate tommy generate immediately above each struct you want codegen for. A package-level directive will exit with no structs with //go:generate tommy generate found.
  • Tag name. Tommy reads the standard Go toml:"..." tag, not a tommy: tag. Fields without a toml tag are silently skipped.

This produces a config_tommy.go file with:

func DecodeConfig(input []byte) (*ConfigDocument, error)
func (d *ConfigDocument) Data() *Config
func (d *ConfigDocument) Encode() ([]byte, error)
func (d *ConfigDocument) Undecoded() []string

Usage:

doc, err := DecodeConfig(input)
cfg := doc.Data()
cfg.Port = 9090
output, err := doc.Encode()
// output preserves all comments and formatting from input
Reflection-based Marshal

For simpler cases without code generation:

import "github.com/amarbel-llc/tommy/pkg/marshal"

var cfg Config
handle, err := marshal.UnmarshalDocument(input, &cfg)

cfg.Port = 9090
output, err := marshal.MarshalDocument(handle, &cfg)
Document API

For direct key-value manipulation:

import "github.com/amarbel-llc/tommy/pkg/document"

doc, err := document.Parse(input)
port, err := document.Get[int](doc, "server.port")
err = doc.Set("server.port", 9090)
output := doc.Bytes()

Supported Field Types

The code generator handles:

Type TOML Representation


string, int, int64, float64, bool Scalar values *string, *int, *bool, etc. Optional scalars Nested structs [table] sections *Struct [table] or flat dotted keys Cross-package structs [table] via delegation []int, []string Arrays []Struct [[array-of-tables]] map[string]string [table] with string values map[string]Struct Sub-tables ([parent.key]) TOMLMarshaler/TOMLUnmarshaler Custom marshal via any TextMarshaler/TextUnmarshaler Custom marshal via string

Cross-Package Structs

When a struct field references a type from another package, the generated code delegates to that package's DecodeInto/EncodeFrom functions instead of inlining field-by-field decoding. This means the external package must also use //go:generate tommy generate on its structs.

// In package "options":
//go:generate tommy generate
type PrintOptions struct {
    Abbreviations *abbreviations `toml:"abbreviations"`
    PrintColors   *bool          `toml:"print-colors"`
}

// In your package:
//go:generate tommy generate
type Config struct {
    Name         string               `toml:"name"`
    PrintOptions options.PrintOptions `toml:"cli-output"`
}

This delegation enables cross-package structs that contain unexported types --- the external package handles its own internals, and the consumer just delegates.

Validation

If your struct implements Validate() error, the generated Decode and Encode methods call it automatically. Decode validates after all fields are set; Encode validates before writing to the CST.

//go:generate tommy generate
type Config struct {
    Port int    `toml:"port"`
    Name string `toml:"name"`
}

func (c Config) Validate() error {
    if c.Port < 1 || c.Port > 65535 {
        return fmt.Errorf("port must be 1-65535, got %d", c.Port)
    }
    return nil
}

No interface import is required --- just add the method and re-run go generate. This also works with newtypes to validate individual values while preserving their native TOML types (no string coercion).

Struct Tag Options
`toml:"key"`                // required — maps field to TOML key
`toml:"key,omitempty"`      // omit zero-value fields on encode
`toml:"key,multiline"`      // use """ multiline string syntax
`toml:"-"`                  // skip this field
Unsupported Field Types

The following will fail at tommy generate time:

  • interface{} / any --- no static type to dispatch on
  • map[string]any, map[string]interface{} --- same reason
  • Anonymous structs in field types --- name the struct and reference it
  • Channels, functions --- not representable in TOML

For untyped or schema-free TOML, use the lower-level pkg/document or pkg/marshal APIs instead of codegen.

Common Errors
  • no structs with //go:generate tommy generate found in <file> --- the directive is on the package or a file-level comment. Move it to immediately above each struct.
  • struct any not found in package --- a field has type any/ interface{} (often via map[string]any). See Unsupported Field Types above.
  • field <Name>.<Field>: ... --- codegen couldn't classify the field. Check its Go type against Supported Field Types; the man page tommy-struct-tags(7) enumerates every kind tommy recognizes.

See Also

  • tommy(1) --- top-level CLI
  • tommy-fmt(1) --- formatter subcommand
  • tommy-generate(1) --- codegen subcommand
  • tommy-struct-tags(7) --- complete struct-tag and field-kind reference

CLI

# Format TOML files in-place
tommy fmt config.toml settings.toml

# Check formatting without modifying (exits non-zero if unformatted)
tommy fmt --check config.toml

# Format from stdin
cat config.toml | tommy fmt -

# Generate code (typically via go generate, not called directly)
tommy generate

License

MIT

Directories

Path Synopsis
cmd
tommy command
internal
pkg
cst

Jump to

Keyboard shortcuts

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