Skip to content

Commit

Permalink
Add more variables available for templating, update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
LandonTClipp committed Mar 9, 2023
1 parent 1f08b60 commit ddd193c
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 151 deletions.
16 changes: 7 additions & 9 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,30 @@ packages:

Included with this feature is the ability to use templated strings for the destination directory and filenames of the generated mocks.

The default parameters are:
The following options are capable of using template variables. These are their defaults:

```yaml title="Defaults"
filename: "mock_{{.InterfaceName}}.go"
dir: "mocks/{{.PackagePath}}"
structname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
```

The template variables available for your use are:
InterfaceNameCamel string
InterfaceNameLowerCamel string
InterfaceNameSnake string

| name | description |
|------|-------------|
| InterfaceDir | The path of the original interface being mocked. This can be used as `#!yaml dir: "{{.InterfaceDir}}"` to place your mocks adjacent to the original interface. This should not be used for external interfaces. |
| InterfaceName | The name of the original interface being mocked |
| InterfaceNameCamel | Converts the `InterfaceName` to camel case |
| InterfaceNameCamel | Converts a string `interface_name` to `InterfaceName` |
| InterfaceNameLowerCamel | Converts `InterfaceName` to `interfaceName` |
| InterfaceNameSnake | Converts `InterfaceName` to `interface_name` |
| PackageName | The name of the package from the original interface |
| Package Path | The fully qualified package path of the original interface |
| MockName | The name of the generated mock |
| PackagePath | The fully qualified package path of the original interface |


!!! warn
Many of the config options when using `packages` have either changed meanings or are no longer used.
!!! warning
Many of the config options when using `packages` have either changed meanings or are no longer used. It's recommended to delete all previous configuration you have as their meanings may have changed.

Mock Constructors
-----------------
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func NewConfigFromViper(v *viper.Viper) (*Config, error) {
} else {
v.SetDefault("dir", "mocks/{{.PackagePath}}")
v.SetDefault("filename", "mock_{{.InterfaceName}}.go")
v.SetDefault("structname", "Mock{{.InterfaceName}}")
v.SetDefault("outpkg", "{{.PackageName}}")
}

if err := v.UnmarshalExact(c); err != nil {
Expand Down
12 changes: 8 additions & 4 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,10 @@ packages:
github.com/vektra/mockery/v2/pkg:
`,
want: &Config{
Dir: "mocks/{{.PackagePath}}",
FileName: "mock_{{.InterfaceName}}.go",
Dir: "mocks/{{.PackagePath}}",
FileName: "mock_{{.InterfaceName}}.go",
StructName: "Mock{{.InterfaceName}}",
Outpkg: "{{.PackageName}}",
},
},
{
Expand All @@ -604,8 +606,10 @@ packages:
github.com/vektra/mockery/v2/pkg:
`,
want: &Config{
Dir: "barfoo",
FileName: "foobar.go",
Dir: "barfoo",
FileName: "foobar.go",
StructName: "Mock{{.InterfaceName}}",
Outpkg: "{{.PackageName}}",
},
},
}
Expand Down
86 changes: 32 additions & 54 deletions pkg/outputter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package pkg
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"

"github.com/chigopher/pathlib"
"github.com/iancoleman/strcase"
Expand Down Expand Up @@ -125,28 +127,11 @@ func (*FileOutputStreamProvider) underscoreCaseName(caseName string) string {
return strings.ToLower(rxp2.ReplaceAllString(s1, "${1}_${2}"))
}

// outputFilePath determines where a particular mock should reside on-disk. This function is
// specific to the `packages` config option. It respects the configuration provided in the
// `packages` section, but provides sensible defaults.
func outputFilePath(
ctx context.Context,
iface *Interface,
interfaceConfig *config.Config,
mockName string,
) (*pathlib.Path, error) {
var filename string
var outputdir string
log := zerolog.Ctx(ctx)

outputFileTemplateString := interfaceConfig.FileName
outputDirTemplate := interfaceConfig.Dir

log.Debug().Msgf("output filename template is: %v", outputFileTemplateString)
log.Debug().Msgf("output dir template is: %v", outputDirTemplate)

templ := templates.New("output-file-template")

// The fields available to the template strings
// parseConfigTemplates parses various templated strings
// in the config struct into their fully defined values. This mutates
// the config object passed.
func parseConfigTemplates(c *config.Config, iface *Interface) error {
// data is the struct sent to the template parser
data := struct {
InterfaceDir string
InterfaceName string
Expand All @@ -155,7 +140,6 @@ func outputFilePath(
InterfaceNameSnake string
PackageName string
PackagePath string
MockName string
}{
InterfaceDir: filepath.Dir(iface.FileName),
InterfaceName: iface.Name,
Expand All @@ -164,35 +148,34 @@ func outputFilePath(
InterfaceNameSnake: strcase.ToSnake(iface.Name),
PackageName: iface.Pkg.Name(),
PackagePath: iface.Pkg.Path(),
MockName: mockName,
}

// Get the name of the file from a template string
filenameTempl, err := templ.Parse(outputFileTemplateString)
if err != nil {
return nil, err
templ := template.New("interface-template")

// These are the config options that we allow
// to be parsed by the templater. The keys are
// just labels we're using for logs/errors
templateMap := map[string]*string{
"filename": &c.FileName,
"dir": &c.Dir,
"structname": &c.StructName,
"outpkg": &c.Outpkg,
}

var filenameBuffer bytes.Buffer
for name, attributePointer := range templateMap {
attributeTempl, err := templ.Parse(*attributePointer)
if err != nil {
return fmt.Errorf("failed to parse %s template: %w", name, err)
}
var parsedBuffer bytes.Buffer

if err := filenameTempl.Execute(&filenameBuffer, data); err != nil {
return nil, err
if err := attributeTempl.Execute(&parsedBuffer, data); err != nil {
return fmt.Errorf("failed to execute %s template: %w", name, err)
}
*attributePointer = parsedBuffer.String()
}
filename = filenameBuffer.String()
log.Debug().Msgf("filename is: %v", filename)

// Get the name of the output dir
outputDirTempl, err := templ.Parse(outputDirTemplate)
if err != nil {
return nil, err
}
var outputDirBuffer bytes.Buffer
if err := outputDirTempl.Execute(&outputDirBuffer, data); err != nil {
return nil, err
}
outputdir = outputDirBuffer.String()
return nil

return pathlib.NewPath(outputdir).Join(filename), nil
}

// Outputter wraps the Generator struct. It calls the generator
Expand Down Expand Up @@ -242,6 +225,9 @@ func (m *Outputter) Generate(ctx context.Context, iface *Interface) error {

for _, interfaceConfig := range interfaceConfigs {
log.Debug().Msg("getting mock generator")

parseConfigTemplates(interfaceConfig, iface)

g := GeneratorConfig{
Boilerplate: m.boilerplate,
DisableVersionString: interfaceConfig.DisableVersionString,
Expand All @@ -262,15 +248,7 @@ func (m *Outputter) Generate(ctx context.Context, iface *Interface) error {
return err
}

outputPath, err := outputFilePath(
ctx,
iface,
interfaceConfig,
generator.mockName(),
)
if err != nil {
return errors.Wrap(err, "failed to determine file path")
}
outputPath := pathlib.NewPath(interfaceConfig.Dir).Join(interfaceConfig.FileName)
if err := outputPath.Parent().MkdirAll(); err != nil {
return errors.Wrapf(err, "failed to mkdir parents of: %v", outputPath)
}
Expand Down
124 changes: 40 additions & 84 deletions pkg/outputter_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package pkg

import (
"context"
"reflect"
"testing"

Expand Down Expand Up @@ -73,103 +72,60 @@ func newConfig(t *testing.T) *config.Config {
return &config.Config{}
}

func Test_foobar(t *testing.T) {
mockPackage := pkgMocks.NewTypesPackage(t)
mockPackage.EXPECT().Name().Return("pkg")
mockPackage.EXPECT().Path().Return("github.com/vektra/mockery")

iface := &Interface{
Name: "interfaceName",
Pkg: mockPackage,
}
path, err := outputFilePath(context.Background(), iface, &config.Config{
FileName: "filename.go",
Dir: "dirname",
}, "")
assert.NoError(t, err)
assert.Equal(t, pathlib.NewPath("dirname/filename.go"), path)
}

func Test_outputFilePath(t *testing.T) {
type parameters struct {
packageName string
packagePath string
interfaceName string
fileName string
fileNameTemplate string
dirTemplate string
mockName string
func Test_parseConfigTemplates(t *testing.T) {
type args struct {
c *config.Config
iface *Interface
}
tests := []struct {
name string
params parameters
want *pathlib.Path
name string
args args

// pkg is used to generate a mock types.Package object.
// It has to take in the testing.T object so we can
// assert expectations.
pkg func(t *testing.T) *pkgMocks.TypesPackage
want *config.Config
wantErr bool
}{
{
name: "defaults",
params: parameters{
packageName: "pkg",
packagePath: "github.com/vektra/mockery",
interfaceName: "Foo",
fileNameTemplate: "mock_{{.InterfaceName}}.go",
dirTemplate: "mocks/{{.PackagePath}}",
name: "standards",
args: args{
c: &config.Config{
Dir: "{{.InterfaceDir}}/{{.PackagePath}}",
FileName: "{{.InterfaceName}}_{{.InterfaceNameCamel}}_{{.InterfaceNameSnake}}.go",
StructName: "{{.InterfaceNameLowerCamel}}",
Outpkg: "{{.PackageName}}",
},

iface: &Interface{
Name: "FooBar",
FileName: "path/to/foobar.go",
},
},
want: pathlib.NewPath("mocks/github.com/vektra/mockery/mock_Foo.go"),
},
{
name: "dir and filename templates",
params: parameters{
packageName: "pkg",
packagePath: "github.com/vektra/mockery",
interfaceName: "Foo",
fileNameTemplate: "{{.MockName}}_{{.PackageName}}_{{.InterfaceName}}.go",
dirTemplate: "{{.PackagePath}}",
mockName: "MockFoo",
pkg: func(t *testing.T) *pkgMocks.TypesPackage {
m := pkgMocks.NewTypesPackage(t)
m.EXPECT().Path().Return("github.com/user/project/package")
m.EXPECT().Name().Return("packageName")
return m
},
want: pathlib.NewPath("github.com/vektra/mockery/MockFoo_pkg_Foo.go"),
},
{
name: "mock next to original interface",
params: parameters{
packageName: "pkg",
packagePath: "github.com/vektra/mockery/pkg/internal",
interfaceName: "Foo",
fileName: "pkg/internal/foo.go",
dirTemplate: "{{.InterfaceDir}}",
fileNameTemplate: "mock_{{.InterfaceName}}.go",
mockName: "MockFoo",
want: &config.Config{
Dir: "path/to/github.com/user/project/package",
FileName: "FooBar_FooBar_foo_bar.go",
StructName: "fooBar",
Outpkg: "packageName",
},
want: pathlib.NewPath("pkg/internal/mock_Foo.go"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPackage := pkgMocks.NewTypesPackage(t)
mockPackage.EXPECT().Name().Return(tt.params.packageName)
mockPackage.EXPECT().Path().Return(tt.params.packagePath)

iface := &Interface{
Name: tt.params.interfaceName,
Pkg: mockPackage,
FileName: tt.params.fileName,
}
tt.args.iface.Pkg = tt.pkg(t)

got, err := outputFilePath(
context.Background(),
iface,
&config.Config{
FileName: tt.params.fileNameTemplate,
Dir: tt.params.dirTemplate,
},
tt.params.mockName,
)
if (err != nil) != tt.wantErr {
t.Errorf("outputFilePath() error = %v, wantErr %v", err, tt.wantErr)
return
if err := parseConfigTemplates(tt.args.c, tt.args.iface); (err != nil) != tt.wantErr {
t.Errorf("parseConfigTemplates() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("outputFilePath() = %v, want %v", got, tt.want)
if !reflect.DeepEqual(tt.args.c, tt.want) {
t.Errorf("*config.Config = %v, want %v", tt.args.c, tt.want)
}
})
}
Expand Down

0 comments on commit ddd193c

Please sign in to comment.