Move genflags tool to cmd/ and pin to previous release

to alleviate problems caused by the circular dependency of using the
same code as a library that is potentially being generated to adhere to
a different API.
This commit is contained in:
2022-08-14 10:02:07 -04:00
parent e3ee4fb3ef
commit 3e4237a67f
17 changed files with 252 additions and 203 deletions

View File

@@ -0,0 +1,21 @@
GOTEST_FLAGS ?= -v --coverprofile main.coverprofile --covermode count --cover github.com/urfave/cli/v2/cmd/urfave-cli-genflags
GOBUILD_FLAGS ?= -x
.PHONY: all
all: test build smoke-test
.PHONY: test
test:
go test $(GOTEST_FLAGS) ./...
.PHONY: build
build:
go build $(GOBUILD_FLAGS) ./...
.PHONY: smoke-test
smoke-test: build
./urfave-cli-genflags --help
.PHONY: show-cover
show-cover:
go tool cover -func main.coverprofile

View File

@@ -0,0 +1,15 @@
# urfave-cli-genflags
This is a tool that is used internally by [urfave/cli] to generate
flag types and methods from a YAML input. It intentionally pins
usage of `github.com/urfave/cli/v2` to a *release* rather than
using the adjacent code so that changes don't result in *this* tool
refusing to compile. It's almost like dogfooding?
## support warning
This tool is maintained as a sub-project and is not covered by the
API and backward compatibility guaranteed by releases of
[urfave/cli].
[urfave/cli]: https://github.com/urfave/cli

View File

@@ -0,0 +1,68 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.PackageName}}
{{range .SortedFlagTypes}}
// {{.TypeName}} is a flag with type {{if .ValuePointer}}*{{end}}{{.GoType}}
type {{.TypeName}} struct {
Name string
Category 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 .GenerateFmtStringerInterface */}}
{{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 */}}
{{if .GenerateRequiredFlagInterface}}
// IsRequired returns whether or not the flag is required
func (f *{{.TypeName}}) IsRequired() bool {
return f.Required
}
{{end}}{{/* /if .GenerateRequiredFlagInterface */}}
{{if .GenerateVisibleFlagInterface}}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *{{.TypeName}}) IsVisible() bool {
return !f.Hidden
}
{{end}}{{/* /if .GenerateVisibleFlagInterface */}}
{{end}}{{/* /range .SortedFlagTypes */}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@@ -0,0 +1,43 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.TestPackageName}}
{{range .SortedFlagTypes}}
{{if .GenerateFlagInterface}}
func Test{{.TypeName}}_SatisfiesFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}Flag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsSet()
_ = f.Names()
}
{{end}}
{{if .GenerateFmtStringerInterface}}
func Test{{.TypeName}}_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.String()
}
{{end}}
{{if .GenerateRequiredFlagInterface}}
func Test{{.TypeName}}_SatisfiesRequiredFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}RequiredFlag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsRequired()
}
{{end}}
{{if .GenerateVisibleFlagInterface}}
func Test{{.TypeName}}_SatisfiesVisibleFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}VisibleFlag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsVisible()
}
{{end}}
{{end}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@@ -0,0 +1,15 @@
module github.com/urfave/cli/v2/cmd/urfave-cli-genflags
go 1.18
require (
github.com/urfave/cli/v2 v2.11.2
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
)

View File

@@ -0,0 +1,14 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA=
github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,292 @@
package main
import (
"bytes"
"context"
_ "embed"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"text/template"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
const (
defaultPackageName = "cli"
)
var (
//go:embed generated.gotmpl
TemplateString string
//go:embed generated_test.gotmpl
TestTemplateString string
titler = cases.Title(language.Und, cases.NoLower)
)
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{
Name: "genflags",
Usage: "Generate flag types for urfave/cli",
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 := &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(TemplateString)
if err != nil {
return err
}
genTestTmpl, err := template.New("gen_test").Parse(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
}
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"
}
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) GenerateRequiredFlagInterface() bool {
return ft.skipInterfaceNamed("RequiredFlag")
}
func (ft *FlagType) GenerateVisibleFlagInterface() bool {
return ft.skipInterfaceNamed("VisibleFlag")
}
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
}

View File

@@ -0,0 +1,146 @@
package main_test
import (
"fmt"
"reflect"
"testing"
main "github.com/urfave/cli/v2/cmd/urfave-cli-genflags"
)
func TestTypeName(t *testing.T) {
for _, tc := range []struct {
gt string
fc *main.FlagTypeConfig
expected string
}{
{gt: "int", fc: nil, expected: "IntFlag"},
{gt: "int", fc: &main.FlagTypeConfig{}, expected: "IntFlag"},
{gt: "int", fc: &main.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"},
{gt: "[]bool", fc: nil, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &main.FlagTypeConfig{}, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &main.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"},
{gt: "time.Rumination", fc: nil, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &main.FlagTypeConfig{}, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &main.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 := main.TypeName(tc.gt, tc.fc)
if tc.expected != actual {
ct.Errorf("expected %q, got %q", tc.expected, actual)
}
},
)
}
}
func TestSpec_SortedFlagTypes(t *testing.T) {
spec := &main.Spec{
FlagTypes: map[string]*main.FlagTypeConfig{
"nerf": &main.FlagTypeConfig{},
"gerf": nil,
},
}
actual := spec.SortedFlagTypes()
expected := []*main.FlagType{
{
GoType: "gerf",
Config: nil,
},
{
GoType: "nerf",
Config: &main.FlagTypeConfig{},
},
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("expected %#v, got %#v", expected, actual)
}
}
func genFlagType() *main.FlagType {
return &main.FlagType{
GoType: "blerf",
Config: &main.FlagTypeConfig{
SkipInterfaces: []string{"fmt.Stringer"},
StructFields: []*main.FlagStructField{
{
Name: "Foibles",
Type: "int",
},
{
Name: "Hoopled",
Type: "bool",
},
},
TypeName: "YeOldeBlerfFlag",
ValuePointer: true,
},
}
}
func TestFlagType_StructFields(t *testing.T) {
ft := genFlagType()
sf := ft.StructFields()
if 2 != len(sf) {
t.Errorf("expected 2 struct fields, got %v", len(sf))
return
}
if "Foibles" != sf[0].Name {
t.Errorf("expected struct field order to be retained")
}
}
func TestFlagType_ValuePointer(t *testing.T) {
ft := genFlagType()
if !ft.ValuePointer() {
t.Errorf("expected ValuePointer to be true")
return
}
ft.Config = nil
if ft.ValuePointer() {
t.Errorf("expected ValuePointer to be false")
}
}
func TestFlagType_GenerateFmtStringerInterface(t *testing.T) {
ft := genFlagType()
if ft.GenerateFmtStringerInterface() {
t.Errorf("expected GenerateFmtStringerInterface to be false")
return
}
ft.Config = nil
if !ft.GenerateFmtStringerInterface() {
t.Errorf("expected GenerateFmtStringerInterface to be true")
}
}
func TestFlagType_GenerateFlagInterface(t *testing.T) {
ft := genFlagType()
if !ft.GenerateFlagInterface() {
t.Errorf("expected GenerateFlagInterface to be true")
return
}
ft.Config = nil
if !ft.GenerateFlagInterface() {
t.Errorf("expected GenerateFlagInterface to be true")
}
}