Skip to content

Commit

Permalink
Merge pull request #578 from LandonTClipp/interface_case
Browse files Browse the repository at this point in the history
Templating variables and casing options
  • Loading branch information
LandonTClipp authored Mar 10, 2023
2 parents b61e29c + 32917d4 commit c96741b
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 161 deletions.
118 changes: 107 additions & 11 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,126 @@ packages:
3. This is telling mockery to generate _all_ interfaces in the `io` package.
4. We can provide interface-specifc overrides to the generation config.

### Templated directory and filenames
### Templated variables

Included with this feature is the ability to use templated strings for the destination directory and filenames of the generated mocks.
Included with this feature is the ability to use templated strings for various configuration options. This is useful to define where your mocks are placed and how to name them.

These are various strategies you may want to adopt:

The default parameters are:
#### Strategies

```yaml title="Defaults"
filename: "mock_{{.InterfaceName}}.go"
dir: "mocks/{{.PackagePath}}"
```
!!! info "Strategies"

=== "defaults"

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

If these variables aren't specified, the above values will be applied to the config options. This strategy places your mocks into a separate `mocks/` directory.

**Interface Description**

| name | value |
|------|-------|
| `InterfaceName` | `MyDatabase` |
| `PackagePath` | `github.com/user/project/pkgName` |
| `PackageName` | `pkgName` |

**Output**

The mock will be generated at:

```
mocks/github.com/user/project/pkgName/mock_MyDatabase.go
```

The mock file will look like:

```go
package pkgName
import mock "github.com/stretchr/testify/mock"
type MockMyDatabase struct {
mock.Mock
}
```
=== "adjacent to interface"

!!! failure

This strategy is not yet functional.

```yaml
filename: "mock_{{.InterfaceName}}.go"
dir: "{{.PackagePathRelative}}"
structname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
```

Instead of the mocks being generated in a different folder, you may elect to generate the mocks alongside the original interface in your package. This may be the way most people define their configs, as it removes circular import issues that can happen with the default config.

For example, the mock might be generated along side the original source file like this:

```
./path/to/pkg/db.go
./path/to/pkg/mock_MyDatabase.go
```

**Interface Description**

| name | value |
|------|-------|
| `InterfaceName` | `MyDatabase` |
| `PackagePath` | `github.com/user/project/path/to/pkg`
| `PackagePathRelative` | `path/to/pkg` |
| `PackageName` | `pkgName` |
| `SourceFile` | `./path/to/pkg/db.go` |

**Output**

Mock file will be generated at:

```
./path/to/pkg/mock_MyDatabase.go
```

The mock file will look like:

```go
package pkgName
import mock "github.com/stretchr/testify/mock"
type MockMyDatabase struct {
mock.Mock
}
```




#### Template Variable Descriptions

The template variables available for your use are:

| 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 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/iancoleman/strcase v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.details
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true

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
109 changes: 47 additions & 62 deletions pkg/outputter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package pkg
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"

"github.com/chigopher/pathlib"
"github.com/iancoleman/strcase"
"github.com/pkg/errors"
"github.com/rs/zerolog"

Expand Down Expand Up @@ -124,68 +127,55 @@ 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
PackageName string
PackagePath string
MockName string
InterfaceDir string
InterfaceName string
InterfaceNameCamel string
InterfaceNameLowerCamel string
InterfaceNameSnake string
PackageName string
PackagePath string
}{
InterfaceDir: filepath.Dir(iface.FileName),
InterfaceName: iface.Name,
PackageName: iface.Pkg.Name(),
PackagePath: iface.Pkg.Path(),
MockName: mockName,
InterfaceDir: filepath.Dir(iface.FileName),
InterfaceName: iface.Name,
InterfaceNameCamel: strcase.ToCamel(iface.Name),
InterfaceNameLowerCamel: strcase.ToLowerCamel(iface.Name),
InterfaceNameSnake: strcase.ToSnake(iface.Name),
PackageName: iface.Pkg.Name(),
PackagePath: iface.Pkg.Path(),
}

// 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 @@ -235,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 @@ -255,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
Loading

0 comments on commit c96741b

Please sign in to comment.