From ddd193c7db8d94275f03a150ce55ebcd34627fbc Mon Sep 17 00:00:00 2001 From: LandonTClipp Date: Thu, 9 Mar 2023 17:59:13 -0600 Subject: [PATCH] Add more variables available for templating, update docs --- docs/features.md | 16 +++-- pkg/config/config.go | 2 + pkg/config/config_test.go | 12 ++-- pkg/outputter.go | 86 ++++++++++---------------- pkg/outputter_test.go | 124 ++++++++++++-------------------------- 5 files changed, 89 insertions(+), 151 deletions(-) diff --git a/docs/features.md b/docs/features.md index 0e27a263..a8a86ee6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 ----------------- diff --git a/pkg/config/config.go b/pkg/config/config.go index 40674d1e..69ef2169 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index dac53d1c..ae319456 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -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}}", }, }, { @@ -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}}", }, }, } diff --git a/pkg/outputter.go b/pkg/outputter.go index 4c4fea09..34b43617 100644 --- a/pkg/outputter.go +++ b/pkg/outputter.go @@ -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" @@ -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 @@ -155,7 +140,6 @@ func outputFilePath( InterfaceNameSnake string PackageName string PackagePath string - MockName string }{ InterfaceDir: filepath.Dir(iface.FileName), InterfaceName: iface.Name, @@ -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 @@ -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, @@ -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) } diff --git a/pkg/outputter_test.go b/pkg/outputter_test.go index 2befcfe6..b03c5a3b 100644 --- a/pkg/outputter_test.go +++ b/pkg/outputter_test.go @@ -1,7 +1,6 @@ package pkg import ( - "context" "reflect" "testing" @@ -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) } }) }