diff --git a/CHANGELOG.md b/CHANGELOG.md index 073a163..217aa30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.9.0] - 2024-09-08 + +Starting with this release, the project's license has been changed from AGPLv3 to Apache License 2.0. The move to the Apache 2.0 license reflects my desire to make the library more accessible and easier to adopt, especially in commercial and proprietary projects. + +### Added + +* Adds the `generate mocks` CLI command that can generate configurable mock implemetations from interface types. + +### Changed + +* Several refactorings to the internal `generator` package with improvements to error handling and extensibility. + +* Adds the `generic_generator.go` module, integrating generator templates, output file configuration, and template execution. The initial implementation resided in the `generate_proxy_command.go` module. By pulling variables and control structures from parameters, the generator command logic could be moved to the (internal) `generator` package, allowing the logic to be reused by other code-file generator commands. Adding other template-based generators based on (interface) type models can be achieved with less effort. + +* Removes methods from the generator type model - uses a function map instead. + +* The generic code generator now formats generated code in canonical go fmt style. + + +### Fixed + +* Fixes generator command short description texts + + ## [v0.8.3] - 2024-09-01 ### Fixed @@ -26,7 +50,9 @@ Parsley is extended by the `parsley-cli` utility application, which is the found ### Added * Adds the `parsley-cli` application that adds code generation capabilities. + * The `init` command bootstraps a new Parsley application (a `main.go` and an `application.go` file providing the bare minimum to kick-start a dependency injection-enabled app). + * The `generate proxy` command generates extensible proxy types by `MethodInterceptor` objects, which can function as proxies or decorator objects. @@ -38,7 +64,7 @@ This version addresses issues with resolving and injecting services as lists. * Adds the `RegisterList[T]` method to enable the resolver to inject lists of services. While resolving lists of a specific service type was already possible by the `ResolveRequiredServices[T]` method, the consumption of arrays in constructor functions requires an explicit registration. The list registration can be mixed with named service registrations. -### Changes +### Changed * Changes the key-type used to register and lookup service registrations (uses `ServiceKey` instead of `reflect.Type`). @@ -55,7 +81,7 @@ This version addresses issues with resolving and injecting services as lists. ## [v0.6.1] - 2024-07-30 -### Changes +### Changed * Registers named services as transient services to resolve them also as a list of services (like services without a name). Changes the `createResolverRegistryAccessor` method so temporary registrations are selected first (and shadow permanent registrations). This behavior can also be leveraged in `ResolverOptionsFunc` to shadow other registrations when resolving instances via `ResolveWithOptions.` @@ -65,25 +91,30 @@ This version addresses issues with resolving and injecting services as lists. ### Added * Adds the `Activate[T]` method which can resolve an instance from an unregistered activator func. + * Allows registration and activation of pointer types (to not enforce usage of interfaces as abstractions). + * Adds the `RegisterNamed[T]` method to register services of the same interface and allow to resolve them by name. -### Changes +### Changed * Renames the `ServiceType[T]` method to `MakeServiceType[T]`; a service type represents now the reflected type and its name (which makes debugging and understanding service dependencies much easier). + * Replaces all usages of `reflect.Type` by `ServiceType` in all Parsley interfaces. + * Changes the `IsSame` method of the `ServiceRegistration` type; service registrations of type function are always treated as different service types. -### Fixes +### Fixed * Fixes a bug in the `detectCircularDependency` function which could make the method get stuck in an infinite loop. -## v0.5.0 - 2024-07-16 +## [v0.5.0] - 2024-07-16 ### Added * The service registry now accepts multiple registrations for the same interface (changes internal data structures to keep track of registrations; see `ServiceRegistrationList`). + * Adds the `ResolveRequiredServices[T]` convenience function to resolve all service instances; `ResolveRequiredService[T]` can resolve a single service but will return an error if service registrations are ambiguous. ### Changed @@ -91,7 +122,7 @@ This version addresses issues with resolving and injecting services as lists. * Extends the resolver to handle multiple service registrations per interface type. The resolver returns resolved objects as a list. -## v0.4.0 - 2024-07-13 +## [v0.4.0] - 2024-07-13 ### Added @@ -100,48 +131,60 @@ This version addresses issues with resolving and injecting services as lists. ### Changed * Reorganizes the whole package structure; adds sub-packages for `registration` and `resolving`. A bunch of types that support the inner functionality of the package have been moved to `internal.`. + * Integration tests are moved to the `internal` package. -## v0.3.0 - 2024-07-12 +## [v0.3.0] - 2024-07-12 ### Added * Service registrations can be bundled in a `ModuleFunc` to register related types as a unit. + * The service registry accepts object instances as singleton service registrations. + * Adds the `ResolveRequiredService[T]` convenience function that resolves and safe-casts objects. + * Registers resolver instance with the registry so that the `Resolver` object can be injected into factory and constructor methods. + * The resolver can now accept instances of non-registered types via the `ResolveWithOptions[T]` method. + * `ServiceRegistry` has new methods for creating linked and scoped registry objects (which share the same `ServiceIdSequence`). Scoped registries inherit all parent service registrations, while linked registries are empty. See `CreateLinkedRegistry` and `CreateScope` methods. ### Changed * A `ServiceRegistryAccessor` is no longer a `ServiceRegisty`, it is the other way around. + * The creation of service registrations and type activators has been refactored; see `activator.go` and `service_registration.go` modules. + * Multiple registries can be grouped with `NewMultiRegistryAccessor` to simplify the lookup of service registrations from linked registries. The resolver uses this accessor type to merge registered service types with object instances for unregistered types. -## v0.2.0 - 2024-07-11 +## [v0.2.0] - 2024-07-11 ### Added * The resolver can now detect circular dependencies. + * Adds helpers to register services with a certain lifetime scope. ### Changed * The registry rejects non-interface types. -### Fixes +### Fixed * Fixes error wrapping in custom error types. + * Improves error handling for service registry and resolver. -## v0.1.0 - 2024-07-10 +## [v0.1.0] - 2024-07-10 ### Added * Adds a service registry; the registry can map interfaces to implementation types via constructor functions. + * Assign lifetime behaviour to services (singleton, scoped, or transient). + * Adds a basic resolver (container) service. diff --git a/cmd/parsley-cli/main.go b/cmd/parsley-cli/main.go index 5ee17a1..ea86809 100644 --- a/cmd/parsley-cli/main.go +++ b/cmd/parsley-cli/main.go @@ -22,6 +22,7 @@ func main() { commands.NewGenerateGroupCommand(), func(w charmer.CommandSetup) { w.AddCommand(commands.NewGenerateProxyCommand()) + w.AddCommand(commands.NewGenerateMocksCommand()) }) app.Execute() diff --git a/go.mod b/go.mod index b353f6e..2f4cf5c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/matzefriedrich/parsley -go 1.22.4 +go 1.23 require ( github.com/matzefriedrich/cobra-extensions v0.2.6 diff --git a/internal/commands/generate_mocks_command.go b/internal/commands/generate_mocks_command.go new file mode 100644 index 0000000..b3bbdfd --- /dev/null +++ b/internal/commands/generate_mocks_command.go @@ -0,0 +1,41 @@ +package commands + +import ( + "fmt" + "github.com/matzefriedrich/cobra-extensions/pkg" + "github.com/matzefriedrich/cobra-extensions/pkg/abstractions" + "github.com/matzefriedrich/parsley/internal/generator" + "github.com/matzefriedrich/parsley/internal/templates" + "github.com/spf13/cobra" +) + +type mockGeneratorCommand struct { + use abstractions.CommandName `flag:"mocks" short:"Generate configurable mocks for interface types."` +} + +func (m *mockGeneratorCommand) Execute() { + + templateLoader := func(_ string) (string, error) { + return templates.MockTemplate, nil + } + + kind := "mocks" + gen, _ := generator.NewCodeFileGenerator(kind, func(config *generator.CodeFileGeneratorOptions) { + config.TemplateLoader = templateLoader + config.ConfigureModelCallback = func(m *generator.Model) { + m.AddImport("github.com/matzefriedrich/parsley/pkg/features") + } + }) + + err := gen.GenerateCode() + if err != nil { + fmt.Println(err) + } +} + +var _ pkg.TypedCommand = (*mockGeneratorCommand)(nil) + +func NewGenerateMocksCommand() *cobra.Command { + command := &mockGeneratorCommand{} + return pkg.CreateTypedCommand(command) +} diff --git a/internal/commands/generate_proxy_command.go b/internal/commands/generate_proxy_command.go index 1fe7d82..d4f2305 100644 --- a/internal/commands/generate_proxy_command.go +++ b/internal/commands/generate_proxy_command.go @@ -2,57 +2,35 @@ package commands import ( "fmt" - "github.com/matzefriedrich/parsley/internal/templates" - "os" - "path" - "path/filepath" - "strings" - "github.com/matzefriedrich/cobra-extensions/pkg" "github.com/matzefriedrich/cobra-extensions/pkg/abstractions" "github.com/matzefriedrich/parsley/internal/generator" + "github.com/matzefriedrich/parsley/internal/templates" "github.com/spf13/cobra" ) type generateProxyCommand struct { - use abstractions.CommandName `flag:"proxy" short:"GenerateProjectFiles generic proxy types for method call interception."` + use abstractions.CommandName `flag:"proxy" short:"Generate generic proxy types for method call interception."` } func (g *generateProxyCommand) Execute() { - goFilePath, err := generator.GetGoFilePath() - if err != nil { - fmt.Println(err) - return + templateLoader := func(_ string) (string, error) { + return templates.ProxyTemplate, nil } - gen := generator.NewGenericCodeGenerator(func(_ string) (string, error) { - return templates.ProxyTemplate, nil + kind := "proxy" + gen, _ := generator.NewCodeFileGenerator(kind, func(config *generator.CodeFileGeneratorOptions) { + config.TemplateLoader = templateLoader + config.ConfigureModelCallback = func(m *generator.Model) { + m.AddImport("github.com/matzefriedrich/parsley/pkg/features") + } }) - builder, err := generator.NewTemplateModelBuilder(goFilePath) + err := gen.GenerateCode() if err != nil { fmt.Println(err) - return } - - model, err := builder.Build() - if err != nil { - fmt.Println(err) - return - } - - model.AddImport("github.com/matzefriedrich/parsley/pkg/features") - - goFileName := path.Base(goFilePath) - goFileNameWithoutExtension := strings.TrimSuffix(goFileName, filepath.Ext(goFileName)) - goFileDirectory := path.Dir(goFilePath) - - targetFilePath := path.Join(goFileDirectory, fmt.Sprintf("%s.proxy.g.go", goFileNameWithoutExtension)) - f, _ := os.OpenFile(targetFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) - defer f.Close() - - gen.Generate("proxy", model, f) } var _ pkg.TypedCommand = &generateProxyCommand{} diff --git a/internal/commands/generator_command.go b/internal/commands/generator_command.go index b2025ca..a4ee8b3 100644 --- a/internal/commands/generator_command.go +++ b/internal/commands/generator_command.go @@ -7,7 +7,7 @@ import ( ) type generatorCommand struct { - use abstractions.CommandName `flag:"generate" short:"GenerateProjectFiles boilerplate code for advanced DI features."` + use abstractions.CommandName `flag:"generate" short:"Generate boilerplate code for advanced DI features."` } func (g *generatorCommand) Execute() { diff --git a/internal/generator/generator.go b/internal/generator/generator.go new file mode 100644 index 0000000..55d3caa --- /dev/null +++ b/internal/generator/generator.go @@ -0,0 +1,83 @@ +package generator + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" +) + +type codeFileGenerator struct { + options CodeFileGeneratorOptions +} + +type CodeFileGeneratorOptions struct { + TemplateLoader TemplateLoader + ConfigureModelCallback ModelConfigurationFunc + kind string +} + +type CodeFileGenerator interface { + GenerateCode() error +} + +type CodeFileGeneratorOptionsFunc func(config *CodeFileGeneratorOptions) + +func NewCodeFileGenerator(kind string, config ...CodeFileGeneratorOptionsFunc) (CodeFileGenerator, error) { + options := CodeFileGeneratorOptions{ + kind: kind, + } + for _, f := range config { + f(&options) + } + if options.TemplateLoader == nil { + return nil, fmt.Errorf("template loader is not set") + } + return &codeFileGenerator{options: options}, nil +} + +func (g *codeFileGenerator) GenerateCode() error { + + goFilePath, err := GetGoFilePath() + if err != nil { + return err + } + + gen := NewGenericCodeGenerator(g.options.TemplateLoader) + err = RegisterTemplateFunctions(gen, RegisterTypeModelFunctions, RegisterNamingFunctions) + if err != nil { + return err + } + + builder, err := NewTemplateModelBuilder(goFilePath) + if err != nil { + return err + } + + model, err := builder.Build() + if err != nil { + return err + } + + if g.options.ConfigureModelCallback != nil { + g.options.ConfigureModelCallback(model) + } + + goFileName := path.Base(goFilePath) + goFileNameWithoutExtension := strings.TrimSuffix(goFileName, filepath.Ext(goFileName)) + goFileDirectory := path.Dir(goFilePath) + + targetFilePath := path.Join(goFileDirectory, fmt.Sprintf("%s.%s.g.go", goFileNameWithoutExtension, g.options.kind)) + f, _ := os.OpenFile(targetFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + defer func(f *os.File) { + _ = f.Close() + }(f) + + err = gen.Generate(g.options.kind, model, f) + if err != nil { + return err + } + + return nil +} diff --git a/internal/generator/generator_errors.go b/internal/generator/generator_errors.go index 263ae11..ab08357 100644 --- a/internal/generator/generator_errors.go +++ b/internal/generator/generator_errors.go @@ -1,55 +1,43 @@ package generator -import "errors" +import ( + "errors" + "github.com/matzefriedrich/parsley/pkg/types" +) const ( - ErrorCannotGenerateProxies = "cannot generate proxies" - ErrorCannotExecuteTemplate = "cannot execute template" - ErrorFailedToOpenTemplateFile = "failed to open template file" - ErrorTemplateFileNotFound = "template file not found" + ErrorCannotExecuteTemplate = "cannot execute template" + ErrorCannotFormatGeneratedCode = "cannot format generated code" + ErrorCannotGenerateProxies = "cannot generate proxies" + ErrorFailedToOpenTemplateFile = "failed to open template file" + ErrorFailedToWriteGeneratedCode = "failed to write generated code" + ErrorTemplateFileNotFound = "template file not found" ) var ( - ErrCannotGenerateProxies = errors.New(ErrorCannotGenerateProxies) - ErrCannotExecuteTemplate = errors.New(ErrorCannotExecuteTemplate) - ErrFailedToOpenTemplateFile = errors.New(ErrorFailedToOpenTemplateFile) - ErrTemplateFileNotFound = errors.New(ErrorTemplateFileNotFound) + ErrCannotExecuteTemplate = errors.New(ErrorCannotExecuteTemplate) + ErrCannotFormatGeneratedCode = errors.New(ErrorCannotFormatGeneratedCode) + ErrCannotGenerateProxies = errors.New(ErrorCannotGenerateProxies) + ErrFailedToOpenTemplateFile = errors.New(ErrorFailedToOpenTemplateFile) + ErrFailedToWriteGeneratedCode = errors.New(ErrorFailedToWriteGeneratedCode) + ErrTemplateFileNotFound = errors.New(ErrorTemplateFileNotFound) ) type generatorError struct { - msg string - cause error -} - -func (g generatorError) Error() string { - return g.msg -} - -func (g generatorError) Unwrap() error { - return g.cause -} - -func (g generatorError) Is(err error) bool { - return g.Error() == err.Error() + types.ParsleyError } var _ error = &generatorError{} -func newGeneratorError(msg string, initializers ...func(error)) error { +func newGeneratorError(msg string, initializers ...types.ParsleyErrorFunc) error { err := &generatorError{ - msg: msg, + ParsleyError: types.ParsleyError{ + Msg: msg, + }, } for _, initializer := range initializers { + initializer(&err.ParsleyError) initializer(err) } return err } - -func WithCause(err error) func(target error) { - return func(target error) { - var errWithCause *generatorError - if errors.As(target, errWithCause) { - errWithCause.cause = err - } - } -} diff --git a/internal/generator/generic_generator.go b/internal/generator/generic_generator.go new file mode 100644 index 0000000..a757f62 --- /dev/null +++ b/internal/generator/generic_generator.go @@ -0,0 +1,115 @@ +package generator + +import ( + "bytes" + "github.com/matzefriedrich/parsley/pkg/types" + "github.com/pkg/errors" + "go/format" + "io" + "os" + "reflect" + "text/template" +) + +type genericGenerator struct { + templateLoader TemplateLoader + funcMap template.FuncMap +} + +type GenericCodeGenerator interface { + AddTemplateFunc(functions ...TemplateFunction) error + Generate(templateName string, model any, writer io.Writer) error +} + +var _ GenericCodeGenerator = (*genericGenerator)(nil) + +func NewGenericCodeGenerator(templateLoader TemplateLoader) GenericCodeGenerator { + g := &genericGenerator{ + templateLoader: templateLoader, + funcMap: template.FuncMap{}, + } + return g +} + +func (g *genericGenerator) AddTemplateFunc(functions ...TemplateFunction) error { + + addFunc := func(name string, f any) error { + + if len(name) == 0 { + return errors.New("function name cannot be empty") + } + reflected := reflect.ValueOf(f) + if reflected.Kind() != reflect.Func { + return errors.New("the given value is not a function") + } + + g.funcMap[name] = f + return nil + } + + for _, function := range functions { + if err := addFunc(function.Name, function.Function); err != nil { + return err + } + } + + return nil +} + +func (g *genericGenerator) Generate(templateName string, templateModel any, writer io.Writer) error { + + tmpl, err := g.templateLoader(templateName) + if err != nil { + return newGeneratorError(ErrorCannotGenerateProxies, types.WithCause(err)) + } + + var generatedCode bytes.Buffer + + t := template.Must(template.New("").Funcs(g.funcMap).Parse(tmpl)) + err = t.Execute(&generatedCode, templateModel) + if err != nil { + return newGeneratorError(ErrorCannotExecuteTemplate, types.WithCause(err)) + } + + formattedCode, formatErr := format.Source(generatedCode.Bytes()) + if formatErr != nil { + _, _ = writer.Write(formattedCode) // just dump the code to the target writer for inspection + return newGeneratorError(ErrorCannotFormatGeneratedCode, types.WithCause(formatErr)) + } + + _, writerErr := writer.Write(formattedCode) + if writerErr != nil { + return newGeneratorError(ErrorFailedToWriteGeneratedCode, types.WithCause(writerErr)) + } + + return nil +} + +func (g *genericGenerator) LoadTemplateFromFile(templateFile string) (string, error) { + + if _, err := os.Stat(templateFile); errors.Is(err, os.ErrNotExist) { + return "", newGeneratorError(ErrorTemplateFileNotFound, types.WithCause(err)) + } + + f, err := os.OpenFile(templateFile, os.O_RDONLY, 400) + defer func(file *os.File) { + _ = file.Close() + }(f) + + if err != nil { + return "", newGeneratorError(ErrorFailedToOpenTemplateFile, types.WithCause(err)) + } + + data, _ := io.ReadAll(f) + return string(data), nil +} + +func RegisterTemplateFunctions(g GenericCodeGenerator, functions ...func(generator GenericCodeGenerator) error) error { + for _, function := range functions { + err := function(g) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/generator/naming_functions.go b/internal/generator/naming_functions.go new file mode 100644 index 0000000..3757e3e --- /dev/null +++ b/internal/generator/naming_functions.go @@ -0,0 +1,31 @@ +package generator + +import "unicode" + +func RegisterNamingFunctions(generator GenericCodeGenerator) error { + return generator.AddTemplateFunc( + NamedFunc("asPrivate", MakePrivate), + NamedFunc("asPublic", MakePublic)) +} + +func MakePrivate(s string) string { + if s == "" { + return "" + } + firstRune := []rune(s)[0] + if unicode.IsUpper(firstRune) { + s = string(unicode.ToLower(firstRune)) + s[1:] + } + return s +} + +func MakePublic(s string) string { + if s == "" { + return "" + } + firstRune := []rune(s)[0] + if unicode.IsLower(firstRune) { + s = string(unicode.ToUpper(firstRune)) + s[1:] + } + return s +} diff --git a/internal/generator/proxy_generator.go b/internal/generator/proxy_generator.go deleted file mode 100644 index 42da0cc..0000000 --- a/internal/generator/proxy_generator.go +++ /dev/null @@ -1,58 +0,0 @@ -package generator - -import ( - "github.com/pkg/errors" - "io" - "os" - "text/template" -) - -type generator struct { - templateLoader TemplateLoader -} - -type GenericCodeGenerator interface { - Generate(templateName string, model any, writer io.Writer) error -} - -type TemplateLoader func(name string) (string, error) - -func NewGenericCodeGenerator(templateLoader TemplateLoader) GenericCodeGenerator { - return &generator{ - templateLoader: templateLoader, - } -} - -func (g *generator) Generate(templateName string, templateModel any, writer io.Writer) error { - tmpl, err := g.templateLoader(templateName) - if err != nil { - return errors.Wrap(err, ErrorCannotGenerateProxies) - } - - t := template.Must(template.New("").Parse(tmpl)) - err = t.Execute(writer, templateModel) - if err != nil { - return errors.Wrap(err, ErrorCannotExecuteTemplate) - } - - return nil -} - -func (g *generator) LoadTemplateFromFile(templateFile string) (string, error) { - - if _, err := os.Stat(templateFile); errors.Is(err, os.ErrNotExist) { - return "", newGeneratorError(ErrorTemplateFileNotFound, WithCause(err)) - } - - f, err := os.OpenFile(templateFile, os.O_RDONLY, 400) - defer func(file *os.File) { - _ = file.Close() - }(f) - - if err != nil { - return "", newGeneratorError(ErrorFailedToOpenTemplateFile, WithCause(err)) - } - - bytes, _ := io.ReadAll(f) - return string(bytes), nil -} diff --git a/internal/generator/template_func.go b/internal/generator/template_func.go new file mode 100644 index 0000000..301ef2e --- /dev/null +++ b/internal/generator/template_func.go @@ -0,0 +1,15 @@ +package generator + +type TemplateFunction struct { + Name string + Function any +} + +func NamedFunc(name string, f any) TemplateFunction { + return TemplateFunction{ + Name: name, + Function: f, + } +} + +type TemplateLoader func(name string) (string, error) diff --git a/internal/generator/proxy_model_builder.go b/internal/generator/type_model_builder.go similarity index 100% rename from internal/generator/proxy_model_builder.go rename to internal/generator/type_model_builder.go diff --git a/internal/generator/type_model_functions.go b/internal/generator/type_model_functions.go new file mode 100644 index 0000000..751bf67 --- /dev/null +++ b/internal/generator/type_model_functions.go @@ -0,0 +1,65 @@ +package generator + +import ( + "fmt" + "strings" +) + +func RegisterTypeModelFunctions(generator GenericCodeGenerator) error { + return generator.AddTemplateFunc( + NamedFunc("HasResults", HasResults), + NamedFunc("FormattedParameters", FormattedParameters), + NamedFunc("FormattedCallParameters", FormattedCallParameters), + NamedFunc("FormattedResultParameters", FormattedResultParameters), + NamedFunc("FormattedResultTypes", FormattedResultTypes), + NamedFunc("Signature", Signature)) +} + +func HasResults(m Method) bool { + return len(m.Results) > 0 +} + +func FormattedParameters(m Method) string { + formattedParameters := make([]string, len(m.Parameters)) + for i, parameter := range m.Parameters { + formattedParameters[i] = fmt.Sprintf("%s %s", parameter.Name, parameter.TypeName) + } + return strings.Join(formattedParameters, ", ") +} + +func FormattedCallParameters(m Method) string { + formattedParameters := make([]string, len(m.Parameters)) + for i, parameter := range m.Parameters { + formattedParameters[i] = fmt.Sprintf("%s", parameter.Name) + } + return strings.Join(formattedParameters, ", ") +} + +func FormattedResultParameters(m Method) string { + formattedResults := make([]string, len(m.Results)) + for i, result := range m.Results { + formattedResults[i] = fmt.Sprintf("%s", result.Name) + } + return strings.Join(formattedResults, ", ") +} + +func FormattedResultTypes(m Method) string { + formattedResults := make([]string, len(m.Results)) + for i, result := range m.Results { + formattedResults[i] = fmt.Sprintf("%s", result.TypeName) + } + if len(formattedResults) == 0 { + return "" + } + return "(" + strings.Join(formattedResults, ", ") + ")" +} + +func Signature(m Method) string { + buffer := strings.Builder{} + buffer.WriteString(fmt.Sprintf("%s", m.Name)) + buffer.WriteString(fmt.Sprintf("(%s)", FormattedParameters(m))) + if len(m.Results) > 0 { + buffer.WriteString(fmt.Sprintf(" %s", FormattedResultTypes(m))) + } + return buffer.String() +} diff --git a/internal/generator/types.go b/internal/generator/types.go index 90ca992..d391f79 100644 --- a/internal/generator/types.go +++ b/internal/generator/types.go @@ -1,7 +1,6 @@ package generator import ( - "fmt" "strings" ) @@ -20,45 +19,6 @@ type Method struct { Results []Parameter } -func (m Method) HasResults() bool { - return len(m.Results) > 0 -} - -func (m Method) FormattedParameters() string { - formattedParameters := make([]string, len(m.Parameters)) - for i, parameter := range m.Parameters { - formattedParameters[i] = fmt.Sprintf("%s %s", parameter.Name, parameter.TypeName) - } - return strings.Join(formattedParameters, ", ") -} - -func (m Method) FormattedCallParameters() string { - formattedParameters := make([]string, len(m.Parameters)) - for i, parameter := range m.Parameters { - formattedParameters[i] = fmt.Sprintf("%s", parameter.Name) - } - return strings.Join(formattedParameters, ", ") -} - -func (m Method) FormattedResultParameters() string { - formattedResults := make([]string, len(m.Results)) - for i, result := range m.Results { - formattedResults[i] = fmt.Sprintf("%s", result.Name) - } - return strings.Join(formattedResults, ", ") -} - -func (m Method) FormattedResultTypes() string { - formattedResults := make([]string, len(m.Results)) - for i, result := range m.Results { - formattedResults[i] = fmt.Sprintf("%s", result.TypeName) - } - if len(formattedResults) == 0 { - return "" - } - return "(" + strings.Join(formattedResults, ", ") + ")" -} - type Interface struct { Name string Methods []Method @@ -78,6 +38,8 @@ type Model struct { Imports []string } +type ModelConfigurationFunc func(m *Model) + func (m *Model) AddImport(s string) { m.Imports = append(m.Imports, s) } diff --git a/internal/templates/generator/method_interception.gotmpl b/internal/templates/generator/method_interception.gotmpl index 8cf41ca..75527ff 100644 --- a/internal/templates/generator/method_interception.gotmpl +++ b/internal/templates/generator/method_interception.gotmpl @@ -9,22 +9,23 @@ package {{.PackageName}} import ({{range $i, $path := .Imports}} "{{$path}}" {{end}}) + {{range $i, $interface := .Interfaces}} -// {{$interface.Name}}ProxyImpl A generated proxy service type for {{$interface.Name}} objects. -type {{$interface.Name}}ProxyImpl struct { +{{- $proxyTypeName := printf "%sProxyImpl" $interface.Name | asPrivate -}} +// {{$proxyTypeName}} A generated proxy service type for {{$interface.Name}} objects. +type {{$proxyTypeName}} struct { features.ProxyBase target {{$interface.Name}} } -{{ $proxyInterfaceTypeName := printf "%sProxy" $interface.Name }} +{{ $proxyInterfaceTypeName := printf "%sProxy" $interface.Name | asPublic -}} // {{$proxyInterfaceTypeName}} An interface type for {{$interface.Name}} objects. Parsley needs this to distinguish the proxy from the actual implementation. type {{$proxyInterfaceTypeName}} interface { {{$interface.Name}} } -{{ $proxyTypeName := printf "%sProxyImpl" $interface.Name }} -// New{{$proxyTypeName}} Creates a new {{$interface.Name}}Proxy object. Register this constructor method with the registry. -func New{{$proxyTypeName}}(target {{$interface.Name}}, interceptors []features.MethodInterceptor) {{$proxyInterfaceTypeName}} { +// New{{ $proxyTypeName | asPublic }} Creates a new {{$interface.Name}}Proxy object. Register this constructor method with the registry. +func New{{ $proxyTypeName | asPublic }}(target {{$interface.Name}}, interceptors []features.MethodInterceptor) {{$proxyInterfaceTypeName}} { return &{{$proxyTypeName}}{ ProxyBase: features.NewProxyBase(target, interceptors), target: target, @@ -32,7 +33,8 @@ func New{{$proxyTypeName}}(target {{$interface.Name}}, interceptors []features.M } {{end}}{{range $i, $interface := .Interfaces}}{{range $m, $method := .Methods}} -func (p *{{$interface.Name}}ProxyImpl) {{$method.Name}}({{$method.FormattedParameters}}) {{$method.FormattedResultTypes}} { +{{ $proxyTypeName := printf "%sProxyImpl" $interface.Name | asPrivate -}} +func (p *{{$proxyTypeName}}) {{$method.Name}}({{$method | FormattedParameters}}) {{$method | FormattedResultTypes}} { const methodName = "{{$method.Name}}" parameters := map[string]interface{}{ {{range @@ -45,13 +47,15 @@ func (p *{{$interface.Name}}ProxyImpl) {{$method.Name}}({{$method.FormattedParam defer func() { p.InvokeExitMethodInterceptors(callContext) }() - {{if $method.HasResults }} - {{$method.FormattedResultParameters}} := p.target.{{$method.Name}}({{$method.FormattedCallParameters}}) - p.InvokeMethodErrorInterceptors(callContext, {{$method.FormattedResultParameters}}) - return {{$method.FormattedResultParameters}}{{else}} - p.target.{{$method.Name}}({{$method.FormattedCallParameters}}){{end}} + {{if $method | HasResults }} + {{$method | FormattedResultParameters}} := p.target.{{$method.Name}}({{$method | FormattedCallParameters}}) + p.InvokeMethodErrorInterceptors(callContext, {{$method | FormattedResultParameters}}) + return {{$method | FormattedResultParameters}}{{else}} + p.target.{{$method.Name}}({{$method | FormattedCallParameters}}){{end}} } {{end}}{{end}} {{range - $i, $interface := .Interfaces}}var _ {{$interface.Name}} = &{{$interface.Name}}ProxyImpl{} + $i, $interface := .Interfaces}} +{{- $proxyTypeName := printf "%sProxyImpl" $interface.Name | asPrivate -}} +var _ {{$interface.Name}} = &{{$proxyTypeName}}{} {{end}} \ No newline at end of file diff --git a/internal/templates/generator/mocks.gotmpl b/internal/templates/generator/mocks.gotmpl new file mode 100644 index 0000000..466fef8 --- /dev/null +++ b/internal/templates/generator/mocks.gotmpl @@ -0,0 +1,98 @@ +{{- /*gotype: import "github.com/matzefriedrich/parsley/pkg/features" */ -}} +// Code generated by parsley-cli; DO NOT EDIT. +// +// This file was automatically generated and any changes to it will be overwritten. +{{ "\n" }} + +{{- /* Package declaration */ -}} +package {{ .PackageName }} + +{{ "" }} + +{{- /* Import statements */ -}} +{{- if .Imports }} +import ( + {{- range .Imports }} + "{{ . }}" + {{- end }} +) +{{ "" }} +{{- end }} + +{{- /* Loop over interfaces */ -}} +{{ range .Interfaces }} +{{- $interfaceName := .Name }} +{{- $mockStructName := printf "%sMock" (.Name | asPrivate) }} + +{{- /* Define the mock struct with func fields for each method */ -}} +type {{ $mockStructName }} struct { + features.MockBase + {{- range .Methods }} + {{ .Name | asPublic }}Func {{ .Name }}Func + {{- end }} +} + +{{- "\n" -}} + +{{- /* Define func types for each method */ -}} +{{- range .Methods }} +type {{ .Name }}Func func({{ FormattedParameters . }}) {{ FormattedResultTypes . }} +{{- end }} + +{{- "\n" -}} + +{{- if .Methods }} +const ( +{{- range .Methods }} + Function{{ .Name }} = "{{ .Name }}" +{{- end }} +) +{{ "" }} +{{- end }} + +{{- "\n" -}} + + +{{- /* Define methods for each function, implementing the interface */ -}} +{{ range .Methods }} +func (m *{{ $mockStructName }}) {{ .Name }}({{ FormattedParameters . }}) {{ FormattedResultTypes . }} { + m.TraceMethodCall(Function{{ .Name }}, {{ FormattedCallParameters . }}) + {{- if HasResults . }} + return m.{{ .Name | asPublic }}Func({{ FormattedCallParameters . }}) + {{- else }} + m.{{ .Name | asPublic }}Func({{ FormattedCallParameters . }}) + {{- end }} +} +{{ end }} + +{{- "\n" -}} + +{{- /* Interface implementation assertion */ -}} +var _ {{ $interfaceName }} = (*{{ $mockStructName }})(nil) + +{{ "" }} + +{{- /* Define a constructor for the mock */ -}} +// New{{ $interfaceName }}Mock Creates a new configurable {{ $mockStructName }} object. +func New{{ $interfaceName }}Mock() *{{ $mockStructName }} { + mock := &{{ $mockStructName }}{ + MockBase: features.NewMockBase(), + {{- range .Methods }} + {{- if HasResults . }} + {{ .Name | asPublic }}Func: func({{ FormattedParameters . }}) {{ FormattedResultTypes . }} { + {{- range .Results }} + var {{ .Name }} {{ .TypeName }} + {{- end}} + return {{ FormattedResultParameters . }} + }, + {{- else }} + {{ .Name | asPublic }}Func: func({{ FormattedParameters . }}) {{ FormattedResultTypes . }} {}, + {{- end }} + {{- end }} + } + {{- range .Methods }} + mock.AddFunction(Function{{.Name}}, "{{ Signature . }}") + {{- end }} + return mock +} +{{ end }} diff --git a/internal/templates/resources.go b/internal/templates/resources.go index 6123bda..73854d4 100644 --- a/internal/templates/resources.go +++ b/internal/templates/resources.go @@ -8,5 +8,8 @@ import ( //go:embed generator/method_interception.gotmpl var ProxyTemplate string +//go:embed generator/mocks.gotmpl +var MockTemplate string + //go:embed bootstrap/* var BootstrapTemplates embed.FS diff --git a/internal/tests/features/mock_test.go b/internal/tests/features/mock_test.go new file mode 100644 index 0000000..aef6d5f --- /dev/null +++ b/internal/tests/features/mock_test.go @@ -0,0 +1,31 @@ +package features + +import ( + "fmt" + "github.com/matzefriedrich/parsley/pkg/features" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_GreeterMock_SayHello(t *testing.T) { + + // Arrange + mock := NewGreeterMock() + mock.SayHelloFunc = func(name string) (string, error) { + return fmt.Sprintf("Hi, %s", name), nil + } + + const expectedName = "John" + + // Act + _, _ = mock.SayHello("Max") + actual, err := mock.SayHello(expectedName) + + // Assert + assert.NoError(t, err) + assert.Equal(t, "Hi, John", actual) + + assert.True(t, mock.Verify(FunctionSayHello, features.TimesOnce(), features.Exact(expectedName))) + assert.True(t, mock.Verify(FunctionSayHello, features.TimesNever(), features.Exact("Jane"))) + assert.True(t, mock.Verify(FunctionSayHello, features.TimesExactly(2))) +} diff --git a/internal/tests/features/types.go b/internal/tests/features/types.go index dc2140c..91d726b 100644 --- a/internal/tests/features/types.go +++ b/internal/tests/features/types.go @@ -1,6 +1,7 @@ package features //go:generate parsley-cli generate proxy +//go:generate parsley-cli generate mocks type Greeter interface { SayHello(name string) (string, error) diff --git a/internal/tests/features/types.mocks.g.go b/internal/tests/features/types.mocks.g.go new file mode 100644 index 0000000..a826988 --- /dev/null +++ b/internal/tests/features/types.mocks.g.go @@ -0,0 +1,51 @@ +// Code generated by parsley-cli; DO NOT EDIT. +// +// This file was automatically generated and any changes to it will be overwritten. + +package features + +import ( + "github.com/matzefriedrich/parsley/pkg/features" +) + +type greeterMock struct { + features.MockBase + SayHelloFunc SayHelloFunc + SayNothingFunc SayNothingFunc +} + +type SayHelloFunc func(name string) (string, error) +type SayNothingFunc func() + +const ( + FunctionSayHello = "SayHello" + FunctionSayNothing = "SayNothing" +) + +func (m *greeterMock) SayHello(name string) (string, error) { + m.TraceMethodCall(FunctionSayHello, name) + return m.SayHelloFunc(name) +} + +func (m *greeterMock) SayNothing() { + m.TraceMethodCall(FunctionSayNothing) + m.SayNothingFunc() +} + +var _ Greeter = (*greeterMock)(nil) + +// NewGreeterMock Creates a new configurable greeterMock object. +func NewGreeterMock() *greeterMock { + mock := &greeterMock{ + MockBase: features.NewMockBase(), + SayHelloFunc: func(name string) (string, error) { + var result0 string + var result1 error + return result0, result1 + }, + SayNothingFunc: func() {}, + } + mock.AddFunction(FunctionSayHello, "SayHello(name string) (string, error)") + mock.AddFunction(FunctionSayNothing, "SayNothing()") + return mock +} diff --git a/internal/tests/features/types.proxy.g.go b/internal/tests/features/types.proxy.g.go index 5f1ffe7..21feab2 100644 --- a/internal/tests/features/types.proxy.g.go +++ b/internal/tests/features/types.proxy.g.go @@ -1,66 +1,63 @@ // Code generated by parsley-cli; DO NOT EDIT. // // This file was automatically generated and any changes to it will be overwritten. -// To extend or modify the behavior of this code, implement the MethodInterceptor interface -// and provide your custom logic there. +// To extend or modify the behavior of this code, implement the MethodInterceptor interface and provide your custom logic there. package features import ( - "github.com/matzefriedrich/parsley/pkg/features" + "github.com/matzefriedrich/parsley/pkg/features" ) -// GreeterProxyImpl A generated proxy service type for Greeter objects. -type GreeterProxyImpl struct { - features.ProxyBase - target Greeter +// greeterProxyImpl A generated proxy service type for Greeter objects. +type greeterProxyImpl struct { + features.ProxyBase + target Greeter } - // GreeterProxy An interface type for Greeter objects. Parsley needs this to distinguish the proxy from the actual implementation. type GreeterProxy interface { - Greeter + Greeter } - // NewGreeterProxyImpl Creates a new GreeterProxy object. Register this constructor method with the registry. func NewGreeterProxyImpl(target Greeter, interceptors []features.MethodInterceptor) GreeterProxy { - return &GreeterProxyImpl{ - ProxyBase: features.NewProxyBase(target, interceptors), - target: target, - } + return &greeterProxyImpl{ + ProxyBase: features.NewProxyBase(target, interceptors), + target: target, + } } -func (p *GreeterProxyImpl) SayHello(name string) (string, error) { +func (p *greeterProxyImpl) SayHello(name string) (string, error) { - const methodName = "SayHello" - parameters := map[string]interface{}{ + const methodName = "SayHello" + parameters := map[string]interface{}{ "name": name, } callContext := features.NewMethodCallContext(methodName, parameters) p.InvokeEnterMethodInterceptors(callContext) defer func() { - p.InvokeExitMethodInterceptors(callContext) + p.InvokeExitMethodInterceptors(callContext) }() - - result0, result1 := p.target.SayHello(name) - p.InvokeMethodErrorInterceptors(callContext, result0, result1) - return result0, result1 + + result0, result1 := p.target.SayHello(name) + p.InvokeMethodErrorInterceptors(callContext, result0, result1) + return result0, result1 } -func (p *GreeterProxyImpl) SayNothing() { +func (p *greeterProxyImpl) SayNothing() { - const methodName = "SayNothing" - parameters := map[string]interface{}{ } + const methodName = "SayNothing" + parameters := map[string]interface{}{} callContext := features.NewMethodCallContext(methodName, parameters) p.InvokeEnterMethodInterceptors(callContext) defer func() { - p.InvokeExitMethodInterceptors(callContext) + p.InvokeExitMethodInterceptors(callContext) }() - - p.target.SayNothing() + + p.target.SayNothing() } -var _ Greeter = &GreeterProxyImpl{} +var _ Greeter = &greeterProxyImpl{} diff --git a/pkg/features/mock.go b/pkg/features/mock.go new file mode 100644 index 0000000..fe2f4a0 --- /dev/null +++ b/pkg/features/mock.go @@ -0,0 +1,41 @@ +package features + +type MockBase struct { + functions map[string]MockFunction +} + +type MockFunction struct { + name string + signature string + tracedCalls []methodCall +} + +type methodCall struct { + args []any +} + +func NewMockBase() MockBase { + return MockBase{ + functions: make(map[string]MockFunction), + } +} + +func (m *MockBase) AddFunction(name string, signature string) { + m.functions[name] = MockFunction{ + name: name, + signature: signature, + tracedCalls: make([]methodCall, 0), + } +} + +func (m *MockBase) TraceMethodCall(name string, arguments ...any) { + function, found := m.functions[name] + if found { + call := methodCall{ + args: arguments, + } + function.tracedCalls = append(function.tracedCalls, call) + m.functions[name] = function + } +} + diff --git a/pkg/features/mock_verify.go b/pkg/features/mock_verify.go new file mode 100644 index 0000000..d99fd13 --- /dev/null +++ b/pkg/features/mock_verify.go @@ -0,0 +1,70 @@ +package features + +type ArgMatch func(actual any) bool + +func IsAny() ArgMatch { + return func(actual any) bool { + return true + } +} + +func Exact[T comparable](expected T) ArgMatch { + return func(actual any) bool { + value, compatible := actual.(T) + if compatible && value == expected { + return true + } + return false + } +} + +type TimesFunc func(times int) bool + +func TimesOnce() TimesFunc { + return func(times int) bool { + return times == 1 + } +} + +func TimesAtLeastOnce() TimesFunc { + return func(times int) bool { + return times >= 1 + } +} + +func TimesExactly(n int) TimesFunc { + return func(times int) bool { + return times == n + } +} + +func TimesNever() TimesFunc { + return func(times int) bool { + return times == 0 + } +} + +func (m *MockBase) Verify(name string, times TimesFunc, matches ...ArgMatch) bool { + function, found := m.functions[name] + if found { + if len(matches) > 0 { + numMatches := 0 + callsLoop: + for _, call := range function.tracedCalls { + for _, arg := range call.args { + for _, doesMatch := range matches { + if doesMatch(arg) == false { + continue callsLoop + } + } + } + numMatches++ + } + return times(numMatches) + } else { + numCalls := len(function.tracedCalls) + return times(numCalls) + } + } + return false +} diff --git a/pkg/types/dependency_error.go b/pkg/types/dependency_error.go index 8a3afcc..710bddb 100644 --- a/pkg/types/dependency_error.go +++ b/pkg/types/dependency_error.go @@ -15,6 +15,6 @@ type DependencyError struct { } func NewDependencyError(msg string) error { - err := DependencyError{ParsleyError{msg: msg}} + err := DependencyError{ParsleyError{Msg: msg}} return err } diff --git a/pkg/types/parsley_error.go b/pkg/types/parsley_error.go index 9205af5..a1589f8 100644 --- a/pkg/types/parsley_error.go +++ b/pkg/types/parsley_error.go @@ -4,11 +4,11 @@ import "errors" type ParsleyError struct { cause error - msg string + Msg string } func (f ParsleyError) Error() string { - return f.msg + return f.Msg } func (f ParsleyError) Unwrap() error { diff --git a/pkg/types/reflection_error.go b/pkg/types/reflection_error.go index 3351d7a..995e7d0 100644 --- a/pkg/types/reflection_error.go +++ b/pkg/types/reflection_error.go @@ -7,7 +7,7 @@ type reflectionError struct { func NewReflectionError(msg string, initializers ...ParsleyErrorFunc) error { err := &reflectionError{ ParsleyError: ParsleyError{ - msg: msg, + Msg: msg, }, } for _, f := range initializers { diff --git a/pkg/types/registry_error.go b/pkg/types/registry_error.go index 077cd87..1946b15 100644 --- a/pkg/types/registry_error.go +++ b/pkg/types/registry_error.go @@ -33,7 +33,7 @@ var _ ParsleyErrorWithServiceTypeName = ®istryError{} func NewRegistryError(msg string, initializers ...ParsleyErrorFunc) error { err := ®istryError{ ParsleyError: ParsleyError{ - msg: msg, + Msg: msg, }, } for _, f := range initializers { diff --git a/pkg/types/resolver_error.go b/pkg/types/resolver_error.go index 8d0d155..c43ebc5 100644 --- a/pkg/types/resolver_error.go +++ b/pkg/types/resolver_error.go @@ -41,7 +41,7 @@ func (r *ResolverError) ServiceTypeName(name string) { func NewResolverError(msg string, initializers ...ParsleyErrorFunc) error { err := &ResolverError{ ParsleyError: ParsleyError{ - msg: msg, + Msg: msg, }, } for _, f := range initializers {