├── test.bat ├── test.sh ├── _gen.go ├── .gitignore ├── pkg.go ├── imports.go ├── benchmarks ├── dummy_obj.go ├── dummy_obj_test.go └── dummyobject_slice.go ├── help_test.go ├── go.mod ├── config.go ├── get_test.go ├── add_test.go ├── list.go ├── list_test.go ├── help.go ├── add.go ├── get.go ├── watch.go ├── run_test.go ├── README.md ├── main_test.go ├── main.go ├── go.sum ├── run.go ├── execute.go ├── LICENSE ├── TYPEWRITERS.md └── CHANGELOG.md /test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | go get 3 | go test -coverprofile=coverage.out 4 | go tool cover -html=coverage.out 5 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | go get 2 | touch coverage.out 3 | go test -coverprofile=coverage.out 4 | go tool cover -html=coverage.out 5 | -------------------------------------------------------------------------------- /_gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/clipperhouse/slice" 5 | _ "github.com/clipperhouse/setwriter" 6 | ) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | install.sh 2 | watch.sh 3 | coverage.out 4 | *_bar.go 5 | *_bar_test.go 6 | *_foo.go 7 | *_foo_test.go 8 | 9 | _site/* 10 | -------------------------------------------------------------------------------- /pkg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/clipperhouse/typewriter" 4 | 5 | type pkg struct { 6 | Name string 7 | Imports typewriter.ImportSpecSet 8 | } 9 | -------------------------------------------------------------------------------- /imports.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // represents the default "built-in" typewriters 4 | import _ "github.com/clipperhouse/slice" 5 | import _ "github.com/clipperhouse/stringer" 6 | -------------------------------------------------------------------------------- /benchmarks/dummy_obj.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | // +gen * slice:"Any,Select[*dummyDestinationSelectObject],SortBy" 4 | type dummyObject struct { 5 | Name string 6 | Num int 7 | } 8 | 9 | type dummyDestinationSelectObject struct { 10 | Name string 11 | } 12 | -------------------------------------------------------------------------------- /help_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestHelp(t *testing.T) { 9 | // use buffer instead of Stdout so help doesn't write to console 10 | var b bytes.Buffer 11 | c := defaultConfig 12 | c.out = &b 13 | 14 | if err := help(c); err != nil { 15 | t.Error(err) 16 | } 17 | 18 | if b.Len() == 0 { 19 | t.Errorf("help() should have resulted in output") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/clipperhouse/gen 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/clipperhouse/slice v0.0.0-20200107170738-a74fc3888fd9 7 | github.com/clipperhouse/stringer v0.0.0-20200107165315-e8ef8175ba3b 8 | github.com/clipperhouse/typewriter v0.0.0-20200107164453-d21420026310 9 | github.com/fsnotify/fsnotify v1.4.7 10 | golang.org/x/sys v0.0.0-20200107162124-548cf772de50 // indirect 11 | golang.org/x/tools v0.0.0-20200110213125-a7a6caa82ab2 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/clipperhouse/typewriter" 8 | ) 9 | 10 | type config struct { 11 | out io.Writer 12 | customName string 13 | *typewriter.Config 14 | } 15 | 16 | var defaultConfig = config{ 17 | out: os.Stdout, 18 | customName: "_gen.go", 19 | Config: &typewriter.Config{}, 20 | } 21 | 22 | // keep in sync with imports.go 23 | var stdImports = typewriter.NewImportSpecSet( 24 | typewriter.ImportSpec{Name: "_", Path: "github.com/clipperhouse/slice"}, 25 | typewriter.ImportSpec{Name: "_", Path: "github.com/clipperhouse/stringer"}, 26 | ) 27 | -------------------------------------------------------------------------------- /get_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestGet(t *testing.T) { 9 | // use custom name so test won't interfere with a real _gen.go 10 | c := defaultConfig 11 | c.customName = "_gen_get_test.go" 12 | 13 | // clean up when done 14 | defer os.Remove(c.customName) 15 | 16 | // standard 17 | imps, err := getTypewriterImports(c) 18 | 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | if len(imps) != 2 { 24 | t.Errorf("should return 2 imports, got %v", len(imps)) 25 | } 26 | 27 | if err := add(c, "github.com/clipperhouse/foowriter", "github.com/clipperhouse/setwriter"); err != nil { 28 | t.Error(err) 29 | } 30 | 31 | // custom file now exists 32 | imps2, err := getTypewriterImports(c) 33 | 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | 38 | if len(imps2) != 4 { 39 | t.Errorf("should return 4 custom imports, got %v", len(imps2)) 40 | } 41 | 42 | // custom get 43 | if err := get(c); err != nil { 44 | t.Error(err) 45 | } 46 | 47 | // custom get with param 48 | if err := get(c, "-d"); err != nil { 49 | t.Error(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /add_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/clipperhouse/typewriter" 8 | ) 9 | 10 | func TestAdd(t *testing.T) { 11 | // use custom name so test won't interfere with a real _gen.go 12 | c := defaultConfig 13 | c.customName = "_gen_add_test.go" 14 | defer os.Remove(c.customName) 15 | 16 | if err := add(c); err == nil { 17 | t.Error("add with no arguments should be an error") 18 | } 19 | 20 | foo := typewriter.ImportSpec{Name: "_", Path: "github.com/clipperhouse/foowriter"} 21 | before, err := getTypewriterImports(c) 22 | 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | // ensure that the custom import is not in the default set 28 | if before.Contains(foo) { 29 | t.Errorf("default imports should not include %s", foo.Path) 30 | } 31 | 32 | // adding import which exists should succeed 33 | if err := add(c, foo.Path); err != nil { 34 | t.Error(err) 35 | } 36 | 37 | after, err := getTypewriterImports(c) 38 | 39 | // the new import should be reflected in imports 40 | if !after.Contains(foo) { 41 | t.Errorf("imports should include %s", foo.Path) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "text/template" 6 | 7 | "github.com/clipperhouse/typewriter" 8 | ) 9 | 10 | func list(c config) error { 11 | imports := typewriter.NewImportSpecSet( 12 | typewriter.ImportSpec{Path: "fmt"}, 13 | typewriter.ImportSpec{Path: "os"}, 14 | typewriter.ImportSpec{Path: "github.com/clipperhouse/typewriter"}, 15 | ) 16 | 17 | listFunc := func(c config) error { 18 | app, err := typewriter.NewApp("+gen") 19 | 20 | if err != nil { 21 | return err 22 | } 23 | 24 | fmt.Fprintln(c.out, "Installed typewriters:") 25 | for _, tw := range app.TypeWriters { 26 | fmt.Fprintf(c.out, " %s\n", tw.Name()) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | return execute(listFunc, c, imports, listTmpl) 33 | } 34 | 35 | var listTmpl = template.Must(template.New("list").Parse(` 36 | func main() { 37 | app, err := typewriter.NewApp("+gen") 38 | 39 | if err != nil { 40 | os.Stderr.WriteString(err.Error() + "\n") 41 | os.Exit(1) 42 | } 43 | 44 | fmt.Println("Imported typewriters:") 45 | for _, tw := range app.TypeWriters { 46 | fmt.Println(" " + tw.Name()) 47 | } 48 | } 49 | `)) 50 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestList(t *testing.T) { 10 | // use buffer instead of Stdout so we can inspect the results 11 | var b bytes.Buffer 12 | c := defaultConfig 13 | c.out = &b 14 | c.customName = "_gen_list_test.go" 15 | 16 | // clean up when done 17 | defer os.Remove(c.customName) 18 | 19 | // standard 20 | if err := list(c); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | // 1 line for title + 2 standard typewriters (see imports.go) 25 | if lines := bytes.Count(b.Bytes(), []byte("\n")); lines != 3 { 26 | t.Errorf("standard list should output 2 lines, got %v", lines) 27 | } 28 | 29 | // clear out the buffer 30 | b.Reset() 31 | 32 | // create a custom typewriter import file 33 | if err := add(c, "github.com/clipperhouse/foowriter"); err != nil { 34 | t.Error(err) 35 | } 36 | 37 | // custom file now exists 38 | if err := list(c); err != nil { 39 | t.Error(err) 40 | } 41 | 42 | // 1 line for title + 2 custom typewriters 43 | if lines := bytes.Count(b.Bytes(), []byte("\n")); lines != 4 { 44 | t.Errorf("standard list should output 4 lines, got %v:\n%s", lines, b.String()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | func help(c config) error { 11 | cmd := filepath.Base(os.Args[0]) 12 | spacer := strings.Repeat(" ", len(cmd)) 13 | 14 | info := helpInfo{ 15 | Name: cmd, 16 | CustomName: c.customName, 17 | Spacer: spacer, 18 | } 19 | 20 | if err := helpTmpl.Execute(c.out, info); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | 27 | type helpInfo struct { 28 | Name, CustomName, Spacer string 29 | } 30 | 31 | var helpTmpl = template.Must(template.New("help").Parse(` 32 | Usage: 33 | {{.Name}} Generate files for types marked with +{{.Name}}. 34 | {{.Name}} list List available typewriters. 35 | {{.Name}} add Add a third-party typewriter to the current package. 36 | {{.Name}} get Download and install imported typewriters. 37 | {{.Spacer}} Optional flags from go get: [-d] [-fix] [-t] [-u]. 38 | {{.Name}} watch Watch the current directory for file changes, run {{.Name}} 39 | {{.Spacer}} when detected. 40 | {{.Name}} help Print usage. 41 | 42 | Further details are available at http://clipperhouse.github.io/gen 43 | 44 | `)) 45 | -------------------------------------------------------------------------------- /add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/clipperhouse/typewriter" 9 | ) 10 | 11 | // add adds a new typewriter import to the current package, by creating (or appending) a _gen.go file. 12 | func add(c config, args ...string) error { 13 | if len(args) == 0 { 14 | return fmt.Errorf("please specify the import path of the typewriter you wish to add") 15 | } 16 | 17 | imports, err := getTypewriterImports(c) 18 | 19 | if err != nil { 20 | return err 21 | } 22 | 23 | for _, arg := range args { 24 | imp := typewriter.ImportSpec{Name: "_", Path: arg} 25 | 26 | // try to go get it 27 | cmd := exec.Command("go", "get", imp.Path) 28 | cmd.Stdout = c.out 29 | 30 | if err := cmd.Run(); err != nil { 31 | return err 32 | } 33 | 34 | imports.Add(imp) 35 | } 36 | 37 | if createCustomFile(c, imports); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func createCustomFile(c config, imports typewriter.ImportSpecSet) error { 45 | w, err := os.Create(c.customName) 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | defer w.Close() 52 | 53 | p := pkg{ 54 | Name: "main", 55 | Imports: imports, 56 | } 57 | 58 | if err := tmpl.Execute(w, p); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/parser" 5 | "go/token" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/clipperhouse/typewriter" 11 | ) 12 | 13 | // get runs `go get` for required typewriters, either default or specified in _gen.go 14 | func get(c config, args ...string) error { 15 | imports, err := getTypewriterImports(c) 16 | 17 | if err != nil { 18 | return err 19 | } 20 | 21 | // we just want the paths 22 | var imps []string 23 | for imp := range imports { 24 | imps = append(imps, imp.Path) 25 | } 26 | 27 | get := []string{"get"} 28 | get = append(get, args...) 29 | get = append(get, imps...) 30 | 31 | cmd := exec.Command("go", get...) 32 | cmd.Stdout = c.out 33 | cmd.Stderr = c.out 34 | 35 | if err := cmd.Run(); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func getTypewriterImports(c config) (typewriter.ImportSpecSet, error) { 43 | imports := typewriter.NewImportSpecSet() 44 | 45 | // check for existence of custom file 46 | if src, err := os.Open(c.customName); err == nil { 47 | defer src.Close() 48 | 49 | // custom file exists, parse its imports 50 | fset := token.NewFileSet() 51 | f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly) 52 | if err != nil { 53 | return imports, err 54 | } 55 | 56 | // convert ast imports into ImportSpecs 57 | for _, v := range f.Imports { 58 | imp := typewriter.ImportSpec{ 59 | Name: v.Name.Name, 60 | Path: strings.Trim(v.Path.Value, `"`), // lose the quotes 61 | } 62 | imports.Add(imp) 63 | } 64 | } else { 65 | // doesn't exist, use standard (clone it) 66 | imports = stdImports.Clone() 67 | } 68 | 69 | return imports, nil 70 | } 71 | -------------------------------------------------------------------------------- /watch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fsnotify/fsnotify" 9 | ) 10 | 11 | func watch(c config) error { 12 | watcher, err := fsnotify.NewWatcher() 13 | 14 | if err != nil { 15 | return err 16 | } 17 | defer watcher.Close() 18 | 19 | const dir = "./" 20 | 21 | if err := watcher.Add(dir); err != nil { 22 | return err 23 | } 24 | 25 | interval := 1 * time.Second 26 | tick := time.Tick(interval) 27 | done := make(chan struct{}, 1) 28 | 29 | // a buffer for events 30 | var events []fsnotify.Event 31 | var loopErr error 32 | 33 | go func() { 34 | Loop: 35 | for { 36 | select { 37 | case event := <-watcher.Events: 38 | if !strings.HasSuffix(event.Name, ".go") { 39 | continue 40 | } 41 | if is(event, fsnotify.Create) || is(event, fsnotify.Write) { 42 | events = append(events, event) 43 | } 44 | case loopErr = <-watcher.Errors: 45 | break Loop 46 | case <-tick: 47 | if len(events) == 0 { 48 | continue 49 | } 50 | 51 | // stop watching while gen'ing files 52 | loopErr = watcher.Remove(dir) 53 | if loopErr != nil { 54 | break Loop 55 | } 56 | 57 | // gen the files 58 | loopErr = run(c) 59 | if loopErr != nil { 60 | fmt.Fprintln(c.out, loopErr) 61 | } 62 | 63 | // clear the buffer 64 | events = make([]fsnotify.Event, 0) 65 | 66 | // resume watching 67 | loopErr = watcher.Add(dir) 68 | if loopErr != nil { 69 | break Loop 70 | } 71 | } 72 | } 73 | done <- struct{}{} 74 | }() 75 | 76 | <-done 77 | close(done) 78 | 79 | if loopErr != nil { 80 | return loopErr 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func is(event fsnotify.Event, op fsnotify.Op) bool { 87 | return event.Op&op == op 88 | } 89 | -------------------------------------------------------------------------------- /run_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/clipperhouse/typewriter" 8 | ) 9 | 10 | // +gen slice:"Where" 11 | type dummy int 12 | 13 | func TestRun(t *testing.T) { 14 | // use custom name so test won't interfere with a real _gen.go 15 | c := defaultConfig 16 | c.customName = "_gen_run_test.go" 17 | 18 | sliceName := "dummy_slice_test.go" 19 | fooName := "dummy_foo_test.go" 20 | 21 | // standard run 22 | if err := run(c); err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | // gen file should exist 27 | if _, err := os.Stat(sliceName); err != nil { 28 | t.Error(err) 29 | } 30 | 31 | // foo file should not exist, not a standard typewriter 32 | if _, err := os.Stat(fooName); err == nil { 33 | t.Errorf("%s should not have been generated", fooName) 34 | } 35 | 36 | // remove just-gen'd file 37 | if err := os.Remove(sliceName); err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | // create a custom typewriter import file 42 | imports := typewriter.NewImportSpecSet( 43 | typewriter.ImportSpec{Name: "_", Path: "github.com/clipperhouse/foowriter"}, 44 | ) 45 | 46 | if err := createCustomFile(c, imports); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | // custom run 51 | if err := run(c); err != nil { 52 | t.Error(err) 53 | } 54 | 55 | // clean up custom file, no longer needed 56 | if err := os.Remove(c.customName); err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | // foo file should exist 61 | if _, err := os.Stat(fooName); err != nil { 62 | t.Error(err) 63 | } 64 | 65 | // clean up foo file 66 | if err := os.Remove(fooName); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | // gen file should not exist, because it was not included in the custom file 71 | if _, err := os.Stat(sliceName); err == nil { 72 | t.Errorf("%s should not have been generated", sliceName) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /benchmarks/dummy_obj_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | dummyObjects dummyObjectSlice 10 | globalBoolResult bool 11 | globalSliceResult []*dummyDestinationSelectObject 12 | globalSliceResul2 []*dummyObject 13 | ) 14 | 15 | //Any 16 | func BenchmarkDummyObjAny_Generics(b *testing.B) { 17 | for n := 0; n < b.N; n++ { 18 | globalBoolResult = dummyObjects.Any(func(d *dummyObject) bool { return d.Num > 10000 }) 19 | } 20 | } 21 | 22 | func BenchmarkDummyObjAny_NativeLoop(b *testing.B) { 23 | for n := 0; n < b.N; n++ { 24 | any := false 25 | for _, d := range dummyObjects { 26 | if d.Num > 10000 { 27 | any = true 28 | break 29 | } 30 | } 31 | globalBoolResult = any 32 | } 33 | } 34 | 35 | //Select 36 | func BenchmarkDummyObjSelect_Generics(b *testing.B) { 37 | for n := 0; n < b.N; n++ { 38 | globalSliceResult = dummyObjects.SelectDummyDestinationSelectObject(func(d *dummyObject) *dummyDestinationSelectObject { return &dummyDestinationSelectObject{d.Name} }) 39 | globalBoolResult = len(globalSliceResult) == len(dummyObjects) 40 | } 41 | } 42 | 43 | func BenchmarkDummyObjSelect_NativeLoop(b *testing.B) { 44 | for n := 0; n < b.N; n++ { 45 | globalSliceResult = []*dummyDestinationSelectObject{} 46 | for _, d := range dummyObjects { 47 | globalSliceResult = append(globalSliceResult, &dummyDestinationSelectObject{d.Name}) 48 | } 49 | globalBoolResult = len(globalSliceResult) == len(dummyObjects) 50 | } 51 | } 52 | 53 | //SortBy 54 | func BenchmarkDummyObjSortBy_Generics(b *testing.B) { 55 | for n := 0; n < b.N; n++ { 56 | globalSliceResul2 = dummyObjects.SortBy(func(a *dummyObject, b *dummyObject) (isLess bool) { return a.Num%3 == 0 }) 57 | } 58 | } 59 | 60 | /*TODO: 61 | func BenchmarkDummyObjSortBy_NativeLoop(b *testing.B) { 62 | 63 | }*/ 64 | 65 | func init() { 66 | dummyObjects = dummyObjectSlice([]*dummyObject{}) 67 | for i := 0; i < 10000; i++ { 68 | dummyObjects = append(dummyObjects, &dummyObject{fmt.Sprintf("Name %d", i), i}) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What’s this? 2 | 3 | `gen` is a code-generation tool for Go. It’s intended to offer generics-like functionality on your types. Out of the box, it offers offers LINQ/underscore-inspired methods. 4 | 5 | It also offers third-party, runtime extensibility via [typewriters](https://github.com/clipperhouse/typewriter). 6 | 7 | #### [Introduction and docs…](http://clipperhouse.github.io/gen/) 8 | 9 | [Changelog](https://github.com/clipperhouse/gen/blob/master/CHANGELOG.md) 10 | 11 | [Hey, a video](https://www.youtube.com/watch?v=KY8OXFi3CDU) 12 | 13 | #### ‼️ 14 | 15 | This project is deprecated. It won't work with recent versions of Go, and...Go now has proper generics! Treat this project as a historical curiosity. 16 | 17 | ### Typewriters 18 | There is a list of open-source typewriters in [TYPEWRITERS.md](https://github.com/clipperhouse/gen/blob/master/TYPEWRITERS.md). Please add your own. 19 | 20 | ### Contributing 21 | 22 | There are three big parts of `gen`. 23 | 24 | #### gen 25 | 26 | This repository. The gen package is primarily the command-line interface. Most of the work is done by the typewriter package, and individual typewriters. 27 | 28 | #### typewriter 29 | 30 | The [typewriter package](https://github.com/clipperhouse/typewriter) is where most of the parsing, type evaluation and code generation architecture lives. 31 | 32 | #### typewriters 33 | 34 | Typewriters are where templates and logic live for generating code. Here’s [set](https://github.com/clipperhouse/set), which will make a lovely Set container for your type. Here’s [slice](https://github.com/clipperhouse/slice), which provides the built-in LINQ-like functionality. Here’s [stringer](https://github.com/clipperhouse/stringer), a fork of Rob Pike’s [tool](https://godoc.org/golang.org/x/tools/cmd/stringer). 35 | 36 | Third-party typewriters are added easily by the end user. You publish them as Go packages for import. [Learn more...](https://clipperhouse.github.io/gen/typewriters/) 37 | 38 | We’d love to see typewriter packages for things like strongly-typed JSON serialization, `Queue`s, `Pool`s or other containers. Anything “of T” is a candidate for a typewriter. 39 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | type parseTest struct { 9 | text string 10 | cmd string 11 | force bool 12 | tail int //length 13 | err bool //exists 14 | } 15 | 16 | func TestParseArgs(t *testing.T) { 17 | tests := []parseTest{ 18 | parseTest{"gen", "", false, 0, false}, 19 | parseTest{"gen -f", "", true, 0, false}, 20 | parseTest{"gen yadda", "", false, 0, true}, // unknown command 21 | parseTest{"gen -bar", "", false, 0, true}, // tail is not ok 22 | parseTest{"gen add", "add", false, 0, false}, 23 | parseTest{"gen add foo bar", "add", false, 2, false}, // tail is ok 24 | parseTest{"gen add -f", "add", true, 0, true}, // force is not ok 25 | parseTest{"gen get", "get", false, 0, false}, 26 | parseTest{"gen get foo bar", "get", false, 2, false}, // tail is ok 27 | parseTest{"gen get -f", "get", true, 0, true}, // force is not ok 28 | parseTest{"gen help", "help", false, 0, false}, 29 | parseTest{"gen help foo bar", "help", false, 0, true}, // tail is not ok 30 | parseTest{"gen help -f", "help", true, 0, true}, // force is not ok 31 | parseTest{"gen list", "list", false, 0, false}, 32 | parseTest{"gen list foo bar", "list", false, 0, true}, // tail is not ok 33 | parseTest{"gen list -f", "list", true, 0, true}, // force is not ok 34 | parseTest{"gen watch", "watch", false, 0, false}, 35 | parseTest{"gen watch foo bar", "watch", false, 0, true}, // tail is not ok 36 | parseTest{"gen watch -f", "watch", true, 0, false}, // force is ok 37 | } 38 | 39 | for i, test := range tests { 40 | cmd, force, tail, err := parseArgs(strings.Split(test.text, " ")) 41 | if cmd != test.cmd { 42 | t.Errorf("tests[%d]: cmd should be %q, got %q", i, test.cmd, cmd) 43 | } 44 | if force != test.force { 45 | t.Errorf("tests[%d]: force should be %v, got %v", i, test.force, force) 46 | } 47 | if len(tail) != test.tail { 48 | t.Errorf("tests[%d]: len(tail) should be %v, got %v", i, test.tail, len(tail)) 49 | } 50 | if (err != nil) != test.err { 51 | t.Errorf("tests[%d]: err existence should be %v, got %v", i, test.err, err) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // gen is a tool for type-driven code generation for Go. Details and docs are available at https://clipperhouse.github.io/gen. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "regexp" 8 | ) 9 | 10 | func main() { 11 | var err error 12 | 13 | defer func() { 14 | if err != nil { 15 | if !exitStatusMsg.MatchString(err.Error()) { 16 | os.Stderr.WriteString(err.Error() + "\n") 17 | } 18 | os.Exit(1) 19 | } 20 | }() 21 | 22 | err = runMain(os.Args) 23 | } 24 | 25 | var exitStatusMsg = regexp.MustCompile("^exit status \\d+$") 26 | 27 | func runMain(args []string) error { 28 | c := defaultConfig 29 | 30 | cmd, force, tail, err := parseArgs(args) 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | c.IgnoreTypeCheckErrors = force 37 | 38 | if len(cmd) == 0 { 39 | // simply typed 'gen'; run is the default command 40 | return run(c) 41 | } 42 | 43 | switch cmd { 44 | case "add": 45 | return add(c, tail...) 46 | case "get": 47 | return get(c, tail...) 48 | case "list": 49 | return list(c) 50 | case "watch": 51 | return watch(c) 52 | default: 53 | return help(c) 54 | } 55 | } 56 | 57 | var s = struct{}{} 58 | 59 | var cmds = map[string]struct{}{ 60 | "add": s, 61 | "get": s, 62 | "help": s, 63 | "list": s, 64 | "watch": s, 65 | } 66 | 67 | func parseArgs(args []string) (cmd string, force bool, tail []string, err error) { 68 | for _, a := range args[1:] { // arg[0] is 'gen' 69 | if _, ok := cmds[a]; ok { 70 | if len(cmd) > 0 { 71 | err = fmt.Errorf("more than one command specified; type gen help for usage") 72 | break 73 | } 74 | cmd = a 75 | continue 76 | } 77 | if a == "-f" { 78 | force = true 79 | continue 80 | } 81 | tail = append(tail, a) 82 | } 83 | 84 | // tail is only valid on add & get; otherwise an error 85 | if len(tail) > 0 && cmd != "add" && cmd != "get" { 86 | err = fmt.Errorf("unknown command(s) %v", tail) 87 | tail = []string{} 88 | } 89 | 90 | // force flag is only valid with run & watch 91 | if force && cmd != "" && cmd != "watch" { 92 | err = fmt.Errorf("-f flag is not valid with %q", cmd) 93 | } 94 | 95 | return cmd, force, tail, err 96 | } 97 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/clipperhouse/slice v0.0.0-20200107170738-a74fc3888fd9 h1:98FuFIacIB6rKfxodqagVhwLsrKEjve2T8l6O0oYjsM= 2 | github.com/clipperhouse/slice v0.0.0-20200107170738-a74fc3888fd9/go.mod h1:SYNbTtqlzmDAa8k/IZo8rBZrLHdB19cwOtJ6ySOwQZ4= 3 | github.com/clipperhouse/stringer v0.0.0-20200107165315-e8ef8175ba3b h1:YnvS5xvtmKSUlkH91qF6HKRlSK27qI8U+1Kl1EsiRKY= 4 | github.com/clipperhouse/stringer v0.0.0-20200107165315-e8ef8175ba3b/go.mod h1:dKiI12fHMknvyIFKvze6MRYQJCYOZ8Onjsvpw5byYrc= 5 | github.com/clipperhouse/typewriter v0.0.0-20200107164453-d21420026310 h1:IC0Ywh01N6qQ+jT87vS4zhVsrE1q3829nVMQwz7PF3o= 6 | github.com/clipperhouse/typewriter v0.0.0-20200107164453-d21420026310/go.mod h1:GbdR1VEFLckXDXyXO3pn/lERXrQLVOI9j1Fly05g32I= 7 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 12 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 17 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20200107162124-548cf772de50 h1:YvQ10rzcqWXLlJZ3XCUoO25savxmscf4+SC+ZqiCHhA= 19 | golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | golang.org/x/tools v0.0.0-20200107050322-53017a39ae36 h1:+eY+U4SdIdum+uGmzG+Y7oP2YWTOsFRElVIBD6K3Wgo= 22 | golang.org/x/tools v0.0.0-20200107050322-53017a39ae36/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 23 | golang.org/x/tools v0.0.0-20200110213125-a7a6caa82ab2 h1:V9r/14uGBqLgNlHRYWdVqjMdWkcOHnE2KG8DwVqQSEc= 24 | golang.org/x/tools v0.0.0-20200110213125-a7a6caa82ab2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 25 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 26 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | 8 | "github.com/clipperhouse/typewriter" 9 | ) 10 | 11 | func run(c config) error { 12 | imports := typewriter.NewImportSpecSet( 13 | typewriter.ImportSpec{Path: "fmt"}, 14 | typewriter.ImportSpec{Path: "os"}, 15 | typewriter.ImportSpec{Path: "regexp"}, 16 | typewriter.ImportSpec{Path: "github.com/clipperhouse/typewriter"}, 17 | ) 18 | 19 | return execute(runStandard, c, imports, runTmpl) 20 | } 21 | 22 | func runStandard(c config) (err error) { 23 | app, err := c.Config.NewApp("+gen") 24 | 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if len(app.Packages) == 0 { 30 | return fmt.Errorf("No packages were found. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 31 | } 32 | 33 | found := false 34 | 35 | for _, p := range app.Packages { 36 | found = found || len(p.Types) > 0 37 | } 38 | 39 | if !found { 40 | return fmt.Errorf("No types marked with +gen were found. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 41 | } 42 | 43 | if len(app.TypeWriters) == 0 { 44 | return fmt.Errorf("No typewriters were imported. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 45 | } 46 | 47 | if _, err := app.WriteAll(); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | var runTmpl = template.Must(template.New("run").Parse(` 55 | 56 | var exitStatusMsg = regexp.MustCompile("^exit status \\d+$") 57 | 58 | func main() { 59 | var err error 60 | 61 | defer func() { 62 | if err != nil { 63 | if !exitStatusMsg.MatchString(err.Error()) { 64 | os.Stderr.WriteString(err.Error() + "\n") 65 | } 66 | os.Exit(1) 67 | } 68 | }() 69 | 70 | err = run() 71 | } 72 | 73 | func run() error { 74 | config := {{ printf "%#v" .Config }} 75 | app, err := config.NewApp("+gen") 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | if len(app.Packages) == 0 { 82 | return fmt.Errorf("No packages were found. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 83 | } 84 | 85 | found := false 86 | 87 | for _, p := range app.Packages { 88 | found = found || len(p.Types) > 0 89 | } 90 | 91 | if !found { 92 | return fmt.Errorf("No types marked with +gen were found. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 93 | } 94 | 95 | if len(app.TypeWriters) == 0 { 96 | return fmt.Errorf("No typewriters were imported. See http://clipperhouse.github.io/gen to get started, or type %s help.", os.Args[0]) 97 | } 98 | 99 | if _, err := app.WriteAll(); err != nil { 100 | return err 101 | } 102 | 103 | return nil 104 | } 105 | `)) 106 | -------------------------------------------------------------------------------- /execute.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "text/template" 10 | 11 | "github.com/clipperhouse/typewriter" 12 | ) 13 | 14 | // execute runs a gen command by first determining whether a custom imports file (typically _gen.go) exists 15 | // 16 | // If no custom file exists, it executes the passed 'standard' func. 17 | // 18 | // If the custom file exists, new files are written to a temp directory and executed via `go run` in the shell. 19 | func execute(standard func(c config) error, c config, imports typewriter.ImportSpecSet, body *template.Template) error { 20 | if importsSrc, err := os.Open(c.customName); err == nil { 21 | defer importsSrc.Close() 22 | 23 | // custom imports file exists, use it 24 | return executeCustom(importsSrc, c, imports, body) 25 | } 26 | 27 | // do it the regular way 28 | return standard(c) 29 | } 30 | 31 | // executeCustom creates a temp directory, copies importsSrc into it and generates a main() using the passed imports and body. 32 | // 33 | // `go run` is then called on those files via os.Command. 34 | func executeCustom(importsSrc io.Reader, c config, imports typewriter.ImportSpecSet, body *template.Template) error { 35 | temp, err := getTempDir() 36 | if err != nil { 37 | return err 38 | } 39 | defer os.RemoveAll(temp) 40 | 41 | // set up imports file containing the custom typewriters (from _gen.go) 42 | importsDst, err := os.Create(filepath.Join(temp, "imports.go")) 43 | if err != nil { 44 | return err 45 | } 46 | defer importsDst.Close() 47 | 48 | io.Copy(importsDst, importsSrc) 49 | 50 | // set up main to be run 51 | main, err := os.Create(filepath.Join(temp, "main.go")) 52 | if err != nil { 53 | return err 54 | } 55 | defer main.Close() 56 | 57 | p := pkg{ 58 | Name: "main", 59 | Imports: imports, 60 | } 61 | 62 | // execute the package declaration and imports 63 | if err := tmpl.Execute(main, p); err != nil { 64 | return err 65 | } 66 | 67 | // write the body, usually a main() 68 | if err := body.Execute(main, c); err != nil { 69 | return err 70 | } 71 | 72 | // call `go run` on these files & send back output/err 73 | cmd := exec.Command("go", "run", main.Name(), importsDst.Name()) 74 | cmd.Stdout = c.out 75 | cmd.Stderr = c.out 76 | 77 | if err := cmd.Run(); err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // set up temp directory under current directory 85 | // make sure to defer os.RemoveAll() in caller 86 | func getTempDir() (string, error) { 87 | caller := filepath.Base(os.Args[0]) 88 | wd, _ := os.Getwd() 89 | return ioutil.TempDir(wd, caller) 90 | } 91 | 92 | var tmpl = template.Must(template.New("package").Parse(`package {{.Name}} 93 | {{if gt (len .Imports) 0}} 94 | import ({{range .Imports.ToSlice}} 95 | {{.Name}} "{{.Path}}"{{end}} 96 | ) 97 | {{end}}`)) 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt Sherman. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Matt Sherman nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | 30 | 31 | Portions of this software are derived from Go source, under the following license: 32 | 33 | --- 34 | 35 | Copyright (c) 2012 The Go Authors. All rights reserved. 36 | 37 | Redistribution and use in source and binary forms, with or without 38 | modification, are permitted provided that the following conditions are 39 | met: 40 | 41 | * Redistributions of source code must retain the above copyright 42 | notice, this list of conditions and the following disclaimer. 43 | * Redistributions in binary form must reproduce the above 44 | copyright notice, this list of conditions and the following disclaimer 45 | in the documentation and/or other materials provided with the 46 | distribution. 47 | * Neither the name of Google Inc. nor the names of its 48 | contributors may be used to endorse or promote products derived from 49 | this software without specific prior written permission. 50 | 51 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 52 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 53 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 54 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 55 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 56 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 57 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 58 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 59 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 60 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 61 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 62 | 63 | 64 | Portions of this software are derived from https://github.com/deckarep/golang-set 65 | 66 | --- 67 | 68 | Open Source Initiative OSI - The MIT License (MIT):Licensing 69 | 70 | The MIT License (MIT) 71 | Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy of 74 | this software and associated documentation files (the "Software"), to deal in 75 | the Software without restriction, including without limitation the rights to 76 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 77 | of the Software, and to permit persons to whom the Software is furnished to do 78 | so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | -------------------------------------------------------------------------------- /TYPEWRITERS.md: -------------------------------------------------------------------------------- 1 | ## Typewriters 2 | 3 | This is a list of known open-source typewriters in alphabetical order. 4 | GoDoc links are to the original implementation that might use `interface{}` instead of a gen type. 5 | Typewriters that would benefit from some improvement are listed at the bottom. 6 | Please add your own by making a pull request. Make sure you have documented the code before submitting a pull request. 7 | 8 | 9 | #### Heap [![GoDoc](https://godoc.org/container/heap?status.svg)](https://golang.org/pkg/container/heap) 10 | `gen add github.com/nickmab/gen/typewriters/container` 11 | 12 | ```go 13 | // +gen container:"Heap, HeapBy" 14 | type MyType struct{} 15 | ``` 16 | 17 | Implements a strongly-typed heap, based on [golang.org/pkg/container/heap](https://golang.org/pkg/container/heap). A heap is a tree with the property that each node is the minimum-valued node in its subtree. Useful implementation of a priority queue. 18 | 19 | 20 | #### LinkedList [![GoDoc](https://godoc.org/container/list?status.svg)](https://godoc.org/container/list) 21 | `gen add github.com/clipperhouse/linkedlist` 22 | 23 | ```go 24 | // +gen list 25 | type MyType struct{} 26 | ``` 27 | 28 | Implements a strongly-typed, doubly-linked list, based on [golang.org/pkg/container/list](https://golang.org/pkg/container/list). 29 | 30 | 31 | #### Map, Atomic [![GoDoc](https://godoc.org/github.com/ninibe/atomicmapper?status.svg)](https://godoc.org/github.com/ninibe/atomicmapper) 32 | `gen add github.com/ninibe/atomicmapper/gen` 33 | 34 | ```go 35 | // +gen atomicmap 36 | type MyType struct{} 37 | ``` 38 | 39 | Atomicmapper is a code generation tool for creating high-performance, scalable, frequently read, but infrequently updated maps of strings to any given `map[string]MyType`. It is based on Go's [atomic.Value read mostly example](https://golang.org/pkg/sync/atomic/#example_Value_readMostly). 40 | 41 | 42 | #### Ring [![GoDoc](https://godoc.org/container/ring?status.svg)](https://godoc.org/container/ring) 43 | `gen add github.com/clipperhouse/ring` 44 | 45 | ```go 46 | // +gen ring 47 | type MyType struct{} 48 | ``` 49 | 50 | Implements strongly-typed operations on circular lists, based on [golang.org/pkg/container/ring](https://golang.org/pkg/container/ring). 51 | 52 | 53 | #### Set [![GoDoc](https://godoc.org/github.com/deckarep/golang-set?status.svg)](https://godoc.org/github.com/deckarep/golang-set) 54 | `gen add github.com/clipperhouse/set` 55 | 56 | ```go 57 | // +gen set 58 | type MyType struct{} 59 | ``` 60 | Implements a strongly-typed unordered set with unique values, based on [github.com/deckarep/golang-set](https://github.com/deckarep/golang-set). 61 | 62 | 63 | #### Signal [![GoDoc](https://godoc.org/github.com/jackc/signal?status.svg)](https://godoc.org/github.com/jackc/signal) 64 | `gen add github.com/jackc/signal` 65 | 66 | ```go 67 | // +gen signal 68 | type MyType struct{} 69 | ``` 70 | Implements the signal pattern. [github.com/jackc/signal](https://github.com/jackc/signal). 71 | 72 | 73 | #### Slice [![GoDoc](https://godoc.org/github.com/clipperhouse/slice?status.svg)](https://godoc.org/github.com/clipperhouse/slice) 74 | `github.com/clipperhouse/slice` `built-in typewriter, no need to install` 75 | 76 | ```go 77 | // +gen slice:"Aggregate[T], All, Any, Average, Average[T], Count, Distinct, DisctinctBy, First, GroupBy[T], Max, Max[T], Min, Min[T], MinBy, Select[T], Shuffle, Sort, SortBy, Where" 78 | type MyType struct{} 79 | ``` 80 | Generates functional convenience methods that will look familiar to users of C#’s LINQ or JavaScript’s Array methods. It is intended to save you some loops, using a “pass a function” pattern. It offers easier ad-hoc sorts. Documentation is available at [clipperhouse.github.io/gen/slice](https://clipperhouse.github.io/gen/slice/). 81 | 82 | 83 | #### Stack [![GoDoc](https://godoc.org/github.com/svett/gen/stack?status.svg)](https://godoc.org/github.com/svett/gen/stack) 84 | `gen add github.com/svett/gen/stack` 85 | 86 | ```go 87 | // +gen stack 88 | type MyType struct{} 89 | ``` 90 | Implements a Stack. 91 | 92 | 93 | #### Stringer [![GoDoc](https://godoc.org/golang.org/x/tools/cmd/stringer?status.svg)](https://godoc.org/golang.org/x/tools/cmd/stringer) 94 | `github.com/clipperhouse/stringer` `built-in typewriter, no need to install` 95 | 96 | ```go 97 | // +gen stringer 98 | type Pill int 99 | 100 | const ( 101 | Placebo Pill = iota 102 | Aspirin 103 | Ibuprofen 104 | Paracetamol 105 | Acetaminophen = Paracetamol 106 | ) 107 | ``` 108 | Generates an implementation of the Stringer interface from const variable names, for pretty-printing, based on [golang.org/x/tools/cmd/stringer](https://golang.org/x/tools/cmd/stringer). 109 | 110 | 111 | 112 | #### Queue [![GoDoc](https://godoc.org/github.com/ggaaooppeenngg/queue?status.svg)](https://godoc.org/github.com/ggaaooppeenngg/queue) 113 | `gen add github.com/ggaaooppeenngg/queue` 114 | 115 | ```go 116 | // +gen queue 117 | type MyType struct{} 118 | ``` 119 | Implements a queue. 120 | 121 | 122 | 123 | ### Improvable typewriters 124 | Feel free to help these typewriters by making installation easier, improving documentation, writing tests or improving the implementation. 125 | 126 | 127 | ##### Flags [![GoDoc](https://godoc.org/github.com/michaelsmanley/flags?status.svg)](https://godoc.org/github.com/michaelsmanley/flags) 128 | `gen add github.com/michaelsmanley/flags` 129 | 130 | ```go 131 | // +gen flags" 132 | type MyType struct{} 133 | ``` 134 | Convenience functions for manipulating flags, in the bitset sense. 135 | 136 | 137 | ##### Sequences [![GoDoc](https://godoc.org/github.com/fernandokm/sequences?status.svg)](https://godoc.org/github.com/fernandokm/sequences) 138 | `gen add github.com/fernandokm/sequences` 139 | 140 | ```go 141 | // +gen sequenceGenerator:"Iterator[int64,uint64,*bigInt]" 142 | type MyType struct{} 143 | ``` 144 | Generates an implementation of a generator of primes, fibonacci or triangular numbers using a given type. 145 | 146 | 147 | ##### Slice extension [![GoDoc](https://godoc.org/github.com/remz/golang-sdk/gen_extras?status.svg)](https://godoc.org/github.com/remz/golang-sdk/gen_extras) 148 | `github.com/remz/golang-sdk/gen_extras` `extends the built-in slice implementation, adding new functions` 149 | 150 | ```go 151 | // These are the new functions added by this slice extension: 152 | // +gen slice:"AddOnce, Contains, Cut, Expand, Extend, FillRange, Find, Insert, InsertMultiple, IsEqualTo, MakeCopy, Mapping, Pop, Push, Remove, RemoveValue, ZeroUpTo" 153 | type MyType struct{} 154 | ``` 155 | Extends the Slice implementation by adding some more functions. Current implementation requires copying files into the Slice implementation folder and rebuilding gen. 156 | 157 | -------------------------------------------------------------------------------- /benchmarks/dummyobject_slice.go: -------------------------------------------------------------------------------- 1 | // Generated by: gen 2 | // TypeWriter: slice 3 | // Directive: +gen on *dummyObject 4 | 5 | package benchmarks 6 | 7 | // Sort implementation is a modification of http://golang.org/pkg/sort/#Sort 8 | // Copyright 2009 The Go Authors. All rights reserved. 9 | // Use of this source code is governed by a BSD-style 10 | // license that can be found at http://golang.org/LICENSE. 11 | 12 | // dummyObjectSlice is a slice of type *dummyObject. Use it where you would use []*dummyObject. 13 | type dummyObjectSlice []*dummyObject 14 | 15 | // Any verifies that one or more elements of dummyObjectSlice return true for the passed func. See: http://clipperhouse.github.io/gen/#Any 16 | func (rcv dummyObjectSlice) Any(fn func(*dummyObject) bool) bool { 17 | for _, v := range rcv { 18 | if fn(v) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // SelectDummyDestinationSelectObject projects a slice of *dummyDestinationSelectObject from dummyObjectSlice, typically called a map in other frameworks. See: http://clipperhouse.github.io/gen/#Select 26 | func (rcv dummyObjectSlice) SelectDummyDestinationSelectObject(fn func(*dummyObject) *dummyDestinationSelectObject) (result []*dummyDestinationSelectObject) { 27 | for _, v := range rcv { 28 | result = append(result, fn(v)) 29 | } 30 | return 31 | } 32 | 33 | // SortBy returns a new ordered dummyObjectSlice, determined by a func defining ‘less’. See: http://clipperhouse.github.io/gen/#SortBy 34 | func (rcv dummyObjectSlice) SortBy(less func(*dummyObject, *dummyObject) bool) dummyObjectSlice { 35 | result := make(dummyObjectSlice, len(rcv)) 36 | copy(result, rcv) 37 | // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. 38 | n := len(result) 39 | maxDepth := 0 40 | for i := n; i > 0; i >>= 1 { 41 | maxDepth++ 42 | } 43 | maxDepth *= 2 44 | quickSortdummyObjectSlice(result, less, 0, n, maxDepth) 45 | return result 46 | } 47 | 48 | // Sort implementation based on http://golang.org/pkg/sort/#Sort, see top of this file 49 | 50 | func swapdummyObjectSlice(rcv dummyObjectSlice, a, b int) { 51 | rcv[a], rcv[b] = rcv[b], rcv[a] 52 | } 53 | 54 | // Insertion sort 55 | func insertionSortdummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, a, b int) { 56 | for i := a + 1; i < b; i++ { 57 | for j := i; j > a && less(rcv[j], rcv[j-1]); j-- { 58 | swapdummyObjectSlice(rcv, j, j-1) 59 | } 60 | } 61 | } 62 | 63 | // siftDown implements the heap property on rcv[lo, hi). 64 | // first is an offset into the array where the root of the heap lies. 65 | func siftDowndummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, lo, hi, first int) { 66 | root := lo 67 | for { 68 | child := 2*root + 1 69 | if child >= hi { 70 | break 71 | } 72 | if child+1 < hi && less(rcv[first+child], rcv[first+child+1]) { 73 | child++ 74 | } 75 | if !less(rcv[first+root], rcv[first+child]) { 76 | return 77 | } 78 | swapdummyObjectSlice(rcv, first+root, first+child) 79 | root = child 80 | } 81 | } 82 | 83 | func heapSortdummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, a, b int) { 84 | first := a 85 | lo := 0 86 | hi := b - a 87 | 88 | // Build heap with greatest element at top. 89 | for i := (hi - 1) / 2; i >= 0; i-- { 90 | siftDowndummyObjectSlice(rcv, less, i, hi, first) 91 | } 92 | 93 | // Pop elements, largest first, into end of rcv. 94 | for i := hi - 1; i >= 0; i-- { 95 | swapdummyObjectSlice(rcv, first, first+i) 96 | siftDowndummyObjectSlice(rcv, less, lo, i, first) 97 | } 98 | } 99 | 100 | // Quicksort, following Bentley and McIlroy, 101 | // Engineering a Sort Function, SP&E November 1993. 102 | 103 | // medianOfThree moves the median of the three values rcv[a], rcv[b], rcv[c] into rcv[a]. 104 | func medianOfThreedummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, a, b, c int) { 105 | m0 := b 106 | m1 := a 107 | m2 := c 108 | // bubble sort on 3 elements 109 | if less(rcv[m1], rcv[m0]) { 110 | swapdummyObjectSlice(rcv, m1, m0) 111 | } 112 | if less(rcv[m2], rcv[m1]) { 113 | swapdummyObjectSlice(rcv, m2, m1) 114 | } 115 | if less(rcv[m1], rcv[m0]) { 116 | swapdummyObjectSlice(rcv, m1, m0) 117 | } 118 | // now rcv[m0] <= rcv[m1] <= rcv[m2] 119 | } 120 | 121 | func swapRangedummyObjectSlice(rcv dummyObjectSlice, a, b, n int) { 122 | for i := 0; i < n; i++ { 123 | swapdummyObjectSlice(rcv, a+i, b+i) 124 | } 125 | } 126 | 127 | func doPivotdummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, lo, hi int) (midlo, midhi int) { 128 | m := lo + (hi-lo)/2 // Written like this to avoid integer overflow. 129 | if hi-lo > 40 { 130 | // Tukey's Ninther, median of three medians of three. 131 | s := (hi - lo) / 8 132 | medianOfThreedummyObjectSlice(rcv, less, lo, lo+s, lo+2*s) 133 | medianOfThreedummyObjectSlice(rcv, less, m, m-s, m+s) 134 | medianOfThreedummyObjectSlice(rcv, less, hi-1, hi-1-s, hi-1-2*s) 135 | } 136 | medianOfThreedummyObjectSlice(rcv, less, lo, m, hi-1) 137 | 138 | // Invariants are: 139 | // rcv[lo] = pivot (set up by ChoosePivot) 140 | // rcv[lo <= i < a] = pivot 141 | // rcv[a <= i < b] < pivot 142 | // rcv[b <= i < c] is unexamined 143 | // rcv[c <= i < d] > pivot 144 | // rcv[d <= i < hi] = pivot 145 | // 146 | // Once b meets c, can swap the "= pivot" sections 147 | // into the middle of the slice. 148 | pivot := lo 149 | a, b, c, d := lo+1, lo+1, hi, hi 150 | for { 151 | for b < c { 152 | if less(rcv[b], rcv[pivot]) { // rcv[b] < pivot 153 | b++ 154 | } else if !less(rcv[pivot], rcv[b]) { // rcv[b] = pivot 155 | swapdummyObjectSlice(rcv, a, b) 156 | a++ 157 | b++ 158 | } else { 159 | break 160 | } 161 | } 162 | for b < c { 163 | if less(rcv[pivot], rcv[c-1]) { // rcv[c-1] > pivot 164 | c-- 165 | } else if !less(rcv[c-1], rcv[pivot]) { // rcv[c-1] = pivot 166 | swapdummyObjectSlice(rcv, c-1, d-1) 167 | c-- 168 | d-- 169 | } else { 170 | break 171 | } 172 | } 173 | if b >= c { 174 | break 175 | } 176 | // rcv[b] > pivot; rcv[c-1] < pivot 177 | swapdummyObjectSlice(rcv, b, c-1) 178 | b++ 179 | c-- 180 | } 181 | 182 | min := func(a, b int) int { 183 | if a < b { 184 | return a 185 | } 186 | return b 187 | } 188 | 189 | n := min(b-a, a-lo) 190 | swapRangedummyObjectSlice(rcv, lo, b-n, n) 191 | 192 | n = min(hi-d, d-c) 193 | swapRangedummyObjectSlice(rcv, c, hi-n, n) 194 | 195 | return lo + b - a, hi - (d - c) 196 | } 197 | 198 | func quickSortdummyObjectSlice(rcv dummyObjectSlice, less func(*dummyObject, *dummyObject) bool, a, b, maxDepth int) { 199 | for b-a > 7 { 200 | if maxDepth == 0 { 201 | heapSortdummyObjectSlice(rcv, less, a, b) 202 | return 203 | } 204 | maxDepth-- 205 | mlo, mhi := doPivotdummyObjectSlice(rcv, less, a, b) 206 | // Avoiding recursion on the larger subproblem guarantees 207 | // a stack depth of at most lg(b-a). 208 | if mlo-a < b-mhi { 209 | quickSortdummyObjectSlice(rcv, less, a, mlo, maxDepth) 210 | a = mhi // i.e., quickSortdummyObjectSlice(rcv, mhi, b) 211 | } else { 212 | quickSortdummyObjectSlice(rcv, less, mhi, b, maxDepth) 213 | b = mlo // i.e., quickSortdummyObjectSlice(rcv, a, mlo) 214 | } 215 | } 216 | if b-a > 1 { 217 | insertionSortdummyObjectSlice(rcv, less, a, b) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 3 Jan 2015 2 | 3 | Added [stringer](https://github.com/clipperhouse/stringer) as a built-in typewriter. 4 | 5 | ### 30 Nov 2014 (v4) 6 | 7 | To get the latest: `go get github.com/clipperhouse/gen`. Type `gen help` to see usage. 8 | 9 | (Because the go.tools import path has changed, you may see import errors. You may wish to delete the code.google.com folder from your $GOPATH/src and maybe from /pkg too. Heck, go ahead delete your $GOPATH/src/github.com/clipperhouse folder to start clean.) 10 | 11 | This release has several substantial changes. 12 | 13 | #### Type parameters 14 | 15 | Tags now have support for type parameters, for example: 16 | 17 | // +gen foo:"Bar[qux], Qaz[thing, stuff]" 18 | type MyType struct{} 19 | 20 | Those type parameters (qux, thing, stuff) are properly evaluated as types, and passed to your typewriters. 21 | 22 | #### Type constraints 23 | 24 | Speaking of above, types are evaluated for Numeric, Ordered and Comparable. Templates, in turn, can have type constraints. 25 | 26 | For example, you can declare your Sum template only to be applicable to Numeric types, and your Set to Comparable types. 27 | 28 | #### gen add 29 | 30 | Third-party typewriters are added to your package using a new command, `add`. It looks like this: 31 | 32 | gen add github.com/clipperhouse/set 33 | 34 | That’s a plain old Go import path. 35 | 36 | After adding, you can mark up a type like: 37 | 38 | // +gen set slice:"GroupBy[string], Select[Foo]" 39 | type MyType struct{} 40 | 41 | As always, it’s up to the third-party typewriter to determine behavior. In this case, a “naked” `set` tag is enough. 42 | 43 | We deprecated the unintuitive `gen custom` command, `add` replaces it. 44 | 45 | #### gen watch 46 | 47 | ...will watch the current directory and `gen` on file changes. 48 | 49 | #### Explcitness 50 | 51 | Previous versions of gen would generate a dozen or so LINQ-style slice methods simply by marking up: 52 | 53 | // +gen 54 | type MyType struct{} 55 | 56 | We’ve opted for explicitness moving forward – in the case of slices, you’ll write this instead: 57 | 58 | // +gen slice:"Where, SortBy, Any" 59 | type MyType struct{} 60 | 61 | In other words, only the methods you want. 62 | 63 | #### Projections 64 | 65 | Certain methods, such as Select and GroupBy require an additional type parameter. I won’t bore you with the convoluted old way. Now it’s: 66 | 67 | // +gen slice:"GroupBy[string], Select[Foo]" 68 | type MyType struct{} 69 | 70 | Those type parameters are properly evaluated, and typewriters get full type information on them. 71 | 72 | #### slice 73 | 74 | The main built-in typewriter used to be called `genwriter`, it is now called `slice`. Instead of the generated slice type being called Things, it’s now called ThingSlice. 75 | 76 | [slice](https://github.com/clipperhouse/slice) is now the only built-in typewriter. 77 | 78 | We’ve deprecated the built-in container typewriter, instead splitting it into optional [Set](https://github.com/clipperhouse/set), [List](https://github.com/clipperhouse/linkedlist) and [Ring](https://github.com/clipperhouse/ring) typewriters. 79 | 80 | You can add them using the `add` command described above: 81 | 82 | gen add github.com/clipperhouse/linkedlist 83 | 84 | #### Smaller interface 85 | 86 | For those developing their own typewriters: the [`TypeWriter` interface](https://github.com/clipperhouse/typewriter/blob/master/typewriter.go) got smaller. It’s now: 87 | 88 | type TypeWriter interface { 89 | Name() string 90 | Imports(t Type) []ImportSpec 91 | Write(w io.Writer, t Type) error 92 | } 93 | 94 | `Validate` is gone, it was awkward. The easy fix there was to allow Write to return an error. `WriteHeader` is gone, there was little use for it in practice. `WriteBody` is now simply `Write`. 95 | 96 | We also run [goimports](https://godoc.org/golang.org/x/tools/imports) on generated code, so if your typewriter only uses the standard library, you might not need to specify anything for Imports() -- they’ll automagically be added to the generated source. 97 | 98 | Let me (@clipperhouse) know if any questions. 99 | 100 | ### 28 Jun 2014 (v3) 101 | 102 | To get the latest: `go get -u github.com/clipperhouse/gen`. Type `gen help` to see commands. 103 | 104 | This release introduces the optional `_gen.go` file for importing custom typewriters. 105 | 106 | Prior to this release, typewriters were simply part of the `gen` binary. Now, by creating a file of the above name in your package, third-party typewriters can be included at runtime. 107 | 108 | `cd` into your package and type `gen custom`. You will see a new `_gen.go` file which looks like this: 109 | 110 | ``` 111 | package main 112 | 113 | import ( 114 | _ "github.com/clipperhouse/containerwriter" 115 | _ "github.com/clipperhouse/typewriters/genwriter" 116 | ) 117 | ``` 118 | 119 | Change those import paths to other, third-party typewriters. Then call `gen get`. `gen list` is helpful too. 120 | 121 | Docs on how to create a typewriter are coming soon. In the meantime, have a look at the [container](https://github.com/clipperhouse/gen/tree/master/typewriters/container) typewriter for a decent example. 122 | 123 | ### 12 Jun 2014 124 | 125 | A new architecture based on ‘typewriters’. Ideally you should see little change, but… 126 | 127 | This was a long-lived branch, around 6 weeks. The architecture should be a lot better, as well as the testing. But of course regressions are possible. 128 | 129 | I will mark this with a release 2.0 tag following semver conventions, since there are some behavioral changes. If there are regressions for you, you can use gopkg.in to stick with 1.0. 130 | 131 | A few important behavioral changes: 132 | 133 | - Each typewriter now outputs a separate file, eg `*_gen.go` and `*_container.go`. This should not be a breaking change. Formerly, there was a single `*_gen.go` file 134 | - Previously-gen’d files which have been ‘un-gen’d’ (i.e. removed a tag) will not be deleted as before. Do this manually if need be, but I hope it’s an edge case. Will consider adding it back in. 135 | - The `-force` flag is gone. Officially, it was undefined behavior, intended for use if you get yourself into a jam. 136 | - The contents of your gen’d files may be slightly different, but their behavior should be unchanged. 137 | 138 | We are going to exploit this architecture to do some ~~evil~~ interesting things, stay tuned. 139 | 140 | Any trouble, please let me know via a GitHub issue, or [Twitter](http://twitter.com/clipperhouse), or… 141 | 142 | ### 9 Mar 2014 143 | 144 | Preliminary support for containers, starting with Set, List and Ring. The use case is to generate a strongly-typed container, to avoid the overhead of casting (type assertions), and to add compile-time safety. 145 | 146 | ### 1 Feb 2014 147 | 148 | gen will now delete `*_gen.go` files in the case that a previously-gen’d type has been removed, per #34. Confirms with the user for safety. And of course you are using version control, right? 149 | 150 | Added new `Max` and `Min` (alongside the existing respective *By). The difference is that these work on types that are known ordered such as type MyOrderable int. It requires no passed ‘less’ func because we already know what ‘less’ is for such types. #28. 151 | 152 | MaxBy(less) and MinBy(less) are still the way to go for structs or ad-hoc ordering. 153 | 154 | ### 30 Jan 2014 155 | 156 | Added new `Sort` (alongside the existing `SortBy`). The difference is that Sort works on types that are known sortable such as `type MySortable int`. It requires no passed ‘less’ func because we already know what ‘less’ is for such types. :) 157 | 158 | `SortBy(less)` is still the way to go for structs or any ad-hoc sorts. 159 | 160 | The ‘integration tests’ (those which test the gen’d code) have been rewritten for clarity. 161 | 162 | Generated code is now passed through the go/format package (gofmt) on output, so you don’t have to. 163 | 164 | ### 26 Jan 2014 165 | 166 | This release includes breaking changes. To update: 167 | 168 | `go get -u github.com/clipperhouse/gen` 169 | 170 | Command-line type specification has been deprecated, and replaced by markup per #23. It takes the form of: 171 | 172 | ``` 173 | // +gen 174 | type MyType struct {...} 175 | Where before the command-line would be gen package.Type, now it's simply gen, which will locate and process your marked-up types. 176 | ``` 177 | Here's a larger example: 178 | 179 | ``` 180 | // +gen * methods:"Count,Where" projections:"SomeType,int" 181 | type MyType struct {...} 182 | ``` 183 | 184 | - The * is a directive to generate methods which take a pointer type instead of a value type. Optional but recommended. 185 | - The methods tag is for subsetting methods; replaces gen:"...". Optional; omit it to generate all standard methods. 186 | - The projections tag specifies types to be projected for methods such as Select and GroupBy. Optional. If the methods tag is omitted, all projection methods will be generated, appropriate to each type. (For example, Average will not be generated for non-numeric types.) You can subset projection methods using the methods tag above. 187 | - The -all flag has been deprecated, it's no longer a valid use case, given the above. The -exported flag, which is a modifier of same, is gone too. 188 | 189 | Custom methods, where specific member fields of a struct are marked up, have been deprecated. The rationale is that we prefer to project types, not fields. 190 | 191 | Sort(func) has been renamed SortBy(func), and similarly Max → MaxBy, Min → MinBy. This is done in anticipation of methods of those names which will not take a func, see #28. 192 | --------------------------------------------------------------------------------