Skip to content

Commit

Permalink
Support for regex for targetting. Added test coverage on the root com…
Browse files Browse the repository at this point in the history
…mand. Introduce abstractions that make it easier to test.
  • Loading branch information
csueiras committed Feb 17, 2021
1 parent 96ab6cd commit e405f5a
Show file tree
Hide file tree
Showing 23 changed files with 1,000 additions and 180 deletions.
38 changes: 38 additions & 0 deletions cmd/reinforcer/cmd/mocks/Executor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions cmd/reinforcer/cmd/mocks/Writer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

218 changes: 115 additions & 103 deletions cmd/reinforcer/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:generate mockery --all

// MIT License
//
// Copyright (c) 2021 Christian Sueiras
Expand Down Expand Up @@ -25,137 +27,154 @@ package cmd
import (
"fmt"
"github.com/csueiras/reinforcer/internal/generator"
"github.com/csueiras/reinforcer/internal/generator/executor"
"github.com/csueiras/reinforcer/internal/loader"
"github.com/csueiras/reinforcer/internal/writer"
"github.com/mitchellh/go-homedir"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"io/ioutil"
"github.com/spf13/viper"
"os"
"path"
"regexp"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)

// Version will be set in CI to the current released version
var Version = "0.0.0"

var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "reinforcer",
Short: "Generates the reinforced middleware code",
Long: `Reinforcer is a CLI tool that generates code from interfaces that
// Writer describes the code generator writer
type Writer interface {
Write(outputDirectory string, generated *generator.Generated) error
}

// Executor describes the code generator executor
type Executor interface {
Execute(settings *executor.Parameters) (*generator.Generated, error)
}

// DefaultRootCmd creates the default root command with its dependencies wired in
func DefaultRootCmd() *cobra.Command {
return NewRootCmd(executor.New(loader.DefaultLoader()), writer.Default())
}

// NewRootCmd creates the root command for reinforcer
func NewRootCmd(exec Executor, writ Writer) *cobra.Command {
rootCmd := &cobra.Command{
Use: "reinforcer",
Short: "Generates the reinforced middleware code",
Long: `Reinforcer is a CLI tool that generates code from interfaces that
will automatically inject middleware. Middlewares provide resiliency constructs
such as circuit breaker, retries, timeouts, etc.
`,
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
if showVersion, _ := flags.GetBool("version"); showVersion {
fmt.Println(Version)
return nil
}

src, _ := flags.GetString("src")
sourceTypeName, _ := cmd.Flags().GetString("name")
outPkg, _ := flags.GetString("outpkg")
outDir, _ := flags.GetString("outputdir")
ignoreNoRet, _ := flags.GetBool("ignorenoret")

if !path.IsAbs(outDir) {
cwd, err := os.Getwd()
if err != nil {
return err
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
if showVersion, _ := flags.GetBool("version"); showVersion {
fmt.Println(Version)
return nil
}
outDir = path.Join(cwd, path.Clean(outDir))
} else {
outDir = path.Clean(outDir)
}

if err := os.MkdirAll(outDir, 0755); err != nil {
return err
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

l := loader.DefaultLoader()
_, typ, err := l.Load(src, sourceTypeName)
if err != nil {
return err
}

code, err := generator.Generate(generator.Config{
OutPkg: outPkg,
IgnoreNoReturnMethods: ignoreNoRet,
Files: map[string]*generator.FileConfig{
src: {
SrcTypeName: sourceTypeName,
OutTypeName: sourceTypeName,
InterfaceType: typ,
},
},
})
if err != nil {
return err
}
debug, _ := flags.GetBool("debug")
silent, _ := flags.GetBool("silent")

if err := saveTo(path.Join(outDir, "reinforcer_common.go"), code.Common); err != nil {
return err
}

if err := saveTo(path.Join(outDir, "reinforcer_constants.go"), code.Constants); err != nil {
return err
}
// Default level for this example is info, unless debug flag is present (or logging is disabled)
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
if silent {
zerolog.SetGlobalLevel(zerolog.Disabled)
}

for _, codegen := range code.Files {
if err := saveTo(path.Join(outDir, toSnakeCase(codegen.TypeName)+".go"), codegen.Contents); err != nil {
sources, err := flags.GetStringSlice("src")
if err != nil {
return err
}
}
if len(sources) == 0 {
goFile := os.Getenv("GOFILE")
if goFile == "" {
return fmt.Errorf("no source provided")
}

defSrcFile, err := os.Getwd()
if err != nil {
return err
}
sources = append(sources, path.Join(defSrcFile, goFile))
}

return nil
},
}
targetAll, err := flags.GetBool("targetall")
if err != nil {
return err
}
targets, err := flags.GetStringSlice("target")
if err != nil {
return err
}
if len(targets) == 0 && !targetAll {
return fmt.Errorf("no targets provided")
}
outPkg, err := flags.GetString("outpkg")
if err != nil {
return err
}
outDir, err := flags.GetString("outputdir")
if err != nil {
return err
}
ignoreNoRet, err := flags.GetBool("ignorenoret")
if err != nil {
return err
}

func saveTo(path string, contents string) error {
if err := ioutil.WriteFile(path, []byte(contents), 0755); err != nil {
return fmt.Errorf("failed to write to %s; error=%w", path, err)
gen, err := exec.Execute(&executor.Parameters{
Sources: sources,
Targets: targets,
TargetsAll: targetAll,
OutPkg: outPkg,
IgnoreNoReturnMethods: ignoreNoRet,
})
if err != nil {
return fmt.Errorf("failed to generate code; error=%w", err)
}
if err := writ.Write(outDir, gen); err != nil {
return fmt.Errorf("failed to save generated code; error=%w", err)
}
return nil
},
}
return nil

rootCmd.PersistentFlags().
StringVar(&cfgFile, "config", "", "config file (default is $HOME/.reinforcer.yaml)")

flags := rootCmd.Flags()
flags.BoolP("version", "v", false, "show reinforcer's version")
flags.BoolP("debug", "d", false, "enables debug logs")
flags.BoolP("silent", "q", false, "disables logging. Mutually exclusive with the debug flag.")
flags.StringSliceP("src", "s", nil, "source files to scan for the target interface. If unspecified the file pointed by the env variable GOFILE will be used.")
flags.StringSliceP("target", "t", []string{}, "name of target type or regex to match interface names with")
flags.BoolP("targetall", "a", false, "codegen for all exported interfaces discovered. This option is mutually exclusive with the target option.")
flags.StringP("outputdir", "o", "./reinforced", "directory to write the generated code to")
flags.StringP("outpkg", "p", "reinforced", "name of generated package")
flags.BoolP("ignorenoret", "i", false, "ignores methods that don't return anything (they won't be wrapped in the middleware). By default they'll be wrapped in a middleware and if the middleware emits an error the call will panic.")

return rootCmd
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
if err := DefaultRootCmd().Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)

defSrcFile := ""
if goFile := os.Getenv("GOFILE"); goFile != "" {
var err error
defSrcFile, err = os.Getwd()
if err != nil {
panic(err)
}
defSrcFile = defSrcFile + "/" + goFile
}

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.reinforcer.yaml)")

flags := rootCmd.Flags()
flags.Bool("version", false, "show reinforcer's version")
flags.String("name", "", "name of interface to generate reinforcer's proxy for")
flags.String("src", defSrcFile, "source file to scan for the target interface. If unspecified the file pointed by the env variable GOFILE will be used.")
flags.String("outputdir", "./reinforced", "directory to write the generated code to")
flags.String("outpkg", "reinforced", "name of generated package")
flags.Bool("ignorenoret", false, "ignores methods that don't return anything (they won't be wrapped in the middleware). By default they'll be wrapped in a middleware and if the middleware emits an error the call will panic.")
}

// initConfig reads in config file and ENV variables if set.
Expand Down Expand Up @@ -183,10 +202,3 @@ func initConfig() {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

// Taken from: https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6
func toSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}
Loading

0 comments on commit e405f5a

Please sign in to comment.