Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using mockery V3 to generate github.com/hexdigest/gowrap style interface wrappers #936

Open
breml opened this issue Mar 3, 2025 · 4 comments

Comments

@breml
Copy link

breml commented Mar 3, 2025

I tried to use mockery V3 in the new matryer/moq mode to generate interface wrappers in the style of https://github.com/hexdigest/gowrap and I made it work, but I found some rough edges, that I would like to mention here:

  • gowrap offers some helpers for usage in templates, that come in handy when generating interface wrappers, namely:
    • .HasParams and .HasResults on MethodData. With mockery I needed to write the condition {{ if gt ($method.Returns | len) 0 }} where in gowrap it is simply {{ if $method.HasResults }}
    • .AcceptsContext and .ReturnsError on MethodData. With mockery I needed create helper variables like this
      {{- $acceptsContext := false -}}
      {{- if and (gt ($method.Params | len) 0) (eq (index $method.Params 0).TypeString "context.Context") -}}
      {{- $acceptsContext = true -}}
      {{- end -}}
      
      and
      {{- $returnsError := false -}}
      {{- $lastType := "" -}}
      {{- range $method.Returns -}}
      {{- $lastType = .TypeString }}
      {{- end -}}
      {{- if eq $lastType "error" -}}
      {{- $returnsError = true -}}
      {{- end -}}
      
      in order to then be able to form conditions like {{- if $returnsError }}, which is cumbersome.
      What made this additionally cumbersome is, that Go templates do no provide basic arithmetic in order to get the last return value with ($method.Returns | len) - 1 (this would be possible with e.g. the math functions provided by sprig). Therefore I needed to range though the list only to get the last item.
  • In contrast to mocks, wrappers (or middlewares) also perform a call to the thing, that they wrap. With gowrap, there is a single instruction called $method.Pass, which accepts a prefix (basically the selector of the attribute of the wrapped instance). It returns the complete call to to the wrapped method including the arguments as well as the return statement, if the method does return anything. My workaround with mockery looks like this:
    {{ if gt ($method.Returns | len) 0 -}}
    return
    {{- end }} _d._base.{{ $method.Name }}({{ $method.ArgCallList }})
    
    which works as well, but is a little bit less nice and more verbose.
  • For the method declaration, in gowrap there is a helper {{$method.Declaration}} and with mockery I needed to do {{.Name}}({{.ArgList}}) ({{.ReturnArgList}}).
  • With gowrap, the name of the template is added as a comment to the generated file. I was not able to find a template variable, which contains the name of the Go template, that has been used to generate the source file. (example: https://github.com/hexdigest/gowrap/blob/master/templates_tests/interface_with_log.go#L2)
@breml
Copy link
Author

breml commented Mar 3, 2025

That being said, in regards to performance, the approach using mockery is way better, it improved the time to generate by a factor of ~10 when generating 91 files (mocks and wrappers) in two separate calls to mockery (one for the mocks, one for the wrappers).

@breml breml changed the title Using mockery V3 to generate https://github.com/hexdigest/gowrap style interface wrappers Using mockery V3 to generate github.com/hexdigest/gowrap style interface wrappers Mar 3, 2025
@breml
Copy link
Author

breml commented Mar 3, 2025

One more thing:
Since I now use mockery to generate Mocks as well as Wrappers and in the case of the Wrappers, I might even end up generating several wrappers for the same interface (e.g. one for logging, one for prometheus metrics), I now have multiple .mockery.yml files and I need to run mockery multiple times, where strictly speaking, it would be enough to just run it once in order to parse the source code and do the type resolving. Therefore it would be nice, if one could handle all of this in a single configuration file. Do you have any ideas on this?

@LandonTClipp
Copy link
Collaborator

This is a really interesting perspective. I knew of the gowrap project but I never explicitly made a goal of being able to replicate (in some form) what it does, or at least satisfy its primary use-case. I have no qualms with adding some convenience methods like what you mentioned, especially if it allows mockery to more robustly compete with gowrap.

From what I'm understanding, you are essentially wanting to run mockery with multiple separate templates, some for mocks, others for middlewares. The core problem with that idea is that each template will accept different template-data schemas, and the config model doesn't necessarily know how to deal with that. We could just provide a templates: key where you pass in a list of templates and the template-data: map could just be a commingled/combined schema. Or... the config model could be updated to something like:

templates:
  - name: "file://path/to/template.txt"
    data: #template-data
  - name: matryer
  - name: moq

I'm just bouncing random ideas around, I'm interested to hear your thoughts.

@breml
Copy link
Author

breml commented Mar 4, 2025

For me, it was always clear, that mockery could become a more generic tool for generating code based on interfaces, if it will be extended such that it operates on Go templates. I already mentioned this 2 years back in #715 (comment) (at the bottom).
Already back then, I was aware of the performance limitations of several tools, that operated on each interface individually (basically one go:generate line per interface), since this does not scale well in larger projects and therefore I saw the opportunity with the approach mockery is taking with a single call per repository (in the optimal case).

That being said, back to your questions:

  • Yes, you got this right. I have a repository (unfortunately currently private), where I have 3 different mockery configs, one for the mocks in moq style, one to generate wrappers for slog and one to generate wrappers for prometheus metrics (both gowrap style).
  • So far, I had no need to use the template-data map for these three cases. I think, it might be enough to just pass the same template-data map to all the defined templates. My assumption basically is, that if something from the map is required in multiple templates, it is actually nice to only have it once in the map. If distinct values are required, they don't hurt, in the cases, where they are not used.
  • So yes, simply having the possibility to list multiple templates, that are then processed in sequence would already help me quite a bit in order to reduce the calls to mockery from currently 3 to 1. That being said, in my case, the config files do differ in some more places:
    • I use include-regex to select the interfaces, I would like to generate code for. The definition for the two wrapping cases is the same, but for the mocking case, the include regex for the interfaces is slightly different.
    • For the dir config setting, I have a more involved definition, since I want to place the generated code in different locations based on the suffix of the interface name. Additionally, I place the mocks and the middlewares (wrappers) not in the same target directory. What I would basically need there is a way to access the template name (or less favorable the template's file name) in order to distinguish where the generated code should be located.
    • Similar to the above item, but less complicated, I also use different filename and mockname settings for the different cases.

Example .mockery.yaml for the mocks:

---

all: False
template: matryer
force-file-write: true
include-regex: "some regex to match the interfaces we would like to generate mocks for"
dir: >-
  {{- $targetDir := "mock" -}}
  {{- if and (.InterfaceName | hasSuffix "SomeSuffix") -}}
  {{- $targetDir = "some/mock" -}}
  {{- end -}}
  {{- if .InterfaceName | hasSuffix "OtherSuffix" -}}
  {{- $targetDir = "other/mock" -}}
  {{- end -}}
  {{- .InterfaceDir -}}/{{- $targetDir -}}
filename: "{{ .InterfaceName | snakecase }}_mock_gen.go"
mockname: "{{ .InterfaceName }}Mock"
pkgname: mock
packages:
  github.com/org/one:
  github.com/org/other:

and the an example for the wrappers:

---

all: False
template: file://./path/to/prometheus.gotmpl
force-file-write: true
include-regex: "some regex to match the interfaces we would like to generate wrappers for"
exclude-regex: "maybe even an exclude regex, since we do not need wrappers for these interfaces"
dir: >-
  {{- $targetDir := "middleware" -}}
  {{- if and (.InterfaceName | hasSuffix "SomeSuffix") -}}
  {{- $targetDir = "some/middleware" -}}
  {{- end -}}
  {{- if .InterfaceName | hasSuffix "OtherSuffix" -}}
  {{- $targetDir = "other/middleware" -}}
  {{- end -}}
  {{- .InterfaceDir -}}/{{- $targetDir -}}
filename: >-
  {{ .InterfaceName | snakecase }}_prometheus_gen.go
mockname: "{{ .InterfaceName }}WithPrometheus"
pkgname: middleware
packages:
  github.com/org/one:
  github.com/org/other:

After putting this together, I start to think, that maybe it is simpler to just add an additional (optional) top level key to the config file, which allows to combine the 3 config files into a single config file but allows all other settings to be distinct, similar to what you already proposed, but then not limited to template-data, but open for all the settings.

Something like:

templates:
  - name: "file://path/to/template.txt"
    config:
      include-regex:
      dir:
      filename:
      ...
  - name: matryer
      include-regex:
      dir:
      filename:
      ...   
  - name: moq
      include-regex:
      dir:
      filename:
      ...

One side note: with the "extended" use of mockery for generating wrappers or middlewares, the mention of mock in the config (e.g. mockname) but also in e.g. the template variables becomes a little bit awkward, but I guess this is not the biggest issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants