Generate flag types (again?)

Closes #1381
This commit is contained in:
2022-05-01 23:02:05 -04:00
parent cbd9bd97e8
commit ed0033984b
32 changed files with 1304 additions and 516 deletions

View File

@@ -6,7 +6,6 @@ import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"log"
"math"
"os"
@@ -16,9 +15,18 @@ import (
"github.com/urfave/cli/v2"
)
var packages = []string{"cli", "altsrc"}
func main() {
top, err := func() (string, error) {
if v, err := sh("git", "rev-parse", "--show-toplevel"); err == nil {
return strings.TrimSpace(v), nil
}
return os.Getwd()
}()
if err != nil {
log.Fatal(err)
}
app := cli.NewApp()
app.Name = "builder"
@@ -45,20 +53,41 @@ func main() {
Name: "check-binary-size",
Action: checkBinarySizeActionFunc,
},
{
Name: "generate",
Action: GenerateActionFunc,
},
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "tags",
Usage: "set build tags",
},
&cli.PathFlag{
Name: "top",
Value: top,
},
&cli.StringSliceFlag{
Name: "packages",
Value: cli.NewStringSlice("cli", "altsrc", "internal/build", "internal/genflags"),
},
}
err := app.Run(os.Args)
if err != nil {
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func sh(exe string, args ...string) (string, error) {
cmd := exec.Command(exe, args...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
outBytes, err := cmd.Output()
return string(outBytes), err
}
func runCmd(arg string, args ...string) error {
cmd := exec.Command(arg, args...)
@@ -66,78 +95,63 @@ func runCmd(arg string, args ...string) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
return cmd.Run()
}
func VetActionFunc(_ *cli.Context) error {
return runCmd("go", "vet")
func VetActionFunc(cCtx *cli.Context) error {
return runCmd("go", "vet", cCtx.Path("top")+"/...")
}
func TestActionFunc(c *cli.Context) error {
tags := c.String("tags")
for _, pkg := range packages {
var packageName string
for _, pkg := range c.StringSlice("packages") {
packageName := "github.com/urfave/cli/v2"
if pkg == "cli" {
packageName = "github.com/urfave/cli/v2"
} else {
if pkg != "cli" {
packageName = fmt.Sprintf("github.com/urfave/cli/v2/%s", pkg)
}
coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg)
err := runCmd("go", "test", "-tags", tags, "-v", coverProfile, packageName)
if err != nil {
if err := runCmd(
"go", "test",
"-tags", tags,
"-v",
"--coverprofile", pkg+".coverprofile",
"--covermode", "count",
"--cover", packageName,
packageName,
); err != nil {
return err
}
}
return testCleanup()
return testCleanup(c.StringSlice("packages"))
}
func testCleanup() error {
var out bytes.Buffer
func testCleanup(packages []string) error {
out := &bytes.Buffer{}
fmt.Fprintf(out, "mode: count\n")
for _, pkg := range packages {
file, err := os.Open(fmt.Sprintf("%s.coverprofile", pkg))
filename := pkg + ".coverprofile"
lineBytes, err := os.ReadFile(filename)
if err != nil {
return err
}
b, err := ioutil.ReadAll(file)
if err != nil {
return err
}
lines := strings.Split(string(lineBytes), "\n")
out.Write(b)
err = file.Close()
if err != nil {
return err
}
fmt.Fprintf(out, strings.Join(lines[1:], "\n"))
err = os.Remove(fmt.Sprintf("%s.coverprofile", pkg))
if err != nil {
if err := os.Remove(filename); err != nil {
return err
}
}
outFile, err := os.Create("coverage.txt")
if err != nil {
return err
}
_, err = out.WriteTo(outFile)
if err != nil {
return err
}
err = outFile.Close()
if err != nil {
return err
}
return nil
return os.WriteFile("coverage.txt", out.Bytes(), 0644)
}
func GfmrunActionFunc(c *cli.Context) error {
@@ -179,17 +193,7 @@ func TocActionFunc(c *cli.Context) error {
filename = "README.md"
}
err := runCmd("markdown-toc", "-i", filename)
if err != nil {
return err
}
err = runCmd("git", "diff", "--exit-code")
if err != nil {
return err
}
return nil
return runCmd("markdown-toc", "-i", filename)
}
// checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down
@@ -201,7 +205,6 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
cliBuiltFilePath = "./internal/example-cli/built-example"
helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go"
helloBuiltFilePath = "./internal/example-hello-world/built-example"
desiredMinBinarySize = 1.675
desiredMaxBinarySize = 2.2
badNewsEmoji = "🚨"
goodNewsEmoji = "✨"
@@ -209,8 +212,14 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
mbStringFormatter = "%.1fMB"
)
desiredMinBinarySize := 1.675
tags := c.String("tags")
if strings.Contains(tags, "urfave_cli_no_docs") {
desiredMinBinarySize = 1.39
}
// get cli example size
cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath, tags)
if err != nil {
@@ -280,6 +289,10 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
return nil
}
func GenerateActionFunc(cCtx *cli.Context) error {
return runCmd("go", "generate", cCtx.Path("top")+"/...")
}
func getSize(sourcePath string, builtPath string, tags string) (size int64, err error) {
// build example binary
err = runCmd("go", "build", "-tags", tags, "-o", builtPath, "-ldflags", "-s -w", sourcePath)

View File

@@ -0,0 +1,161 @@
package main
import (
"bytes"
"context"
_ "embed"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/template"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/internal/genflags"
"gopkg.in/yaml.v2"
)
const (
defaultPackageName = "cli"
)
func sh(ctx context.Context, exe string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, exe, args...)
cmd.Stderr = os.Stderr
outBytes, err := cmd.Output()
return string(outBytes), err
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
top := "../../"
if v, err := sh(ctx, "git", "rev-parse", "--show-toplevel"); err == nil {
top = strings.TrimSpace(v)
}
app := &cli.App{
Flags: []cli.Flag{
&cli.PathFlag{
Name: "flag-spec-yaml",
Aliases: []string{"f"},
Value: filepath.Join(top, "flag-spec.yaml"),
},
&cli.PathFlag{
Name: "generated-output",
Aliases: []string{"o"},
Value: filepath.Join(top, "zz_generated.flags.go"),
},
&cli.PathFlag{
Name: "generated-test-output",
Aliases: []string{"t"},
Value: filepath.Join(top, "zz_generated.flags_test.go"),
},
&cli.StringFlag{
Name: "generated-package-name",
Aliases: []string{"p"},
Value: defaultPackageName,
},
&cli.StringFlag{
Name: "generated-test-package-name",
Aliases: []string{"T"},
Value: defaultPackageName + "_test",
},
&cli.StringFlag{
Name: "urfave-cli-namespace",
Aliases: []string{"n"},
Value: "",
},
&cli.StringFlag{
Name: "urfave-cli-test-namespace",
Aliases: []string{"N"},
Value: "cli.",
},
},
Action: runGenFlags,
}
if err := app.RunContext(ctx, os.Args); err != nil {
log.Fatal(err)
}
}
func runGenFlags(cCtx *cli.Context) error {
specBytes, err := os.ReadFile(cCtx.Path("flag-spec-yaml"))
if err != nil {
return err
}
spec := &genflags.Spec{}
if err := yaml.Unmarshal(specBytes, spec); err != nil {
return err
}
if cCtx.IsSet("generated-package-name") {
spec.PackageName = strings.TrimSpace(cCtx.String("generated-package-name"))
}
if strings.TrimSpace(spec.PackageName) == "" {
spec.PackageName = defaultPackageName
}
if cCtx.IsSet("generated-test-package-name") {
spec.TestPackageName = strings.TrimSpace(cCtx.String("generated-test-package-name"))
}
if strings.TrimSpace(spec.TestPackageName) == "" {
spec.TestPackageName = defaultPackageName + "_test"
}
if cCtx.IsSet("urfave-cli-namespace") {
spec.UrfaveCLINamespace = strings.TrimSpace(cCtx.String("urfave-cli-namespace"))
}
if cCtx.IsSet("urfave-cli-test-namespace") {
spec.UrfaveCLITestNamespace = strings.TrimSpace(cCtx.String("urfave-cli-test-namespace"))
} else {
spec.UrfaveCLITestNamespace = "cli."
}
genTmpl, err := template.New("gen").Parse(genflags.TemplateString)
if err != nil {
return err
}
genTestTmpl, err := template.New("gen_test").Parse(genflags.TestTemplateString)
if err != nil {
return err
}
genBuf := &bytes.Buffer{}
if err := genTmpl.Execute(genBuf, spec); err != nil {
return err
}
genTestBuf := &bytes.Buffer{}
if err := genTestTmpl.Execute(genTestBuf, spec); err != nil {
return err
}
if err := os.WriteFile(cCtx.Path("generated-output"), genBuf.Bytes(), 0644); err != nil {
return err
}
if err := os.WriteFile(cCtx.Path("generated-test-output"), genTestBuf.Bytes(), 0644); err != nil {
return err
}
if _, err := sh(cCtx.Context, "goimports", "-w", cCtx.Path("generated-output")); err != nil {
return err
}
if _, err := sh(cCtx.Context, "goimports", "-w", cCtx.Path("generated-test-output")); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,53 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.PackageName}}
{{range .SortedFlagTypes}}
// {{.TypeName}} is a flag with type {{.GoType}}
type {{.TypeName}} struct {
Name string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value {{if .ValuePointer}}*{{end}}{{.GoType}}
Destination *{{.GoType}}
Aliases []string
EnvVars []string
{{range .StructFields}}
{{.Name}} {{.Type}}
{{end}}
}
{{if .GenerateFmtStringerInterface}}
// String returns a readable representation of this value (for usage defaults)
func (f *{{.TypeName}}) String() string {
return {{$.UrfaveCLINamespace}}FlagStringer(f)
}
{{end}}
{{if .GenerateFlagInterface}}
// IsSet returns whether or not the flag has been set through env or file
func (f *{{.TypeName}}) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *{{.TypeName}}) Names() []string {
return {{$.UrfaveCLINamespace}}FlagNames(f.Name, f.Aliases)
}
{{end}}{{/* /if .GenerateFlagInterface */}}
{{end}}{{/* /range .SortedFlagTypes */}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@@ -0,0 +1,22 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.TestPackageName}}
{{range .SortedFlagTypes}}
{{if .GenerateFlagInterface}}
func Test{{.TypeName}}_SatisfiesFlagInterface(t *testing.T) {
var _ {{$.UrfaveCLITestNamespace}}Flag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
}
{{end}}
{{if .GenerateFmtStringerInterface}}
func Test{{.TypeName}}_SatisfiesFmtStringerInterface(t *testing.T) {
var _ fmt.Stringer = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
}
{{end}}
{{end}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@@ -0,0 +1,34 @@
package genflags
import (
_ "embed"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
//go:embed generated.gotmpl
TemplateString string
//go:embed generated_test.gotmpl
TestTemplateString string
titler = cases.Title(language.Und, cases.NoLower)
)
func TypeName(goType string, fc *FlagTypeConfig) string {
if fc != nil && strings.TrimSpace(fc.TypeName) != "" {
return strings.TrimSpace(fc.TypeName)
}
dotSplit := strings.Split(goType, ".")
goType = dotSplit[len(dotSplit)-1]
if strings.HasPrefix(goType, "[]") {
return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag"
}
return titler.String(goType) + "Flag"
}

View File

@@ -0,0 +1,41 @@
package genflags_test
import (
"fmt"
"testing"
"github.com/urfave/cli/v2/internal/genflags"
)
func TestTypeName(t *testing.T) {
for _, tc := range []struct {
gt string
fc *genflags.FlagTypeConfig
expected string
}{
{gt: "int", fc: nil, expected: "IntFlag"},
{gt: "int", fc: &genflags.FlagTypeConfig{}, expected: "IntFlag"},
{gt: "int", fc: &genflags.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"},
{gt: "[]bool", fc: nil, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &genflags.FlagTypeConfig{}, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &genflags.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"},
{gt: "time.Rumination", fc: nil, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &genflags.FlagTypeConfig{}, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &genflags.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"},
} {
t.Run(
fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string {
if tc.fc != nil {
return tc.fc.TypeName
}
return "nil"
}()),
func(ct *testing.T) {
actual := genflags.TypeName(tc.gt, tc.fc)
if tc.expected != actual {
ct.Errorf("expected %q, got %q", tc.expected, actual)
}
},
)
}
}

100
internal/genflags/spec.go Normal file
View File

@@ -0,0 +1,100 @@
package genflags
import (
"sort"
"strings"
)
type Spec struct {
FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"`
PackageName string `yaml:"package_name"`
TestPackageName string `yaml:"test_package_name"`
UrfaveCLINamespace string `yaml:"urfave_cli_namespace"`
UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"`
}
func (gfs *Spec) SortedFlagTypes() []*FlagType {
typeNames := []string{}
for name := range gfs.FlagTypes {
if strings.HasPrefix(name, "[]") {
name = strings.TrimPrefix(name, "[]") + "Slice"
}
typeNames = append(typeNames, name)
}
sort.Strings(typeNames)
ret := make([]*FlagType, len(typeNames))
for i, typeName := range typeNames {
ret[i] = &FlagType{
GoType: typeName,
Config: gfs.FlagTypes[typeName],
}
}
return ret
}
type FlagTypeConfig struct {
SkipInterfaces []string `yaml:"skip_interfaces"`
StructFields []*FlagStructField `yaml:"struct_fields"`
TypeName string `yaml:"type_name"`
ValuePointer bool `yaml:"value_pointer"`
}
type FlagStructField struct {
Name string
Type string
}
type FlagType struct {
GoType string
Config *FlagTypeConfig
}
func (ft *FlagType) StructFields() []*FlagStructField {
if ft.Config == nil || ft.Config.StructFields == nil {
return []*FlagStructField{}
}
return ft.Config.StructFields
}
func (ft *FlagType) ValuePointer() bool {
if ft.Config == nil {
return false
}
return ft.Config.ValuePointer
}
func (ft *FlagType) TypeName() string {
return TypeName(ft.GoType, ft.Config)
}
func (ft *FlagType) GenerateFmtStringerInterface() bool {
return ft.skipInterfaceNamed("fmt.Stringer")
}
func (ft *FlagType) GenerateFlagInterface() bool {
return ft.skipInterfaceNamed("Flag")
}
func (ft *FlagType) skipInterfaceNamed(name string) bool {
if ft.Config == nil {
return true
}
lowName := strings.ToLower(name)
for _, interfaceName := range ft.Config.SkipInterfaces {
if strings.ToLower(interfaceName) == lowName {
return false
}
}
return true
}