printsrc

package module
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Dec 13, 2021 License: MIT Imports: 10 Imported by: 4

README

printsrc: Printing Go Values as Source

There are many packages that print Go values so people can read them. This package prints Go values so the Go compiler can read them. It is intended for use in code generators.

Background

I wanted to provide some data to my program at startup. I could have used go:embed to store the raw data with the program and process it at startup, but I wanted to pay the processing cost beforehand and generate Go data structures into a .go file that could be linked with the rest of my code.

So I need something that printed Go values as Go source. I looked around at the many pretty-printing packages out there:

  • github.com/davecgh/go-spew/spew
  • github.com/k0kubun/pp/v3
  • github.com/kr/pretty
  • github.com/kylelemons/godebug/pretty
  • github.com/sanity-io/litter

and more. They do a great job of formatting Go values for people to read. But I couldn't find one that correctly prints values as Go source. So I wrote this package.

Issues with Printing Source

Here are a few challenges that come up when trying to print Go values in a way that the compiler can understand.

Special floating-point values

Consider the floating-point value for positive infinity. There is no Go literal for this value, but it can be obtained with math.Inf(1). Calling fmt.Sprintf("%#v", math.Inf(1)) returns +Inf, which is not valid Go.

The printsrc package prints a float64 positive infinity as math.Inf(1) and a float32 positive infinity as float32(math.Inf(1)). It handles negative infinity and NaN similarly.

Values that cannot be represented

Function and channel values cannot be written as source using information available from the reflect package. Pretty-printers do their best to render these values, as they should, but printsrc fails on them so you can discover the problem quickly.

Pointers

When faced with a pointer, printers either print the address (like Go's fmt package) or follow the pointer and print the value. Neither of those, when fed back into Go, will produce the right value. Given

i := 5
s := []*int{&i, &i}

this package will print s as

[]*int{
    func() *int { var x int = 5; return &x }(),
    func() *int { var x int = 5; return &x }(),
}

That is a valid Go expression, although it doesn't preserve the sharing relationship of the original. For simplicity, printsrc doesn't detect sharing, and fails on cycles.

Types from other packages

Say your data structure contains a time.Duration. Depending on where it occurs, such values may have to be rendered with their type, like time.Duration(100). But that code won't compile unless the time package has been imported (and imported under the name "time"). Types in the package for where the generated code lives don't have that problem; they can be generated without a qualifying package identifier.

printsrc assumes that packages it encounters have been imported using the identifier that is the last component of their import path. Most of the time that is correct, but when it isn't you can register a different identifier with an import path.

Values that need constructors

The time.Time type has unexported fields, so it can't be usably printed as a Go struct literal (unless the code is being generated in the time package itself). There are many other types that need to be constructed with a function call or in some other way. Since printsrc can't discover the constructors for these types on its own, it lets you provide custom printing functions for any type. The one for time.Time is built in and prints a call to time.Date. (You can override it if you want.)

Documentation

Overview

Package printsrc prints Go values as Go source. It strives to render legal Go source code, and returns an error when detects that it cannot.

To generate code for a slice of Points in package "geo":

package main

import "my/packages/geo"

func main() {
    p := NewPrinter("my/packages/geo")
    data := []geo.Point{{1, 2}, {3, 4}}
    if err := p.Fprint(out, data); err != nil {
        log.Fatal(err)
    }
}

Registering Import Path Identifiers

To print the names of a type in another package, printsrc needs to know how to refer to the package. Usually, but not always, the package identifier is the last component of the package path. For example, the types in the standard library package "database/sql" are normally prefixed by "sql". But this rule doesn't hold for all packages. The actual identifier is the package name as declared in its files' package clause, which need not be the same as the last component of the import path. (A common case: import paths ending in "/v2", "/v3" and so on.) Also, an import statement can specify a different identifier. Since printsrc can't know about these special cases, you must call Printer.RegisterImport to tell it the identifier to use for a given import path.

Registering Custom Printers

Sometimes there is no way for printsrc to discover how to print a value as valid Go source code. For example, the math/big.Int type is a struct with no exported fields, so a big.Int cannot be printed as a struct literal. (A big.Int can be constructed with the NewInt function or the SetString method.)

Use Printer.RegisterPrinter to associate a type with a function that returns source code for a value of that type.

A custom printer for time.Time is registered by default. It prints a time.Time by printing a call to time.Date. An error is returned if the time's location is not Local or UTC, since those are the only locations for which source expressions can be produced.

Registering Less Functions

This package makes an effort to sort map keys in order to generate deterministic output. But if it can't sort the keys it prints the map anyway. The output will be valid Go but the order of the keys will change from run to run. That creates noise in code review diffs.

Use Printer.RegisterLess to register a function that compares two values of a type. It will be called to sort map keys of that type.

Type Elision

This package elides the types of composite literals when it can. For example, the value

[]Point{Point{x: 1, y: 2}}

will print in its simplified form,

[]Point{{x: 1, y: 2}}

Known Issues

Maps with multiple NaN keys are not handled.

The reflect package provides no way to distinguish a type defined inside a function from one at top level. So printsrc will print expressions containing names for those types which will not compile.

Sharing relationships are not preserved. For example, if two pointers in the input point to the same value, they will point to different values in the output.

Unexported fields of structs defined outside the generated package are ignored, because there is no way to set them (without using unsafe code). So important state may fail to be printed. As a safety feature, printsrc fails if it is asked to print a non-zero struct from outside the generated package with no exported fields. You must register custom printers for such structs. But that won't catch problems with types that have at least one exported field.

Cycles are detected by the crude heuristic of limiting recursion depth. Cycles cause printsrc to fail. A more sophisticated approach would represent cyclical values using intermediate variables, but it doesn't seem worth it.

Example
//go:build go1.16
// +build go1.16

package main

import (
	"fmt"
	"go/format"
	"log"

	"github.com/jba/printsrc"
)

type Student struct {
	Name    string
	ID      int
	GPA     float64
	Classes []Class
}

type Class struct {
	Name string
	Room string
}

func main() {
	s := Student{
		Name: "Pat A. Gonia",
		ID:   17,
		GPA:  3.8,
		Classes: []Class{
			{"Geometry", "3.14"},
			{"Dance", "123123"},
		},
	}
	p := printsrc.NewPrinter("github.com/jba/printsrc_test")
	str, err := p.Sprint(s)
	if err != nil {
		log.Fatal(err)
	}
	src, err := format.Source([]byte(str))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", src)

}
Output:

Student{
	Name: "Pat A. Gonia",
	ID:   17,
	GPA:  3.8,
	Classes: []Class{
		{
			Name: "Geometry",
			Room: "3.14",
		},
		{
			Name: "Dance",
			Room: "123123",
		},
	},
}
Example (Template)

This example shows how to use printsrc along with the text/template package to generate a file of Go code.

package main

import (
	"bytes"
	"database/sql"
	"fmt"
	"go/format"
	"log"
	"text/template"
	"time"

	"github.com/jba/printsrc"
)

func main() {
	p := printsrc.NewPrinter("github.com/jba/printsrc_test")

	data := struct {
		Start   time.Time
		Strings []sql.NullString
	}{
		Start: time.Date(2021, 12, 21, 14, 32, 11, 00, time.Local),
		Strings: []sql.NullString{
			{String: "ok", Valid: true},
			{String: "", Valid: false},
		},
	}

	tmpl := template.Must(template.New("").
		Funcs(template.FuncMap{"src": p.Sprint}).
		Parse(`
		// Code generated by example_template_test.go. DO NOT EDIT.

		package example

        import (
	      "database/sql"
	      "time"
		)

		var startTime = {{.Start | src}}

		var strings = {{.Strings | src}}
	`))

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		log.Fatal(err)
	}
	src, err := format.Source(buf.Bytes())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s", src)

}
Output:

// Code generated by example_template_test.go. DO NOT EDIT.

package example

import (
	"database/sql"
	"time"
)

var startTime = time.Date(2021, time.December, 21, 14, 32, 11, 0, time.Local)

var strings = []sql.NullString{
	{
		String: "ok",
		Valid:  true,
	},
	{},
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Printer

type Printer struct {
	// contains filtered or unexported fields
}

A Printer prints Go values as source code.

func NewPrinter

func NewPrinter(packagePath string) *Printer

NewPrinter constructs a Printer. The argument is the import path of the package where the printed code will reside.

A custom printer for time.Time is registered by default. To override it or to add custom printers for other types, call RegisterPrinter.

func (*Printer) Fprint

func (p *Printer) Fprint(w io.Writer, value interface{}) error

Fprint prints a valid Go expression for value to w.

func (*Printer) PackageIdentifier

func (p *Printer) PackageIdentifier(pkgPath string) string

PackageIdentifier returns the identifier that should prefix type names from the given import path. It returns the empty string if pkgPath is the same as the path given to NewPrinter. Otherwise, if an identifier has been provided with RegisterImport, it uses that. Finally, it returns the last component of the import path.

func (*Printer) RegisterImport

func (p *Printer) RegisterImport(packagePath, ident string)

RegisterImport tells the Printer to use the given identifier when printing types imported from packagePath.

func (*Printer) RegisterLess

func (p *Printer) RegisterLess(lessFunc interface{})

RegisterLess associates a function with a type that will be used to sort map keys of that type.

When rendering a map value as Go source, printsrc will sort the keys if it can figure out how. By default it can sort any type whose underlying type is numeric, string or bool. Maps with other key types will have their keys printed in random order, complicating diffs. If a less function is registered for a key type, however, then map keys of that type will be sorted.

The provided lessFunc must be a function of two arguments, both of the same type, and a single bool return value. It should report whether its first argument is less than its second. When confronted with a map whose key type is the function's argument type, printsrc will use the registered function to sort the keys.

RegisterLess panics if the function signature is invalid.

RegisterLess can be used to override the built-in less functions.

func (*Printer) RegisterPrinter

func (p *Printer) RegisterPrinter(printFunc interface{})

RegisterPrinter associates a type with a function that renders values of that type as Go source. An existing function for the type is replaced.

The printFunc argument must be a function of one argument. Values with the type of that argument will be rendered with the function instead of in the usual way. The function's return type must be string or (string, error). RegisterPrinter panics if the function signature is invalid.

func (*Printer) Sprint

func (p *Printer) Sprint(value interface{}) (string, error)

Sprint returns a string that is a valid Go expression for value. See the template example for how to use Sprint with text/template to generate code.

Jump to

Keyboard shortcuts

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