Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
LandonTClipp committed Dec 26, 2024
1 parent 82d3313 commit 1f30801
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 444 deletions.
2 changes: 2 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ disable-version-string: True
mockname: "{{.InterfaceName}}Mock"
filename: "{{.InterfaceName}}.go"
dir: "mocks/moq/{{.PackagePath}}"
formatter: "goimports"
packages:
github.com/vektra/mockery/v2/pkg/fixtures:
config:
include-regex: '.*'
exclude-regex: 'RequesterGenerics|UnsafeInterface|requester_unexported'
filename: "mocks.go"
template: moq
outpkg: test
template-map:
Expand Down
20 changes: 13 additions & 7 deletions cmd/mockery.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/vektra/mockery/v2/pkg"
"github.com/vektra/mockery/v2/pkg/config"
"github.com/vektra/mockery/v2/pkg/logging"
"github.com/vektra/mockery/v2/pkg/stackerr"
)
Expand Down Expand Up @@ -150,12 +149,12 @@ func initConfig(
const regexMetadataChars = "\\.+*?()|[]{}^$"

type RootApp struct {
config.Config
pkg.Config
}

func GetRootAppFromViper(v *viper.Viper) (*RootApp, error) {
r := &RootApp{}
config, err := config.NewConfigFromViper(v)
config, err := pkg.NewConfigFromViper(v)
if err != nil {
return nil, stackerr.NewStackErrf(err, "failed to get config")
}
Expand Down Expand Up @@ -208,7 +207,8 @@ func (r *RootApp) Run() error {
log.Error().Err(err).Msg("unable to parse packages")
return err
}
log.Info().Msg("done loading, visiting interface nodes")
mockFileToInterfaces := map[string][]*pkg.Interface{}

for _, iface := range interfaces {
ifaceLog := log.
With().
Expand All @@ -227,11 +227,17 @@ func (r *RootApp) Run() error {
continue
}
ifaceLog.Debug().Msg("config specifies to generate this interface")

outputter := pkg.NewOutputter(&r.Config, boilerplate, r.Config.DryRun)
if err := outputter.Generate(ifaceCtx, iface); err != nil {
ifaceConfigs, err := r.Config.GetInterfaceConfig(ctx, iface.Pkg.PkgPath, iface.Name)
if err != nil {
return err
}
for _, ifaceConfig := range ifaceConfigs {
if interfaces, ok := mockFileToInterfaces[ifaceConfig.FilePath(ctx).String()]; !ok {
interfaces = []*pkg.Interface{}
mockFileToInterfaces[ifaceConfig.FilePath(ctx).String()] = interfaces
}
interfaces = append(interfaces, iface)
}
}

return nil
Expand Down
3 changes: 2 additions & 1 deletion mocks/moq/github.com/vektra/mockery/v2/pkg/fixtures/A.go

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

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

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

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

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

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

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

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

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

176 changes: 170 additions & 6 deletions pkg/config/config.go → pkg/config.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package config
package pkg

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"go/ast"
"html/template"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"

"github.com/chigopher/pathlib"
"github.com/huandu/xstrings"
"github.com/iancoleman/strcase"
"github.com/jinzhu/copier"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
Expand All @@ -27,10 +33,6 @@ var (
ErrPkgNotFound = fmt.Errorf("package not found in config")
)

type Interface struct {
Config Config `mapstructure:"config"`
}

type Config struct {
All bool `mapstructure:"all"`
Anchors map[string]any `mapstructure:"_anchors"`
Expand Down Expand Up @@ -106,6 +108,10 @@ func (c *Config) Initialize(ctx context.Context) error {
return nil
}

func (c *Config) FilePath(ctx context.Context) *pathlib.Path {
return pathlib.NewPath(c.Dir).Join(c.FileName)
}

// CfgAsMap reads in the config file and returns a map representation, instead of a
// struct representation. This is mainly needed because viper throws away case-sensitivity
// in the `packages` section, which won't work when defining interface names 😞
Expand Down Expand Up @@ -485,7 +491,7 @@ func (c *Config) addSubPkgConfig(ctx context.Context, subPkgPath string, parentP
return nil
}

func isAutoGenerated(path *pathlib.Path) (bool, error) {
func IsAutoGenerated(path *pathlib.Path) (bool, error) {
file, err := path.OpenFile(os.O_RDONLY)
if err != nil {
return false, stackerr.NewStackErr(err)
Expand Down Expand Up @@ -767,3 +773,161 @@ func (c *Config) TagName(name string) string {
}
return string(field.Tag.Get("mapstructure"))
}

var ErrInfiniteLoop = fmt.Errorf("infinite loop in template variables detected")

// Functions available in the template for manipulating
//
// Since the map and its functions are stateless, it exists as
// a package var rather than being initialized on every call
// in [parseConfigTemplates] and [generator.printTemplate]
var templateFuncMap = template.FuncMap{
// String inspection and manipulation
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"join": strings.Join,
"replace": strings.Replace,
"replaceAll": strings.ReplaceAll,
"split": strings.Split,
"splitAfter": strings.SplitAfter,
"splitAfterN": strings.SplitAfterN,
"trim": strings.Trim,
"trimLeft": strings.TrimLeft,
"trimPrefix": strings.TrimPrefix,
"trimRight": strings.TrimRight,
"trimSpace": strings.TrimSpace,
"trimSuffix": strings.TrimSuffix,
"lower": strings.ToLower,
"upper": strings.ToUpper,
"camelcase": xstrings.ToCamelCase,
"snakecase": xstrings.ToSnakeCase,
"kebabcase": xstrings.ToKebabCase,
"firstLower": xstrings.FirstRuneToLower,
"firstUpper": xstrings.FirstRuneToUpper,

// Regular expression matching
"matchString": regexp.MatchString,
"quoteMeta": regexp.QuoteMeta,

// Filepath manipulation
"base": filepath.Base,
"clean": filepath.Clean,
"dir": filepath.Dir,

// Basic access to reading environment variables
"expandEnv": os.ExpandEnv,
"getenv": os.Getenv,
}

// parseConfigTemplates parses various templated strings
// in the config struct into their fully defined values. This mutates
// the config object passed.
func (c *Config) ParseTemplates(ctx context.Context, iface *Interface) error {
log := zerolog.Ctx(ctx)

mock := "mock"
if ast.IsExported(iface.Name) {
mock = "Mock"
}

workingDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}
var interfaceDirRelative string
interfaceDir := pathlib.NewPath(iface.FileName).Parent()
interfaceDirRelativePath, err := interfaceDir.RelativeToStr(workingDir)
if errors.Is(err, pathlib.ErrRelativeTo) {
log.Debug().
Stringer("interface-dir", interfaceDir).
Str("working-dir", workingDir).
Msg("can't make interfaceDir relative to working dir. Setting InterfaceDirRelative to package path.")

interfaceDirRelative = iface.Pkg.Types.Path()
} else {
interfaceDirRelative = interfaceDirRelativePath.String()
}

// data is the struct sent to the template parser
data := struct {
ConfigDir string
InterfaceDir string
InterfaceDirRelative string
InterfaceFile string
InterfaceName string
InterfaceNameCamel string
InterfaceNameLowerCamel string
InterfaceNameSnake string
InterfaceNameLower string
Mock string
MockName string
PackageName string
PackagePath string
}{
ConfigDir: filepath.Dir(c.Config),
InterfaceDir: filepath.Dir(iface.FileName),
InterfaceDirRelative: interfaceDirRelative,
InterfaceFile: iface.FileName,
InterfaceName: iface.Name,
// Deprecated: All custom case variables of InterfaceName will be removed in the next major version
// Use the template functions instead
InterfaceNameCamel: strcase.ToCamel(iface.Name),
InterfaceNameLowerCamel: strcase.ToLowerCamel(iface.Name),
InterfaceNameSnake: strcase.ToSnake(iface.Name),
InterfaceNameLower: strings.ToLower(iface.Name),
Mock: mock,
MockName: c.MockName,
PackageName: iface.Pkg.Types.Name(),
PackagePath: iface.Pkg.Types.Path(),
}
// 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,
"mockname": &c.MockName,
"outpkg": &c.Outpkg,
}

changesMade := true
for i := 0; changesMade; i++ {
if i >= 20 {
log.Error().Msg("infinite loop in template variables detected")
for key, val := range templateMap {
l := log.With().Str("variable-name", key).Str("variable-value", *val).Logger()
l.Error().Msg("config variable value")
}
return ErrInfiniteLoop
}
// Templated variables can refer to other templated variables,
// so we need to continue parsing the templates until it can't
// be parsed anymore.
changesMade = false

for name, attributePointer := range templateMap {
oldVal := *attributePointer

attributeTempl, err := template.New("interface-template").Funcs(templateFuncMap).Parse(*attributePointer)
if err != nil {
return fmt.Errorf("failed to parse %s template: %w", name, err)
}
var parsedBuffer bytes.Buffer

if err := attributeTempl.Execute(&parsedBuffer, data); err != nil {
return fmt.Errorf("failed to execute %s template: %w", name, err)
}
*attributePointer = parsedBuffer.String()
if *attributePointer != oldVal {
changesMade = true
}
}
}

return nil
}

func (c *Config) ClearCfgAsMap() {
c._cfgAsMap = nil
}
Loading

0 comments on commit 1f30801

Please sign in to comment.