Skip to content

Commit

Permalink
Feature/mocks template (#21)
Browse files Browse the repository at this point in the history
* Renames proxy generator and model builder modules
* Refactors the generate_proxy_command.go module; adds new generic (code-file) generator.go module
* Adds new mocks generator command
* Removes functions from the type model; registers template functions instead. Adopts changes to the proxy generator template.
* Adds error handling to the generate_mocks_command.go module
* The generator now formats generated code in canonical go fmt style
* Exports the Msg field of the ParsleyError type, and fixes error wrapping in the generic_generator.go module. Extends the mocks.gotmpl template; derives generated mocks from MockBase to trace method calls.
* Adds mock verification functionality
  • Loading branch information
matzefriedrich authored Sep 8, 2024
1 parent 267070d commit 8533007
Show file tree
Hide file tree
Showing 29 changed files with 785 additions and 225 deletions.
63 changes: 53 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.


Expand All @@ -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`).

Expand All @@ -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.`

Expand All @@ -65,33 +91,38 @@ 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

* 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

Expand All @@ -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.
1 change: 1 addition & 0 deletions cmd/parsley-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func main() {
commands.NewGenerateGroupCommand(),
func(w charmer.CommandSetup) {
w.AddCommand(commands.NewGenerateProxyCommand())
w.AddCommand(commands.NewGenerateMocksCommand())
})

app.Execute()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
41 changes: 41 additions & 0 deletions internal/commands/generate_mocks_command.go
Original file line number Diff line number Diff line change
@@ -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)
}
44 changes: 11 additions & 33 deletions internal/commands/generate_proxy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/generator_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
83 changes: 83 additions & 0 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 8533007

Please sign in to comment.