blob: 77813e476c906f4c9f34417a780046630d04e971 [file] [log] [blame]
// Program makeset generates source code for a set package. The type of the
// elements of the set is determined by a TOML configuration stored in a file
// named by the -config flag.
//
// Usage:
// go run makeset.go -output $DIR -config config.toml
//
package main
//go:generate go run github.com/creachadair/staticfile/compiledata -pkg main -out static.go *.go.in
import (
"bytes"
"errors"
"flag"
"fmt"
"go/format"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"text/template"
"github.com/BurntSushi/toml"
"github.com/creachadair/staticfile"
)
// A Config describes the nature of the set to be constructed.
type Config struct {
// A human-readable description of the set this config defines.
// This is ignored by the code generator, but may serve as documentation.
Desc string
// The name of the resulting set package, e.g., "intset" (required).
Package string
// The name of the type contained in the set, e.g., "int" (required).
Type string
// The spelling of the zero value for the set type, e.g., "0" (required).
Zero string
// If set, a type definition is added to the package mapping Type to this
// structure, e.g., "struct { ... }". You may prefix Decl with "=" to
// generate a type alias (this requires Go ≥ 1.9).
Decl string
// If set, the body of a function with signature func(x, y Type) bool
// reporting whether x is less than y.
//
// For example:
// if x[0] == y[0] {
// return x[1] < y[1]
// }
// return x[0] < y[0]
Less string
// If set, the body of a function with signature func(x Type) string that
// converts x to a human-readable string.
//
// For example:
// return strconv.Itoa(x)
ToString string
// If set, additional packages to import in the generated code.
Imports []string
// If set, additional packages to import in the test.
TestImports []string
// If true, include transformations, e.g., Map, Partition, Each.
Transforms bool
// A list of exactly ten ordered test values used for the construction of
// unit tests. If omitted, unit tests are not generated.
TestValues []interface{} `json:"testValues,omitempty"`
}
func (c *Config) validate() error {
if c.Package == "" {
return errors.New("invalid: missing package name")
} else if c.Type == "" {
return errors.New("invalid: missing type name")
} else if c.Zero == "" {
return errors.New("invalid: missing zero value")
}
return nil
}
var (
configPath = flag.String("config", "", "Path of configuration file (required)")
outDir = flag.String("output", "", "Output directory path (required)")
baseImports = []string{"reflect", "sort", "strings"}
)
func main() {
flag.Parse()
switch {
case *outDir == "":
log.Fatal("You must specify a non-empty -output directory")
case *configPath == "":
log.Fatal("You must specify a non-empty -config path")
}
conf, err := readConfig(*configPath)
if err != nil {
log.Fatalf("Error loading configuration: %v", err)
}
if len(conf.TestValues) > 0 && len(conf.TestValues) != 10 {
log.Fatalf("Wrong number of test values (%d); exactly 10 are required", len(conf.TestValues))
}
if err := os.MkdirAll(*outDir, 0755); err != nil {
log.Fatalf("Unable to create output directory: %v", err)
}
mainT, err := template.New("main").Parse(string(staticfile.MustReadAll("core.go.in")))
if err != nil {
log.Fatalf("Invalid main source template: %v", err)
}
testT, err := template.New("test").Parse(string(staticfile.MustReadAll("core_test.go.in")))
if err != nil {
log.Fatalf("Invalid test source template: %v", err)
}
mainPath := filepath.Join(*outDir, conf.Package+".go")
if err := generate(mainT, conf, mainPath); err != nil {
log.Fatal(err)
}
if len(conf.TestValues) != 0 {
testPath := filepath.Join(*outDir, conf.Package+"_test.go")
if err := generate(testT, conf, testPath); err != nil {
log.Fatal(err)
}
}
}
// readConfig loads a configuration from the specified path and reports whether
// it is valid.
func readConfig(path string) (*Config, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
var c Config
if err := toml.Unmarshal(data, &c); err != nil {
return nil, err
}
// Deduplicate the import list, including all those specified by the
// configuration as well as those needed by the static code.
imps := make(map[string]bool)
for _, pkg := range baseImports {
imps[pkg] = true
}
for _, pkg := range c.Imports {
imps[pkg] = true
}
if c.ToString == "" {
imps["fmt"] = true // for fmt.Sprint
}
c.Imports = make([]string, 0, len(imps))
for pkg := range imps {
c.Imports = append(c.Imports, pkg)
}
sort.Strings(c.Imports)
return &c, c.validate()
}
// generate renders source text from t using the values in c, formats the
// output as Go source, and writes the result to path.
func generate(t *template.Template, c *Config, path string) error {
var buf bytes.Buffer
if err := t.Execute(&buf, c); err != nil {
return fmt.Errorf("generating source for %q: %v", path, err)
}
src, err := format.Source(buf.Bytes())
if err != nil {
return fmt.Errorf("formatting source for %q: %v", path, err)
}
return ioutil.WriteFile(path, src, 0644)
}