Skip to content

Commit

Permalink
Merge pull request #40 from csueiras/struct-support
Browse files Browse the repository at this point in the history
Support extracting contract from a struct.
  • Loading branch information
csueiras authored Mar 8, 2021
2 parents 1c9276b + b23fdb4 commit 245479f
Show file tree
Hide file tree
Showing 20 changed files with 454 additions and 192 deletions.
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# reinforcer

![Tests](https://github.com/csueiras/reinforcer/workflows/run%20tests/badge.svg?branch=develop)
[![Coverage Status](https://coveralls.io/repos/github/csueiras/reinforcer/badge.svg?branch=develop)](https://coveralls.io/github/csueiras/reinforcer?branch=develop)
[![Go Report Card](https://goreportcard.com/badge/github.com/csueiras/reinforcer)](https://goreportcard.com/report/github.com/csueiras/reinforcer)
Expand All @@ -9,13 +10,15 @@ Reinforcer is a code generation tool that automates middleware injection in a pr
implementation, this aids in building more resilient code as you can use common resiliency patterns in the middlewares
such as circuit breakers, retrying, timeouts and others.

**NOTE:** _While version is < 1.0.0 the APIs might dramatically change between minor versions, any breaking changes will be enumerated here starting with version 0.7.0 and forward._
**NOTE:** _While version is < 1.0.0 the APIs might dramatically change between minor versions, any breaking changes will
be enumerated here starting with version 0.7.0 and forward._

## Install

### Releases

Visit the [releases page](https://github.com/csueiras/reinforcer/releases) for pre-built binaries for OS X, Linux and Windows.
Visit the [releases page](https://github.com/csueiras/reinforcer/releases) for pre-built binaries for OS X, Linux and
Windows.

### Docker

Expand Down Expand Up @@ -43,22 +46,26 @@ brew upgrade csueiras/reinforcer/reinforcer

### CLI

Generate reinforced code for all exported interfaces:
Generate reinforced code for all exported interfaces and structs:

```
reinforcer --src=./service.go --targetall --outputdir=./reinforced
```

Generate reinforced code using regex:

```
reinforcer --src=./service.go --target='.*Service' --outputdir=./reinforced
```

Generate reinforced code using an exact match:

```
reinforcer --src=./service.go --target=MyService --outputdir=./reinforced
```

For more options:

```
reinforcer --help
```
Expand All @@ -79,15 +86,14 @@ Flags:
-p, --outpkg string name of generated package (default "reinforced")
-o, --outputdir string directory to write the generated code to (default "./reinforced")
-q, --silent disables logging. Mutually exclusive with the debug flag.
-s, --src strings source files to scan for the target interface. If unspecified the file pointed by the env variable GOFILE will be used.
-t, --target strings name of target type or regex to match interface names with
-a, --targetall codegen for all exported interfaces discovered. This option is mutually exclusive with the target option.
-s, --src strings source files to scan for the target interface or struct. If unspecified the file pointed by the env variable GOFILE will be used.
-t, --target strings name of target type or regex to match interface or struct names with
-a, --targetall codegen for all exported interfaces/structs discovered. This option is mutually exclusive with the target option.
-v, --version show reinforcer's version
```

### Using Reinforced Code


1. Describe the target that you want to generate code for:

```
Expand All @@ -96,7 +102,25 @@ type Client interface {
}
```

2. Create the runner/middleware factory with the middlewares you want to inject into the generated code:
Or from a struct:

```
type Client struct {
}
func (c *Client) DoOperation(ctx context.Context, arg string) error {
// ...
return nil
}
```

2. Generate the reinforcer code:

```
reinforcer --debug --src='./client.go' --target=Client --outputdir=./reinforced
```

3. Create the runner/middleware factory with the middlewares you want to inject into the generated code:

```
r := runner.NewFactory(
Expand All @@ -108,7 +132,7 @@ r := runner.NewFactory(
)
```

3. Optionally create your predicate for errors that shouldn't be retried
4. Optionally create your predicate for errors that shouldn't be retried

```
// shouldRetryErrPredicate is a predicate that ignores the "NotFound" errors emited by the DoOperation in Client. All other errors
Expand All @@ -121,7 +145,7 @@ shouldRetryErrPredicate := func(method string, err error) bool {
}
```

4. Wrap the "real"/unrealiable implementation in the generated code:
5. Wrap the "real"/unrealiable implementation in the generated code:

```
c := client.NewClient(...)
Expand Down
6 changes: 3 additions & 3 deletions cmd/reinforcer/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ such as circuit breaker, retries, timeouts, etc.
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.StringSliceP("src", "s", nil, "source files to scan for the target interface or struct. 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 or struct names with")
flags.BoolP("targetall", "a", false, "codegen for all exported interfaces/structs 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.")
Expand Down
15 changes: 13 additions & 2 deletions example/client/client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:generate reinforcer --debug --target=Client --target=SomeOtherClient --outputdir=./reinforced
//go:generate reinforcer --debug --target=Client --target=SomeOtherClient --target=Service --outputdir=./reinforced

package client

Expand Down Expand Up @@ -26,10 +26,21 @@ type SomeOtherClient interface {
DoStuff() error
SaveFile(myFile *File, osFile *os.File) error
GetUser(ctx context.Context) (*sub.User, error)
MethodWithChannel(myChan <- chan bool) error
MethodWithChannel(myChan <-chan bool) error
MethodWithWildcard(arg interface{})
}

// Service is an example of a struct defined contract that will be reversed engineered by reinforcer
type Service struct{}

// GetData retrieves data it might randomly error out
func (s *Service) GetData() ([]byte, error) {
if rand.Int()%5 == 0 {
return nil, fmt.Errorf("random failure")
}
return []byte{0xB, 0xE, 0xE, 0xF}, nil
}

// FakeClient is a Client implementation that will randomly fail
type FakeClient struct {
}
Expand Down
7 changes: 7 additions & 0 deletions example/client/reinforced/reinforcer_constants.go

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

50 changes: 50 additions & 0 deletions example/client/reinforced/service.go

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

10 changes: 5 additions & 5 deletions internal/generator/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"github.com/csueiras/reinforcer/internal/generator"
"github.com/csueiras/reinforcer/internal/loader"
"go/types"
)

// ErrNoTargetableTypesFound indicates that no types that could be targeted for code generation were discovered
Expand Down Expand Up @@ -44,7 +43,8 @@ func New(l Loader) *Executor {

// Execute orchestrates code generation sourced from multiple files/targets
func (e *Executor) Execute(settings *Parameters) (*generator.Generated, error) {
results := make(map[string]*types.Interface)
discoveredTypes := make(map[string]struct{})

var cfg []*generator.FileConfig
var err error
for _, source := range settings.Sources {
Expand All @@ -60,11 +60,11 @@ func (e *Executor) Execute(settings *Parameters) (*generator.Generated, error) {

// Check types aren't repeated before adding them to the generator's config
for typName, res := range match {
if _, ok := results[typName]; ok {
if _, ok := discoveredTypes[typName]; ok {
return nil, fmt.Errorf("multiple types with same name discovered with name %s", typName)
}
results[typName] = res.InterfaceType
cfg = append(cfg, generator.NewFileConfig(typName, typName, res.InterfaceType))
discoveredTypes[typName] = struct{}{}
cfg = append(cfg, generator.NewFileConfig(typName, typName, res.Methods))
}
}

Expand Down
15 changes: 7 additions & 8 deletions internal/generator/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package executor_test
import (
"github.com/csueiras/reinforcer/internal/generator/executor"
"github.com/csueiras/reinforcer/internal/generator/executor/mocks"
"github.com/csueiras/reinforcer/internal/generator/method"
"github.com/csueiras/reinforcer/internal/loader"
"github.com/stretchr/testify/require"
"go/token"
"go/types"
"testing"
)
Expand All @@ -16,8 +16,8 @@ func TestExecutor_Execute(t *testing.T) {
l.On("LoadMatched", "./testpkg.go", []string{"MyService"}, loader.FileLoadMode).Return(
map[string]*loader.Result{
"LockService": {
Name: "LockService",
InterfaceType: createTestInterfaceType(),
Name: "LockService",
Methods: createTestServiceMethods(),
},
}, nil,
)
Expand Down Expand Up @@ -52,11 +52,10 @@ func TestExecutor_Execute(t *testing.T) {
})
}

func createTestInterfaceType() *types.Interface {
func createTestServiceMethods() []*method.Method {
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 []*method.Method{
method.MustParseMethod("Lock", nullary),
method.MustParseMethod("Unlock", nullary),
}
return types.NewInterfaceType(methods, nil).Complete()
}
31 changes: 7 additions & 24 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/csueiras/reinforcer/internal/generator/retryable"
"github.com/dave/jennifer/jen"
"github.com/rs/zerolog/log"
"go/types"
"strings"
)

Expand All @@ -21,16 +20,16 @@ type FileConfig struct {
srcTypeName string
// outTypeName is the desired output type name
outTypeName string
// interfaceType holds the type information for SrcTypeName
interfaceType *types.Interface
// methods that should be in the generated type
methods []*method.Method
}

// NewFileConfig creates a new instance of the FileConfig which holds code generation configuration
func NewFileConfig(srcTypeName string, outTypeName string, interfaceType *types.Interface) *FileConfig {
func NewFileConfig(srcTypeName, outTypeName string, methods []*method.Method) *FileConfig {
return &FileConfig{
srcTypeName: strings.Title(srcTypeName),
outTypeName: strings.Title(outTypeName),
interfaceType: interfaceType,
srcTypeName: strings.Title(srcTypeName),
outTypeName: strings.Title(outTypeName),
methods: methods,
}
}

Expand Down Expand Up @@ -97,10 +96,7 @@ func Generate(cfg Config) (*Generated, error) {
var fileMethods []*fileMeta

for _, fileConfig := range cfg.Files {
methods, err := parseMethods(fileConfig.outTypeName, fileConfig.interfaceType)
if err != nil {
return nil, err
}
methods := fileConfig.methods
s, err := generateFile(cfg.OutPkg, cfg.IgnoreNoReturnMethods, fileConfig, methods)
if err != nil {
return nil, err
Expand Down Expand Up @@ -268,19 +264,6 @@ func generateConstants(outPkg string, meta []*fileMeta) (string, error) {
return renderToString(f)
}

func parseMethods(typeName string, interfaceType *types.Interface) ([]*method.Method, error) {
var methods []*method.Method
for m := 0; m < interfaceType.NumMethods(); m++ {
meth := interfaceType.Method(m)
mm, err := method.ParseMethod(typeName, meth.Name(), meth.Type().(*types.Signature))
if err != nil {
return nil, err
}
methods = append(methods, mm)
}
return methods, nil
}

func renderToString(f *jen.File) (string, error) {
b := &bytes.Buffer{}
if err := f.Render(b); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ func loadInterface(t *testing.T, filesCode map[string]input) []*generator.FileCo
require.NoError(t, err)
loadedTypes = append(loadedTypes, generator.NewFileConfig(in.interfaceName,
fmt.Sprintf("Generated%s", strings.Title(in.interfaceName)),
svc.InterfaceType,
svc.Methods,
))
}
return loadedTypes
Expand Down
Loading

0 comments on commit 245479f

Please sign in to comment.