183 lines
5.0 KiB
Go
183 lines
5.0 KiB
Go
|
// 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 -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.MustReadFile("core.go.in")))
|
||
|
if err != nil {
|
||
|
log.Fatalf("Invalid main source template: %v", err)
|
||
|
}
|
||
|
testT, err := template.New("test").Parse(string(staticfile.MustReadFile("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)
|
||
|
}
|