From e405f5a4e0c2b7beb29544b6486c7af62d04e93d Mon Sep 17 00:00:00 2001 From: Christian Sueiras Date: Wed, 17 Feb 2021 08:25:49 -0500 Subject: [PATCH] Support for regex for targetting. Added test coverage on the root command. Introduce abstractions that make it easier to test. --- cmd/reinforcer/cmd/mocks/Executor.go | 38 +++ cmd/reinforcer/cmd/mocks/Writer.go | 27 +++ cmd/reinforcer/cmd/root.go | 218 +++++++++--------- cmd/reinforcer/cmd/root_test.go | 72 ++++++ example/client/client.go | 7 +- .../client/reinforced/reinforcer_constants.go | 7 + .../client/reinforced/some_other_client.go | 49 ++++ go.mod | 2 + go.sum | 29 +++ internal/generator/executor/executor.go | 81 +++++++ internal/generator/executor/executor_test.go | 40 ++++ internal/generator/executor/mocks/Loader.go | 60 +++++ internal/generator/generator.go | 26 +-- internal/generator/generator_test.go | 52 +---- internal/generator/method/method.go | 2 +- internal/loader/loader.go | 90 ++++++-- internal/loader/loader_test.go | 106 ++++++++- internal/tools.go | 7 + internal/writer/filename/filename.go | 32 +++ internal/writer/filename/filename_test.go | 38 +++ internal/writer/io/io.go | 77 +++++++ internal/writer/writer.go | 61 +++++ internal/writer/writer_test.go | 59 +++++ 23 files changed, 1000 insertions(+), 180 deletions(-) create mode 100644 cmd/reinforcer/cmd/mocks/Executor.go create mode 100644 cmd/reinforcer/cmd/mocks/Writer.go create mode 100644 cmd/reinforcer/cmd/root_test.go mode change 100644 => 100755 example/client/reinforced/reinforcer_constants.go create mode 100755 example/client/reinforced/some_other_client.go create mode 100644 internal/generator/executor/executor.go create mode 100644 internal/generator/executor/executor_test.go create mode 100644 internal/generator/executor/mocks/Loader.go create mode 100644 internal/tools.go create mode 100644 internal/writer/filename/filename.go create mode 100644 internal/writer/filename/filename_test.go create mode 100644 internal/writer/io/io.go create mode 100644 internal/writer/writer.go create mode 100644 internal/writer/writer_test.go diff --git a/cmd/reinforcer/cmd/mocks/Executor.go b/cmd/reinforcer/cmd/mocks/Executor.go new file mode 100644 index 0000000..af539e2 --- /dev/null +++ b/cmd/reinforcer/cmd/mocks/Executor.go @@ -0,0 +1,38 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + generator "github.com/csueiras/reinforcer/internal/generator" + executor "github.com/csueiras/reinforcer/internal/generator/executor" + + mock "github.com/stretchr/testify/mock" +) + +// Executor is an autogenerated mock type for the Executor type +type Executor struct { + mock.Mock +} + +// Execute provides a mock function with given fields: settings +func (_m *Executor) Execute(settings *executor.Parameters) (*generator.Generated, error) { + ret := _m.Called(settings) + + var r0 *generator.Generated + if rf, ok := ret.Get(0).(func(*executor.Parameters) *generator.Generated); ok { + r0 = rf(settings) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*generator.Generated) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*executor.Parameters) error); ok { + r1 = rf(settings) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/cmd/reinforcer/cmd/mocks/Writer.go b/cmd/reinforcer/cmd/mocks/Writer.go new file mode 100644 index 0000000..afecaf5 --- /dev/null +++ b/cmd/reinforcer/cmd/mocks/Writer.go @@ -0,0 +1,27 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + generator "github.com/csueiras/reinforcer/internal/generator" + mock "github.com/stretchr/testify/mock" +) + +// Writer is an autogenerated mock type for the Writer type +type Writer struct { + mock.Mock +} + +// Write provides a mock function with given fields: outputDirectory, generated +func (_m *Writer) Write(outputDirectory string, generated *generator.Generated) error { + ret := _m.Called(outputDirectory, generated) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *generator.Generated) error); ok { + r0 = rf(outputDirectory, generated) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/cmd/reinforcer/cmd/root.go b/cmd/reinforcer/cmd/root.go index 4a86f89..0f9ef4d 100644 --- a/cmd/reinforcer/cmd/root.go +++ b/cmd/reinforcer/cmd/root.go @@ -1,3 +1,5 @@ +//go:generate mockery --all + // MIT License // // Copyright (c) 2021 Christian Sueiras @@ -25,110 +27,147 @@ 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) } @@ -136,26 +175,6 @@ func Execute() { 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. @@ -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) -} diff --git a/cmd/reinforcer/cmd/root_test.go b/cmd/reinforcer/cmd/root_test.go new file mode 100644 index 0000000..f179978 --- /dev/null +++ b/cmd/reinforcer/cmd/root_test.go @@ -0,0 +1,72 @@ +package cmd_test + +import ( + "bytes" + "github.com/csueiras/reinforcer/cmd/reinforcer/cmd" + "github.com/csueiras/reinforcer/cmd/reinforcer/cmd/mocks" + "github.com/csueiras/reinforcer/internal/generator" + "github.com/csueiras/reinforcer/internal/generator/executor" + "github.com/stretchr/testify/require" + "testing" +) + +func TestRootCommand(t *testing.T) { + gen := &generator.Generated{} + + t.Run("Provide Targets", func(t *testing.T) { + exec := &mocks.Executor{} + exec.On("Execute", &executor.Parameters{ + Sources: []string{"/path/to/target.go"}, + Targets: []string{"Client", "SomeOtherClient"}, + TargetsAll: false, + OutPkg: "reinforced", + IgnoreNoReturnMethods: false, + }).Return(gen, nil) + writ := &mocks.Writer{} + writ.On("Write", "./reinforced", gen).Return(nil) + + b := bytes.NewBufferString("") + c := cmd.NewRootCmd(exec, writ) + c.SetOut(b) + c.SetArgs([]string{"--src=/path/to/target.go", "--target=Client", "--target=SomeOtherClient", "--outputdir=./reinforced"}) + require.NoError(t, c.Execute()) + }) + + t.Run("Target All", func(t *testing.T) { + exec := &mocks.Executor{} + exec.On("Execute", &executor.Parameters{ + Sources: []string{"/path/to/target.go"}, + Targets: []string{}, + TargetsAll: true, + OutPkg: "reinforced", + IgnoreNoReturnMethods: false, + }).Return(gen, nil) + writ := &mocks.Writer{} + writ.On("Write", "./reinforced", gen).Return(nil) + + b := bytes.NewBufferString("") + c := cmd.NewRootCmd(exec, writ) + c.SetOut(b) + c.SetArgs([]string{"--src=/path/to/target.go", "--targetall", "--outputdir=./reinforced"}) + require.NoError(t, c.Execute()) + }) + + t.Run("Ignore No Return Methods", func(t *testing.T) { + exec := &mocks.Executor{} + exec.On("Execute", &executor.Parameters{ + Sources: []string{"/path/to/target.go"}, + Targets: []string{"Client", "SomeOtherClient"}, + TargetsAll: false, + OutPkg: "reinforced", + IgnoreNoReturnMethods: true, + }).Return(gen, nil) + writ := &mocks.Writer{} + writ.On("Write", "./reinforced", gen).Return(nil) + + b := bytes.NewBufferString("") + c := cmd.NewRootCmd(exec, writ) + c.SetOut(b) + c.SetArgs([]string{"--src=/path/to/target.go", "--target=Client", "--target=SomeOtherClient", "--outputdir=./reinforced", "--ignorenoret"}) + require.NoError(t, c.Execute()) + }) +} diff --git a/example/client/client.go b/example/client/client.go index a5bc5f6..63e9e3f 100644 --- a/example/client/client.go +++ b/example/client/client.go @@ -1,4 +1,4 @@ -//go:generate reinforcer --name=Client --outputdir=./reinforced +//go:generate reinforcer --debug --target=Client --target=SomeOtherClient --outputdir=./reinforced package client @@ -14,6 +14,11 @@ type Client interface { GenerateGreeting(ctx context.Context, name string) (string, error) } +// SomeOtherClient is another example service interface that can be targeted +type SomeOtherClient interface { + DoStuff() error +} + // FakeClient is a Client implementation that will randomly fail type FakeClient struct { } diff --git a/example/client/reinforced/reinforcer_constants.go b/example/client/reinforced/reinforcer_constants.go old mode 100644 new mode 100755 index 46d5e70..c0b147b --- a/example/client/reinforced/reinforcer_constants.go +++ b/example/client/reinforced/reinforcer_constants.go @@ -10,3 +10,10 @@ var ClientMethods = struct { GenerateGreeting: "GenerateGreeting", SayHello: "SayHello", } + +// SomeOtherClientMethods are the methods in SomeOtherClient +var SomeOtherClientMethods = struct { + DoStuff string +}{ + DoStuff: "DoStuff", +} diff --git a/example/client/reinforced/some_other_client.go b/example/client/reinforced/some_other_client.go new file mode 100755 index 0000000..83a5bf6 --- /dev/null +++ b/example/client/reinforced/some_other_client.go @@ -0,0 +1,49 @@ +// Code generated by reinforcer, DO NOT EDIT. + +package reinforced + +import "context" + +type targetSomeOtherClient interface { + DoStuff() error +} +type SomeOtherClient struct { + *base + delegate targetSomeOtherClient +} + +func NewSomeOtherClient(delegate targetSomeOtherClient, runnerFactory runnerFactory, options ...Option) *SomeOtherClient { + if delegate == nil { + panic("provided nil delegate") + } + if runnerFactory == nil { + panic("provided nil runner factory") + } + c := &SomeOtherClient{ + base: &base{ + errorPredicate: RetryAllErrors, + runnerFactory: runnerFactory, + }, + delegate: delegate, + } + for _, o := range options { + o(c.base) + } + return c +} +func (s *SomeOtherClient) DoStuff() error { + var nonRetryableErr error + err := s.run(context.Background(), SomeOtherClientMethods.DoStuff, func(_ context.Context) error { + var err error + err = s.delegate.DoStuff() + if s.errorPredicate(SomeOtherClientMethods.DoStuff, err) { + return err + } + nonRetryableErr = err + return nil + }) + if nonRetryableErr != nil { + return nonRetryableErr + } + return err +} diff --git a/go.mod b/go.mod index b94f356..5eb3c79 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ go 1.15 require ( github.com/dave/jennifer v1.4.1 github.com/mitchellh/go-homedir v1.1.0 + github.com/rs/zerolog v1.20.0 github.com/slok/goresilience v0.2.0 github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 + github.com/vektra/mockery/v2 v2.6.0 golang.org/x/tools v0.1.0 ) diff --git a/go.sum b/go.sum index acdbc26..cbaac2a 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= @@ -29,7 +30,9 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -75,6 +78,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -117,6 +121,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -142,6 +147,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -168,6 +174,10 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -185,6 +195,7 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= @@ -192,12 +203,14 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -206,8 +219,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/vektra/mockery/v2 v2.6.0 h1:yVqYLShwENx8CB3IR3Jl2mC4mDfgVK9YaBP3tjYAxtI= +github.com/vektra/mockery/v2 v2.6.0/go.mod h1:rBZUbbhMbiSX1WlCGsOgAi6xjuJGxB7KKbnoL0XNYW8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -220,6 +239,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -240,6 +260,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -254,8 +275,10 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -265,6 +288,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -305,14 +329,17 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -335,6 +362,7 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -347,6 +375,7 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/generator/executor/executor.go b/internal/generator/executor/executor.go new file mode 100644 index 0000000..5f75992 --- /dev/null +++ b/internal/generator/executor/executor.go @@ -0,0 +1,81 @@ +//go:generate mockery --all + +package executor + +import ( + "fmt" + "github.com/csueiras/reinforcer/internal/generator" + "go/types" +) + +// Loader describes the loader component +type Loader interface { + LoadAll(path string) (map[string]*types.Interface, error) + LoadMatched(path string, expressions []string) (map[string]*types.Interface, error) +} + +// Parameters are the input parameters for the executor +type Parameters struct { + // Sources are the paths to the packages that are eligible for targetting + Sources []string + // Targets contains the target types to search for, these are expressions that may contain RegEx + Targets []string + // TargetsAll enables targeting of every exported interface type + TargetsAll bool + // OutPkg the package name for the output code + OutPkg string + // IgnoreNoReturnMethods disables proxying of methods that don't return anything + IgnoreNoReturnMethods bool +} + +// Executor is a utility service to orchestrate code generation +type Executor struct { + loader Loader +} + +// New creates an instance of the executor with the given type loader +func New(l Loader) *Executor { + return &Executor{loader: l} +} + +// Execute orchestrates code generation sourced from multiple files/targets +func (e *Executor) Execute(settings *Parameters) (*generator.Generated, error) { + results := make(map[string]*types.Interface) + var cfg []*generator.FileConfig + var err error + for _, source := range settings.Sources { + var match map[string]*types.Interface + if settings.TargetsAll { + match, err = e.loader.LoadAll(source) + } else { + match, err = e.loader.LoadMatched(source, settings.Targets) + } + if err != nil { + return nil, err + } + + // Check types aren't repeated before adding them to the generator's config + for typName, typ := range match { + if _, ok := results[typName]; ok { + return nil, fmt.Errorf("multiple types with same name discovered with name %s", typName) + } + results[typName] = typ + + cfg = append(cfg, &generator.FileConfig{ + SrcTypeName: typName, + OutTypeName: typName, + InterfaceType: typ, + }) + } + } + + code, err := generator.Generate(generator.Config{ + OutPkg: settings.OutPkg, + IgnoreNoReturnMethods: settings.IgnoreNoReturnMethods, + Files: cfg, + }) + if err != nil { + return nil, err + } + return code, nil +} diff --git a/internal/generator/executor/executor_test.go b/internal/generator/executor/executor_test.go new file mode 100644 index 0000000..5d2703f --- /dev/null +++ b/internal/generator/executor/executor_test.go @@ -0,0 +1,40 @@ +package executor_test + +import ( + "github.com/csueiras/reinforcer/internal/generator/executor" + "github.com/csueiras/reinforcer/internal/generator/executor/mocks" + "github.com/stretchr/testify/require" + "go/token" + "go/types" + "testing" +) + +func TestExecutor_Execute(t *testing.T) { + l := &mocks.Loader{} + l.On("LoadMatched", "github.com/csueiras/reinforcer/pkg/testpkg", []string{"MyService"}).Return( + map[string]*types.Interface{ + "LockService": createTestInterfaceType(), + }, nil, + ) + + exec := executor.New(l) + got, err := exec.Execute(&executor.Parameters{ + Sources: []string{"github.com/csueiras/reinforcer/pkg/testpkg"}, + Targets: []string{"MyService"}, + OutPkg: "testpkg", + IgnoreNoReturnMethods: false, + }) + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, 1, len(got.Files)) + require.Equal(t, "LockService", got.Files[0].TypeName) +} + +func createTestInterfaceType() *types.Interface { + nullary := types.NewSignature(nil, nil, nil, false) // func() + methods := []*types.Func{ + types.NewFunc(token.NoPos, nil, "Lock", nullary), + types.NewFunc(token.NoPos, nil, "Unlock", nullary), + } + return types.NewInterfaceType(methods, nil).Complete() +} diff --git a/internal/generator/executor/mocks/Loader.go b/internal/generator/executor/mocks/Loader.go new file mode 100644 index 0000000..8d5f70f --- /dev/null +++ b/internal/generator/executor/mocks/Loader.go @@ -0,0 +1,60 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + types "go/types" + + mock "github.com/stretchr/testify/mock" +) + +// Loader is an autogenerated mock type for the Loader type +type Loader struct { + mock.Mock +} + +// LoadAll provides a mock function with given fields: path +func (_m *Loader) LoadAll(path string) (map[string]*types.Interface, error) { + ret := _m.Called(path) + + var r0 map[string]*types.Interface + if rf, ok := ret.Get(0).(func(string) map[string]*types.Interface); ok { + r0 = rf(path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*types.Interface) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoadMatched provides a mock function with given fields: path, expressions +func (_m *Loader) LoadMatched(path string, expressions []string) (map[string]*types.Interface, error) { + ret := _m.Called(path, expressions) + + var r0 map[string]*types.Interface + if rf, ok := ret.Get(0).(func(string, []string) map[string]*types.Interface); ok { + r0 = rf(path, expressions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*types.Interface) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, []string) error); ok { + r1 = rf(path, expressions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/internal/generator/generator.go b/internal/generator/generator.go index a90a5f1..a540559 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -8,13 +8,11 @@ import ( "github.com/csueiras/reinforcer/internal/generator/passthrough" "github.com/csueiras/reinforcer/internal/generator/retryable" "github.com/dave/jennifer/jen" + "github.com/rs/zerolog/log" "go/types" "strings" ) -// ErrNoRetryableMethodsFound is the error used whenever a configuration contains non-retryable methods, that is methods -// that do not return errors -var ErrNoRetryableMethodsFound = fmt.Errorf("no methods returning errors were found in the target") var fileHeader = "Code generated by reinforcer, DO NOT EDIT." // FileConfig holds the code generation configuration for a specific type @@ -40,7 +38,7 @@ type Config struct { // OutPkg holds the name of the output package OutPkg string // Files holds the code generation configuration for every file being processed - Files map[string]*FileConfig + Files []*FileConfig // IgnoreNoReturnMethods determines whether methods that don't return anything should be wrapped in the middleware or not. IgnoreNoReturnMethods bool } @@ -58,7 +56,6 @@ type statement interface { } type fileMeta struct { - fileName string fileConfig *FileConfig methods []*method.Method } @@ -70,7 +67,7 @@ type Generated struct { // Constants is the golang code that holds constants for compile-time safe references to the proxied methods Constants string // Files is the golang code that was generated for every type that was processed - Files map[string]*GeneratedFile + Files []*GeneratedFile } // Generate processes the given configuration and performs reinforcer's code generation @@ -86,12 +83,11 @@ func Generate(cfg Config) (*Generated, error) { gen := &Generated{ Common: c, - Files: make(map[string]*GeneratedFile), } var fileMethods []*fileMeta - for fileName, fileConfig := range cfg.Files { + for _, fileConfig := range cfg.Files { methods, err := parseMethods(fileConfig.OutTypeName, fileConfig.InterfaceType) if err != nil { return nil, err @@ -100,11 +96,11 @@ func Generate(cfg Config) (*Generated, error) { if err != nil { return nil, err } - gen.Files[fileName] = &GeneratedFile{ + gen.Files = append(gen.Files, &GeneratedFile{ TypeName: fileConfig.OutTypeName, Contents: s, - } - fileMethods = append(fileMethods, &fileMeta{fileName: fileName, fileConfig: fileConfig, methods: methods}) + }) + fileMethods = append(fileMethods, &fileMeta{fileConfig: fileConfig, methods: methods}) } consts, err := generateConstants(cfg.OutPkg, fileMethods) @@ -249,6 +245,7 @@ func generateConstants(outPkg string, meta []*fileMeta) (string, error) { } constObjName := fmt.Sprintf("%sMethods", fm.fileConfig.OutTypeName) + log.Debug().Msgf("Adding constants for type %s", fm.fileConfig.OutTypeName) f.Add(jen.Comment(fmt.Sprintf("%s are the methods in %s", constObjName, fm.fileConfig.OutTypeName))) f.Add( jen.Var().Id(constObjName).Op("=").Struct( @@ -263,7 +260,6 @@ func generateConstants(outPkg string, meta []*fileMeta) (string, error) { } func parseMethods(typeName string, interfaceType *types.Interface) ([]*method.Method, error) { - anyErrorReturningMethod := false var methods []*method.Method for m := 0; m < interfaceType.NumMethods(); m++ { meth := interfaceType.Method(m) @@ -271,14 +267,8 @@ func parseMethods(typeName string, interfaceType *types.Interface) ([]*method.Me if err != nil { return nil, err } - if mm.ReturnsError { - anyErrorReturningMethod = true - } methods = append(methods, mm) } - if !anyErrorReturningMethod { - return nil, ErrNoRetryableMethodsFound - } return methods, nil } diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go index 7a781a5..3184256 100644 --- a/internal/generator/generator_test.go +++ b/internal/generator/generator_test.go @@ -23,24 +23,6 @@ func TestGenerator_Generate(t *testing.T) { outCode *generator.Generated wantErr bool }{ - { - name: "Interface without retryable methods", - ignoreNoReturnMethods: false, - inputs: map[string]input{ - "service.go": { - interfaceName: "Service", - code: `package fake - -import goctx "context" - -type Service interface { - A(ctx goctx.Context) -} -`, - }, - }, - wantErr: true, - }, { name: "Using aliased import", ignoreNoReturnMethods: false, @@ -101,8 +83,8 @@ var GeneratedServiceMethods = struct { A: "A", } `, - Files: map[string]*generator.GeneratedFile{ - "my_service.go": { + Files: []*generator.GeneratedFile{ + { TypeName: "GeneratedService", Contents: `// Code generated by reinforcer, DO NOT EDIT. @@ -236,8 +218,8 @@ var GeneratedServiceMethods = struct { HasVariadic: "HasVariadic", } `, - Files: map[string]*generator.GeneratedFile{ - "users_service.go": { + Files: []*generator.GeneratedFile{ + { TypeName: "GeneratedService", Contents: `// Code generated by reinforcer, DO NOT EDIT. @@ -428,8 +410,8 @@ var GeneratedServiceMethods = struct { B: "B", } `, - Files: map[string]*generator.GeneratedFile{ - "users_service.go": { + Files: []*generator.GeneratedFile{ + { TypeName: "GeneratedService", Contents: `// Code generated by reinforcer, DO NOT EDIT. @@ -508,15 +490,6 @@ func (g *GeneratedService) B(ctx context.Context, arg1 string) (string, error) { require.NoError(t, err) require.NotNil(t, got) - var expected []string - for fileName := range tt.outCode.Files { - expected = append(expected, fileName) - } - var files []string - for fileName := range got.Files { - files = append(files, fileName) - } - require.ElementsMatch(t, expected, files) require.Equal(t, tt.outCode.Constants, got.Constants) require.Equal(t, tt.outCode.Common, got.Common) @@ -530,7 +503,7 @@ func (g *GeneratedService) B(ctx context.Context, arg1 string) (string, error) { } } -func loadInterface(t *testing.T, filesCode map[string]input) map[string]*generator.FileConfig { +func loadInterface(t *testing.T, filesCode map[string]input) []*generator.FileConfig { pkg := "github.com/csueiras/fake/unresilient" m := map[string]interface{}{} for fileName, in := range filesCode { @@ -551,16 +524,15 @@ func loadInterface(t *testing.T, filesCode map[string]input) map[string]*generat return packages.Load(exported.Config, patterns...) }) - loadedTypes := make(map[string]*generator.FileConfig) - for fileName, in := range filesCode { - - _, svc, err := l.Load(pkg, in.interfaceName) + var loadedTypes []*generator.FileConfig + for _, in := range filesCode { + svc, err := l.LoadOne(pkg, in.interfaceName) require.NoError(t, err) - loadedTypes[fileName] = &generator.FileConfig{ + loadedTypes = append(loadedTypes, &generator.FileConfig{ SrcTypeName: in.interfaceName, OutTypeName: fmt.Sprintf("Generated%s", in.interfaceName), InterfaceType: svc, - } + }) } return loadedTypes } diff --git a/internal/generator/method/method.go b/internal/generator/method/method.go index 7a94887..398a69a 100644 --- a/internal/generator/method/method.go +++ b/internal/generator/method/method.go @@ -35,7 +35,7 @@ func init() { errType.Complete() ErrType = types.NewNamed(types.NewTypeName(0, nil, "error", nil), errType, nil) - _, iface, err := loader.DefaultLoader().Load("context", "Context") + iface, err := loader.DefaultLoader().LoadOne("context", "Context") if err != nil { panic(err) } diff --git a/internal/loader/loader.go b/internal/loader/loader.go index 07df792..66bd01e 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -2,11 +2,15 @@ package loader import ( "fmt" + "github.com/rs/zerolog/log" "go/types" "golang.org/x/tools/go/packages" + "regexp" "strings" ) +const regexChars = "\\.+*?()|[]{}^$" + // LoadingError holds any errors that occurred while loading a package type LoadingError struct { Errors []error @@ -41,8 +45,48 @@ func NewLoader(pkgLoader func(cfg *packages.Config, patterns ...string) ([]*pack } } -// Load loads the package in path and extracts out the interface type by the name of targetTypeName -func (l *Loader) Load(path, targetTypeName string) (*packages.Package, *types.Interface, error) { +// LoadOne loads the given type +func (l *Loader) LoadOne(path, name string) (*types.Interface, error) { + results, err := l.LoadMatched(path, []string{fmt.Sprintf(`\b%s\b`, name)}) + if err != nil { + return nil, err + } + if len(results) > 1 { + // This should technically be impossible + return nil, fmt.Errorf("multiple interfaces with name %s found", name) + } + for _, typ := range results { + return typ, nil + } + return nil, fmt.Errorf("%s not found", name) +} + +// LoadAll loads all types discovered in the path that are interface types +func (l *Loader) LoadAll(path string) (map[string]*types.Interface, error) { + return l.LoadMatched(path, []string{".*"}) +} + +// LoadMatched loads types that match the given expressions, the expressions can be regex or strings to be exact-matched +func (l *Loader) LoadMatched(path string, expressions []string) (map[string]*types.Interface, error) { + results := make(map[string]*types.Interface) + + filter, err := exprToFilter(expressions) + if err != nil { + return nil, err + } + + _, interfaces, err := l.loadExpr(path, filter) + if err != nil { + return nil, err + } + + for name, interfaceType := range interfaces { + results[name] = interfaceType + } + return results, nil +} + +func (l *Loader) loadExpr(path string, expr *regexp.Regexp) (*packages.Package, map[string]*types.Interface, error) { cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedImports} pkgs, err := l.loaderFn(cfg, path) @@ -55,20 +99,24 @@ func (l *Loader) Load(path, targetTypeName string) (*packages.Package, *types.In } pkg := pkgs[0] - obj := pkg.Types.Scope().Lookup(targetTypeName) - if obj == nil { - return nil, nil, fmt.Errorf("%s not found in declared types of %s", targetTypeName, pkg) - } - - if _, ok := obj.(*types.TypeName); !ok { - return nil, nil, fmt.Errorf("%v is not a named type", obj) - } - - interfaceType, ok := obj.Type().Underlying().(*types.Interface) - if !ok { - return nil, nil, fmt.Errorf("type %v is not an Interface", obj) + typesFound := pkg.Types.Scope().Names() + results := make(map[string]*types.Interface) + for _, typeFound := range typesFound { + if expr.MatchString(typeFound) { + obj := pkg.Types.Scope().Lookup(typeFound) + if obj == nil { + return nil, nil, fmt.Errorf("%s not found in declared types of %s", typeFound, pkg) + } + interfaceType, ok := obj.Type().Underlying().(*types.Interface) + if !ok { + log.Debug().Msgf("Ignoring matching type %s because it is not an interface type", typeFound) + continue + } + log.Info().Msgf("Discovered type %s", typeFound) + results[typeFound] = interfaceType + } } - return pkg, interfaceType, nil + return pkg, results, nil } func extractPackageErrors(pkgs []*packages.Package) error { @@ -85,3 +133,15 @@ func extractPackageErrors(pkgs []*packages.Package) error { } return nil } + +func exprToFilter(expressions []string) (*regexp.Regexp, error) { + expression := strings.Join(expressions, "|") + if strings.ContainsAny(expression, regexChars) { + filter, err := regexp.Compile(expression) + if err != nil { + return nil, fmt.Errorf("failed to compile expression %q; error=%w", expression, err) + } + return filter, nil + } + return regexp.MustCompile(fmt.Sprintf("\\b%s\\b", expression)), nil +} diff --git a/internal/loader/loader_test.go b/internal/loader/loader_test.go index 900fde2..963f841 100644 --- a/internal/loader/loader_test.go +++ b/internal/loader/loader_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" + "testing" ) @@ -28,9 +29,110 @@ type Service interface { return packages.Load(exported.Config, patterns...) }) - pkg, svc, err := l.Load("github.com/csueiras/fake", "Service") + svc, err := l.LoadOne("github.com/csueiras/fake", "Service") require.NoError(t, err) - require.NotNil(t, pkg) require.NotNil(t, svc) require.Equal(t, "interface{GetUserID(ctx context.Context, userID string) (string, error)}", svc.String()) } + +func TestLoadMatched(t *testing.T) { + exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ + Name: "github.com/csueiras", + Files: map[string]interface{}{ + "fake/fake.go": `package fake + +import "context" + +type UserService interface { + GetUserID(ctx context.Context, userID string) (string, error) +} + +type HelloWorldService interface { + Hello(ctx context.Context, name string) error +} + +type unexportedService interface { + ShouldNotBeSeen() +} + +type NotAnInterface struct { + SomeField string +} +`, + }}}) + t.Cleanup(exported.Cleanup) + + t.Run("RegEx", func(t *testing.T) { + l := loader.NewLoader(func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + exported.Config.Mode = cfg.Mode + return packages.Load(exported.Config, patterns...) + }) + + results, err := l.LoadMatched("github.com/csueiras/fake", []string{".*Service"}) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, 2, len(results)) + require.NotNil(t, results["UserService"]) + require.Equal(t, "interface{GetUserID(ctx context.Context, userID string) (string, error)}", results["UserService"].String()) + require.NotNil(t, results["HelloWorldService"]) + require.Equal(t, "interface{Hello(ctx context.Context, name string) error}", results["HelloWorldService"].String()) + }) + + t.Run("Multiple RegEx Expressions", func(t *testing.T) { + l := loader.NewLoader(func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + exported.Config.Mode = cfg.Mode + return packages.Load(exported.Config, patterns...) + }) + + results, err := l.LoadMatched("github.com/csueiras/fake", []string{"User.*", "Hello.*Service"}) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, 2, len(results)) + require.NotNil(t, results["UserService"]) + require.Equal(t, "interface{GetUserID(ctx context.Context, userID string) (string, error)}", results["UserService"].String()) + require.NotNil(t, results["HelloWorldService"]) + require.Equal(t, "interface{Hello(ctx context.Context, name string) error}", results["HelloWorldService"].String()) + }) + + t.Run("Exact Match", func(t *testing.T) { + l := loader.NewLoader(func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + exported.Config.Mode = cfg.Mode + return packages.Load(exported.Config, patterns...) + }) + + results, err := l.LoadMatched("github.com/csueiras/fake", []string{"HelloWorldService"}) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, 1, len(results)) + require.NotNil(t, results["HelloWorldService"]) + require.Equal(t, "interface{Hello(ctx context.Context, name string) error}", results["HelloWorldService"].String()) + }) + + t.Run("Exact Match: No Match", func(t *testing.T) { + l := loader.NewLoader(func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + exported.Config.Mode = cfg.Mode + return packages.Load(exported.Config, patterns...) + }) + + results, err := l.LoadMatched("github.com/csueiras/fake", []string{"Hello"}) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, 0, len(results)) + }) + + t.Run("Multiple Exact Matches", func(t *testing.T) { + l := loader.NewLoader(func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + exported.Config.Mode = cfg.Mode + return packages.Load(exported.Config, patterns...) + }) + + results, err := l.LoadMatched("github.com/csueiras/fake", []string{"UserService", "HelloWorldService", "NotAnInterface"}) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, 2, len(results)) + require.NotNil(t, results["UserService"]) + require.Equal(t, "interface{GetUserID(ctx context.Context, userID string) (string, error)}", results["UserService"].String()) + require.NotNil(t, results["HelloWorldService"]) + require.Equal(t, "interface{Hello(ctx context.Context, name string) error}", results["HelloWorldService"].String()) + }) +} diff --git a/internal/tools.go b/internal/tools.go new file mode 100644 index 0000000..2526c4b --- /dev/null +++ b/internal/tools.go @@ -0,0 +1,7 @@ +// +build tools + +package internal + +import ( + _ "github.com/vektra/mockery/v2" +) diff --git a/internal/writer/filename/filename.go b/internal/writer/filename/filename.go new file mode 100644 index 0000000..5c0689e --- /dev/null +++ b/internal/writer/filename/filename.go @@ -0,0 +1,32 @@ +package filename + +import ( + "regexp" + "strings" +) + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +// Strategy is a file naming strategy for generating names of files where a type should be code gen into +type Strategy interface { + // GenerateFileName generates the filename (without an extension) for the given type name + GenerateFileName(typeName string) string +} + +// snakeCaseFileNameStrategy is a file naming strategy that snake-cases the type name +type snakeCaseFileNameStrategy struct { +} + +// GenerateFileName generates the filename (without an extension) in snake case +func (s *snakeCaseFileNameStrategy) GenerateFileName(typeName string) string { + // Taken from: https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6 + snake := matchFirstCap.ReplaceAllString(typeName, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToLower(snake) +} + +// SnakeCaseStrategy is a file naming strategy that uses snake case +func SnakeCaseStrategy() Strategy { + return &snakeCaseFileNameStrategy{} +} diff --git a/internal/writer/filename/filename_test.go b/internal/writer/filename/filename_test.go new file mode 100644 index 0000000..a548b5a --- /dev/null +++ b/internal/writer/filename/filename_test.go @@ -0,0 +1,38 @@ +package filename_test + +import ( + "github.com/csueiras/reinforcer/internal/writer/filename" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSnakeCaseFileNameStrategy_GenerateFileName(t *testing.T) { + tests := []struct { + name string + typeName string + want string + }{ + { + name: "Default", + typeName: "HelloWorldService", + want: "hello_world_service", + }, + { + name: "All Caps", + typeName: "HTML", + want: "html", + }, + { + name: "Abbrv. all caps and multiple words", + typeName: "SQSEventHandler", + want: "sqs_event_handler", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := filename.SnakeCaseStrategy() + got := s.GenerateFileName(tt.typeName) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/writer/io/io.go b/internal/writer/io/io.go new file mode 100644 index 0000000..da8ce5f --- /dev/null +++ b/internal/writer/io/io.go @@ -0,0 +1,77 @@ +package io + +import ( + "bytes" + "fmt" + "io" + "os" + "path" +) + +// OutputProvider provides the means to write to the underlying storage medium such as a file system or an in-memory store +type OutputProvider interface { + GetOutputTarget(filename string) (io.WriteCloser, error) +} + +// FSOutputProvider is an OutputProvider that creates writers for the file system +type FSOutputProvider struct { +} + +// NewFSOutputProvider creates an OutputProvider that creates writers for the file system +func NewFSOutputProvider() *FSOutputProvider { + return &FSOutputProvider{} +} + +// GetOutputTarget creates a local filesystem writer for the given filename, it will pre-create any directories that are +// in the filename's path +func (F *FSOutputProvider) GetOutputTarget(filename string) (io.WriteCloser, error) { + dir := path.Dir(filename) + if !path.IsAbs(dir) { + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + dir = path.Join(cwd, path.Clean(dir)) + } else { + dir = path.Clean(dir) + } + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + filename = path.Base(filename) + fullPath := path.Join(dir, filename) + f, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return nil, fmt.Errorf("failed to open file %s; error=%w", fullPath, err) + } + return f, nil +} + +type nopCloser struct { + w io.Writer +} + +func (f *nopCloser) Write(p []byte) (n int, err error) { + return f.w.Write(p) +} + +func (f *nopCloser) Close() error { + return nil +} + +// BufferOutputProvider is an OutputProvider that creates writers backed by bytes.Buffer +type BufferOutputProvider struct { + Buffers map[string]*bytes.Buffer +} + +// NewBufferOutputProvider is a constructor for the BufferOutputProvider +func NewBufferOutputProvider() *BufferOutputProvider { + return &BufferOutputProvider{Buffers: map[string]*bytes.Buffer{}} +} + +// GetOutputTarget creates a writer for an in memory bytes.Buffer uniquely identified by the given target argument +func (b *BufferOutputProvider) GetOutputTarget(target string) (io.WriteCloser, error) { + b.Buffers[target] = &bytes.Buffer{} + return &nopCloser{w: b.Buffers[target]}, nil +} diff --git a/internal/writer/writer.go b/internal/writer/writer.go new file mode 100644 index 0000000..cab2643 --- /dev/null +++ b/internal/writer/writer.go @@ -0,0 +1,61 @@ +package writer + +import ( + "github.com/csueiras/reinforcer/internal/generator" + "github.com/csueiras/reinforcer/internal/writer/filename" + wio "github.com/csueiras/reinforcer/internal/writer/io" + "path" +) + +// Writer is responsible for unloading the generated code into the output +type Writer struct { + fileNameStrategy filename.Strategy + outputProvider wio.OutputProvider +} + +// New is a constructor for Writer +func New(outputProvider wio.OutputProvider, fileNameStrategy filename.Strategy) *Writer { + return &Writer{ + fileNameStrategy: fileNameStrategy, + outputProvider: outputProvider, + } +} + +// Default creates the default provider that writes to the local file system and uses snake case file naming strategy +func Default() *Writer { + return New(wio.NewFSOutputProvider(), filename.SnakeCaseStrategy()) +} + +// Write saves the generated contents to the given output location +func (w *Writer) Write(outputDirectory string, generated *generator.Generated) error { + if err := w.writeTo(path.Join(outputDirectory, "reinforcer_common.go"), generated.Common); err != nil { + return err + } + + if err := w.writeTo(path.Join(outputDirectory, "reinforcer_constants.go"), generated.Constants); err != nil { + return err + } + + for _, codegen := range generated.Files { + filePath := path.Join(outputDirectory, w.fileNameStrategy.GenerateFileName(codegen.TypeName)+".go") + if err := w.writeTo(filePath, codegen.Contents); err != nil { + return err + } + } + + return nil +} + +func (w *Writer) writeTo(target string, contents string) error { + writeTarget, err := w.outputProvider.GetOutputTarget(target) + if err != nil { + return err + } + defer func() { + _ = writeTarget.Close() + }() + if _, err = writeTarget.Write([]byte(contents)); err != nil { + return err + } + return nil +} diff --git a/internal/writer/writer_test.go b/internal/writer/writer_test.go new file mode 100644 index 0000000..eaa3b51 --- /dev/null +++ b/internal/writer/writer_test.go @@ -0,0 +1,59 @@ +package writer_test + +import ( + "github.com/csueiras/reinforcer/internal/generator" + "github.com/csueiras/reinforcer/internal/writer" + "github.com/csueiras/reinforcer/internal/writer/filename" + wio "github.com/csueiras/reinforcer/internal/writer/io" + "github.com/stretchr/testify/require" + "testing" +) + +func TestWriter_Write(t *testing.T) { + bop := wio.NewBufferOutputProvider() + w := writer.New(bop, filename.SnakeCaseStrategy()) + require.NoError(t, w.Write("testing", &generator.Generated{ + Common: `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Common Code Here +`, + Constants: `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Constants Here +`, + Files: []*generator.GeneratedFile{ + { + TypeName: "GeneratedService", + Contents: `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Proxy Code Here +`, + }, + }, + })) + + require.Equal(t, `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Common Code Here +`, bop.Buffers["testing/reinforcer_common.go"].String()) + require.Equal(t, `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Constants Here +`, bop.Buffers["testing/reinforcer_constants.go"].String()) + require.Equal(t, `// Code generated by reinforcer, DO NOT EDIT. + +package mytestpackage + +// Proxy Code Here +`, bop.Buffers["testing/generated_service.go"].String()) +}