7d78e65d0c
Add semantic versioning to all services * working * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver * 1.24 mod * go upgrade * tool * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver
379 lines
12 KiB
Go
379 lines
12 KiB
Go
package pkg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/chigopher/pathlib"
|
|
"github.com/huandu/xstrings"
|
|
"github.com/iancoleman/strcase"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/vektra/mockery/v2/pkg/config"
|
|
"github.com/vektra/mockery/v2/pkg/logging"
|
|
"github.com/vektra/mockery/v2/pkg/stackerr"
|
|
)
|
|
|
|
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,
|
|
}
|
|
|
|
type Cleanup func() error
|
|
|
|
type OutputStreamProvider interface {
|
|
GetWriter(context.Context, *Interface) (io.Writer, error, Cleanup)
|
|
}
|
|
|
|
type StdoutStreamProvider struct{}
|
|
|
|
func (*StdoutStreamProvider) GetWriter(ctx context.Context, iface *Interface) (io.Writer, error, Cleanup) {
|
|
return os.Stdout, nil, func() error { return nil }
|
|
}
|
|
|
|
type FileOutputStreamProvider struct {
|
|
Config config.Config
|
|
BaseDir string
|
|
InPackage bool
|
|
InPackageSuffix bool
|
|
TestOnly bool
|
|
Case string
|
|
KeepTree bool
|
|
KeepTreeOriginalDirectory string
|
|
FileName string
|
|
}
|
|
|
|
func (p *FileOutputStreamProvider) GetWriter(ctx context.Context, iface *Interface) (io.Writer, error, Cleanup) {
|
|
log := zerolog.Ctx(ctx).With().Str(logging.LogKeyInterface, iface.Name).Logger()
|
|
// ctx = log.WithContext(ctx)
|
|
|
|
var path string
|
|
|
|
caseName := iface.Name
|
|
if p.Case == "underscore" || p.Case == "snake" {
|
|
caseName = p.underscoreCaseName(caseName)
|
|
}
|
|
|
|
if p.KeepTree {
|
|
absOriginalDir, err := filepath.Abs(p.KeepTreeOriginalDirectory)
|
|
if err != nil {
|
|
return nil, err, func() error { return nil }
|
|
}
|
|
relativePath := strings.TrimPrefix(
|
|
filepath.Join(filepath.Dir(iface.FileName), p.filename(caseName)),
|
|
absOriginalDir)
|
|
|
|
// as it's not possible to import from internal path, we have to replace it in mocks when KeepTree is used
|
|
relativePath = strings.Replace(relativePath, "/internal/", "/internal_/", -1)
|
|
|
|
path = filepath.Join(p.BaseDir, relativePath)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return nil, err, func() error { return nil }
|
|
}
|
|
} else if p.InPackage {
|
|
path = filepath.Join(filepath.Dir(iface.FileName), p.filename(caseName))
|
|
} else {
|
|
path = filepath.Join(p.BaseDir, p.filename(caseName))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return nil, err, func() error { return nil }
|
|
}
|
|
}
|
|
|
|
log = log.With().Str(logging.LogKeyPath, path).Logger()
|
|
|
|
log.Debug().Msgf("creating writer to file")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return nil, err, func() error { return nil }
|
|
}
|
|
|
|
return f, nil, func() error {
|
|
return f.Close()
|
|
}
|
|
}
|
|
|
|
func (p *FileOutputStreamProvider) filename(name string) string {
|
|
if p.FileName != "" {
|
|
return p.FileName
|
|
}
|
|
|
|
if p.InPackage && p.TestOnly {
|
|
if p.InPackageSuffix {
|
|
return name + "_mock_test.go"
|
|
}
|
|
|
|
return "mock_" + name + "_test.go"
|
|
} else if p.InPackage && !p.KeepTree {
|
|
if p.InPackageSuffix {
|
|
return name + "_mock.go"
|
|
}
|
|
|
|
return "mock_" + name + ".go"
|
|
} else if p.TestOnly {
|
|
return name + "_test.go"
|
|
}
|
|
|
|
return name + ".go"
|
|
}
|
|
|
|
// shamelessly taken from http://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-camel-caseo
|
|
func (*FileOutputStreamProvider) underscoreCaseName(caseName string) string {
|
|
rxp1 := regexp.MustCompile("(.)([A-Z][a-z]+)")
|
|
s1 := rxp1.ReplaceAllString(caseName, "${1}_${2}")
|
|
rxp2 := regexp.MustCompile("([a-z0-9])([A-Z])")
|
|
return strings.ToLower(rxp2.ReplaceAllString(s1, "${1}_${2}"))
|
|
}
|
|
|
|
// parseConfigTemplates parses various templated strings
|
|
// in the config struct into their fully defined values. This mutates
|
|
// the config object passed.
|
|
func parseConfigTemplates(ctx context.Context, c *config.Config, 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.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.Name(),
|
|
PackagePath: iface.Pkg.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
|
|
}
|
|
|
|
// Outputter wraps the Generator struct. It calls the generator
|
|
// to create the mock implementations in-memory, then has additional
|
|
// logic to determine where the mock should be written to on disk.
|
|
type Outputter struct {
|
|
boilerplate string
|
|
config *config.Config
|
|
dryRun bool
|
|
}
|
|
|
|
func NewOutputter(
|
|
config *config.Config,
|
|
boilerplate string,
|
|
dryRun bool,
|
|
) *Outputter {
|
|
return &Outputter{
|
|
boilerplate: boilerplate,
|
|
config: config,
|
|
dryRun: dryRun,
|
|
}
|
|
}
|
|
|
|
func (m *Outputter) Generate(ctx context.Context, iface *Interface) error {
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str(logging.LogKeyInterface, iface.Name).
|
|
Str(logging.LogKeyQualifiedName, iface.QualifiedName).
|
|
Logger()
|
|
ctx = log.WithContext(ctx)
|
|
|
|
log.Info().Msg("generating mocks for interface")
|
|
|
|
log.Debug().Msg("getting config for interface")
|
|
interfaceConfigs, err := m.config.GetInterfaceConfig(ctx, iface.QualifiedName, iface.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, interfaceConfig := range interfaceConfigs {
|
|
interfaceConfig.LogUnsupportedPackagesConfig(ctx)
|
|
|
|
log.Debug().Msg("getting mock generator")
|
|
|
|
if err := parseConfigTemplates(ctx, interfaceConfig, iface); err != nil {
|
|
return fmt.Errorf("failed to parse config template: %w", err)
|
|
}
|
|
interfaceConfig.LogDeprecatedConfig(ctx)
|
|
|
|
g := GeneratorConfig{
|
|
Boilerplate: m.boilerplate,
|
|
DisableVersionString: interfaceConfig.DisableVersionString,
|
|
Exported: interfaceConfig.Exported,
|
|
InPackage: interfaceConfig.InPackage,
|
|
Issue845Fix: interfaceConfig.Issue845Fix,
|
|
KeepTree: interfaceConfig.KeepTree,
|
|
Note: interfaceConfig.Note,
|
|
MockBuildTags: interfaceConfig.MockBuildTags,
|
|
Outpkg: interfaceConfig.Outpkg,
|
|
PackageNamePrefix: interfaceConfig.Packageprefix,
|
|
StructName: interfaceConfig.MockName,
|
|
UnrollVariadic: interfaceConfig.UnrollVariadic,
|
|
WithExpecter: interfaceConfig.WithExpecter,
|
|
ReplaceType: interfaceConfig.ReplaceType,
|
|
ResolveTypeAlias: interfaceConfig.ResolveTypeAlias,
|
|
}
|
|
generator := NewGenerator(ctx, g, iface, interfaceConfig.Outpkg)
|
|
|
|
log.Debug().Msg("generating mock in-memory")
|
|
if err := generator.GenerateAll(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Log where the file would be written to before checking whether to create the directories and files
|
|
outputPath := pathlib.NewPath(interfaceConfig.Dir).Join(interfaceConfig.FileName)
|
|
fileLog := log.With().Stringer(logging.LogKeyFile, outputPath).Logger()
|
|
fileLog.Info().Msg("writing to file")
|
|
|
|
if m.dryRun {
|
|
continue
|
|
}
|
|
|
|
if err := outputPath.Parent().MkdirAll(); err != nil {
|
|
return stackerr.NewStackErrf(err, "failed to mkdir parents of: %v", outputPath)
|
|
}
|
|
|
|
file, err := outputPath.OpenFile(os.O_RDWR | os.O_CREATE | os.O_TRUNC)
|
|
if err != nil {
|
|
return stackerr.NewStackErrf(err, "failed to open output file for mock: %v", outputPath)
|
|
}
|
|
defer file.Close()
|
|
if err := generator.Write(file); err != nil {
|
|
return stackerr.NewStackErrf(err, "failed to write to file")
|
|
}
|
|
}
|
|
return nil
|
|
}
|