| // 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) |
| } |