Skip to content

Commit

Permalink
feature/full-control-over-mock-generation (#26)
Browse files Browse the repository at this point in the history
* Refactors the type_model_builder.go module; adds the reflection package with extensible syntax walker and symbol visitor modules.
* Extends the generate_mocks_command.go module
  • Loading branch information
matzefriedrich authored Sep 13, 2024
1 parent ad80749 commit 321a30b
Show file tree
Hide file tree
Showing 21 changed files with 563 additions and 151 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.9.3] - 2024-09-13

### Changed

* The `type_model_builder.go` module has been refactored to improve flexibility and maintain separation of concerns. By decoupling AST traversal from model building, the system now employs an extensible visitor pattern.

* The `generate mocks` command does now support additional annotations; use `//parsley:mock` and `//parsley:ignore` to gain full control over how mock generation is handled, while keeping the default behavior of including all interfaces. If `//parsley:mock` is present, it takes precedence, meaning all interfaces are excluded by default, and only those explicitly marked with `//parsley:mock` are included.


## [v0.9.2] - 2024-09-09

### Added
Expand Down
4 changes: 2 additions & 2 deletions cmd/parsley-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ func main() {
app.AddGroupCommand(
commands.NewGenerateGroupCommand(),
func(w charmer.CommandSetup) {
w.AddCommand(commands.NewGenerateProxyCommand())
w.AddCommand(commands.NewGenerateMocksCommand())
w.AddCommand(commands.NewGenerateProxyCommand())
})

app.Execute()
_ = app.Execute()
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.20.0
golang.org/x/mod v0.21.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ 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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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=
Expand Down
124 changes: 119 additions & 5 deletions internal/commands/generate_mocks_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,44 @@ import (
"github.com/matzefriedrich/cobra-extensions/pkg"
"github.com/matzefriedrich/cobra-extensions/pkg/abstractions"
"github.com/matzefriedrich/parsley/internal/generator"
"github.com/matzefriedrich/parsley/internal/reflection"
"github.com/matzefriedrich/parsley/internal/templates"
"github.com/spf13/cobra"
"slices"
"strings"
)

type mockGeneratorCommand struct {
type mocksGeneratorCommand struct {
use abstractions.CommandName `flag:"mocks" short:"Generate configurable mocks for interface types."`
}

func (m *mockGeneratorCommand) Execute() {
type MocksGeneratorBehavior int

const (
Default MocksGeneratorBehavior = iota
OnlyMarked
ExcludeIgnored
)

type ParsleyMockAnnotationAttribute int

const (
Mock ParsleyMockAnnotationAttribute = iota + 1
Ignore
)

func (p ParsleyMockAnnotationAttribute) String() string {
switch p {
case Mock:
return "mock"
case Ignore:
return "ignore"
default:
return ""
}
}

func (m *mocksGeneratorCommand) Execute() {

templateLoader := func(_ string) (string, error) {
return templates.MockTemplate, nil
Expand All @@ -22,8 +51,9 @@ func (m *mockGeneratorCommand) Execute() {
kind := "mocks"
gen, _ := generator.NewCodeFileGenerator(kind, func(config *generator.CodeFileGeneratorOptions) {
config.TemplateLoader = templateLoader
config.ConfigureModelCallback = func(m *generator.Model) {
config.ConfigureModelCallback = func(m *reflection.Model) {
m.AddImport("github.com/matzefriedrich/parsley/pkg/features")
m.Interfaces = filterInterfaces(m)
}
})

Expand All @@ -33,9 +63,93 @@ func (m *mockGeneratorCommand) Execute() {
}
}

var _ pkg.TypedCommand = (*mockGeneratorCommand)(nil)
func filterInterfaces(m *reflection.Model) []reflection.Interface {

behavior := determineMockGeneratorBehavior(m)

filterIdentifiers := func(attribute ParsleyMockAnnotationAttribute) []uint64 {
identifiers := make([]uint64, 0)
for _, comment := range m.Comments {
if isParsleyMockDirective(comment, attribute) {
p := comment.Pos
for _, t := range m.Interfaces {
if t.Pos > p {
identifiers = append(identifiers, t.Id)
break
}
}
}
}
return identifiers
}

switch behavior {
case OnlyMarked:
keep := filterIdentifiers(Mock)
// Keep interfaces whose identifier is in the keep slice
return slices.DeleteFunc(m.Interfaces, func(i reflection.Interface) bool {
return !slices.Contains(keep, i.Id)
})
case ExcludeIgnored:
removed := filterIdentifiers(Ignore)
// Remove interfaces whose identifier is in the removed slice
return slices.DeleteFunc(m.Interfaces, func(i reflection.Interface) bool {
return slices.Contains(removed, i.Id)
})
default:
return m.Interfaces
}
}

var _ pkg.TypedCommand = (*mocksGeneratorCommand)(nil)

func NewGenerateMocksCommand() *cobra.Command {
command := &mockGeneratorCommand{}
command := &mocksGeneratorCommand{}
return pkg.CreateTypedCommand(command)
}

func determineMockGeneratorBehavior(m *reflection.Model) MocksGeneratorBehavior {

hasMockAnnotations := slices.ContainsFunc(m.Comments, func(comment reflection.Comment) bool {
return isParsleyMockDirective(comment, Mock)
})

if hasMockAnnotations {
return OnlyMarked
}

hasIgnoreAnnotations := slices.ContainsFunc(m.Comments, func(comment reflection.Comment) bool {
return isParsleyMockDirective(comment, Ignore)
})

if hasIgnoreAnnotations {
return ExcludeIgnored
}

return Default
}

// isGenerateMockDirective Returns true if the comment matches the directive, otherwise false.
func isParsleyMockDirective(comment reflection.Comment, annotation ParsleyMockAnnotationAttribute) bool {

commentText := strings.TrimSpace(comment.Text)
words := strings.Fields(commentText)

annotationString := annotation.String()
if annotationString == "" {
return false
}

expected := []string{fmt.Sprintf("//parsley:%s", annotationString)}

if len(words) >= len(expected) {
for i := range expected {
if words[i] != expected[i] {
return false
}
}
return true
}

return false
}
3 changes: 2 additions & 1 deletion internal/commands/generate_proxy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/matzefriedrich/cobra-extensions/pkg"
"github.com/matzefriedrich/cobra-extensions/pkg/abstractions"
"github.com/matzefriedrich/parsley/internal/generator"
"github.com/matzefriedrich/parsley/internal/reflection"
"github.com/matzefriedrich/parsley/internal/templates"
"github.com/spf13/cobra"
)
Expand All @@ -22,7 +23,7 @@ func (g *generateProxyCommand) Execute() {
kind := "proxy"
gen, _ := generator.NewCodeFileGenerator(kind, func(config *generator.CodeFileGeneratorOptions) {
config.TemplateLoader = templateLoader
config.ConfigureModelCallback = func(m *generator.Model) {
config.ConfigureModelCallback = func(m *reflection.Model) {
m.AddImport("github.com/matzefriedrich/parsley/pkg/features")
}
})
Expand Down
8 changes: 3 additions & 5 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package generator

import (
"fmt"
"github.com/matzefriedrich/parsley/internal/reflection"
"os"
"path"
"path/filepath"
Expand All @@ -14,7 +15,7 @@ type codeFileGenerator struct {

type CodeFileGeneratorOptions struct {
TemplateLoader TemplateLoader
ConfigureModelCallback ModelConfigurationFunc
ConfigureModelCallback reflection.ModelConfigurationFunc
kind string
}

Expand Down Expand Up @@ -50,10 +51,7 @@ func (g *codeFileGenerator) GenerateCode() error {
return err
}

builder, err := NewTemplateModelBuilder(AstFromFile(goFilePath))
if err != nil {
return err
}
builder := NewTemplateModelBuilder(reflection.AstFromFile(goFilePath))

model, err := builder.Build()
if err != nil {
Expand Down
112 changes: 11 additions & 101 deletions internal/generator/type_model_builder.go
Original file line number Diff line number Diff line change
@@ -1,117 +1,27 @@
package generator

import (
"fmt"
"go/ast"
"github.com/matzefriedrich/parsley/internal/reflection"
)

type TemplateModelBuilder struct {
node *ast.File
accessor reflection.AstFileAccessor
}

func NewTemplateModelBuilder(accessor AstFileAccessor) (*TemplateModelBuilder, error) {
node, err := accessor()
if err != nil {
return nil, err
}
func NewTemplateModelBuilder(accessor reflection.AstFileAccessor) *TemplateModelBuilder {
return &TemplateModelBuilder{
node: node,
}, nil
}

func (b *TemplateModelBuilder) Build() (*Model, error) {
packageName := b.node.Name.Name
m := NewModel(packageName)
b.collectImports(m)
b.collectInterfaces(m)
return m, nil
}

func (b *TemplateModelBuilder) collectImports(m *Model) {
ast.Inspect(b.node, func(n ast.Node) bool {
importSpec, ok := n.(*ast.ImportSpec)
if ok {
m.Imports = append(m.Imports, importSpec.Path.Value)
}
return true
})
}

func (b *TemplateModelBuilder) collectInterfaces(m *Model) {
ast.Inspect(b.node, func(n ast.Node) bool {
typeSpec, interfaceType, ok := isInterfaceType(n)
if ok {
interfaceModel := InterfaceWithName(typeSpec.Name.Name)
b.collectMethodsFor(interfaceType, &interfaceModel)
m.Interfaces = append(m.Interfaces, interfaceModel)
}
return true
})
}

func (b *TemplateModelBuilder) collectMethodsFor(interfaceType *ast.InterfaceType, interfaceModel *Interface) {
for _, method := range interfaceType.Methods.List {
if funcType, ok := method.Type.(*ast.FuncType); ok {
name := method.Names[0].Name
parameters := b.collectParametersFor(funcType)
results := b.collectResultFieldsFor(funcType)
interfaceModel.Methods = append(interfaceModel.Methods, Method{
Name: name,
Parameters: parameters,
Results: results,
})
}
accessor: accessor,
}
}

func (b *TemplateModelBuilder) collectParametersFor(funcType *ast.FuncType) []Parameter {
parameters := make([]Parameter, 0)
for _, param := range funcType.Params.List {
paramType := param.Type
paramArrayType, isArrayType := paramType.(*ast.ArrayType)
if isArrayType {
paramType = paramArrayType.Elt
}
paramTypeIdentifier, _ := paramType.(*ast.Ident)
for _, paramName := range param.Names {
parameters = append(parameters, Parameter{
Name: paramName.Name,
TypeName: paramTypeIdentifier.Name,
IsArray: isArrayType,
})
}
}
return parameters
}
func (b *TemplateModelBuilder) Build() (*reflection.Model, error) {

func (b *TemplateModelBuilder) collectResultFieldsFor(funcType *ast.FuncType) []Parameter {
parameters := make([]Parameter, 0)
if funcType.Results == nil {
return parameters
}
for index, field := range funcType.Results.List {
fieldType := field.Type
fieldArrayType, isArrayType := fieldType.(*ast.ArrayType)
if isArrayType {
fieldType = fieldArrayType.Elt
}
fieldTypeIdentifier, ok := fieldType.(*ast.Ident)
if ok {
parameters = append(parameters, Parameter{
Name: fmt.Sprintf("result%d", index),
TypeName: fieldTypeIdentifier.Name,
IsArray: isArrayType,
})
}
fileVisitor := reflection.NewFileVisitor()
walker := reflection.NewSyntaxWalker(fileVisitor)
err := walker.WalkSyntaxTree(b.accessor)
if err != nil {
return nil, err
}
return parameters
}

func isInterfaceType(n ast.Node) (*ast.TypeSpec, *ast.InterfaceType, bool) {
typeSpec, ok := n.(*ast.TypeSpec)
if ok {
interfaceType, isInterface := typeSpec.Type.(*ast.InterfaceType)
return typeSpec, interfaceType, isInterface
}
return nil, nil, false
return fileVisitor.Model()
}
Loading

0 comments on commit 321a30b

Please sign in to comment.