qdiimpl
is a Go generator cli that generates "Quick and Dirty Interface Implementations" meant for quick
debugging, absolutely not production-ready.
This is for that time when you want to test one feature that depends on lots of external service interfaces like databases, cloud storage, message queues, and you don't want to use the real thing just to test something that has nothing to do with these interfaces.
Another option would be using a mock, however outside of tests they are cumbersome to use because mocks need to set expectations and usually are limited by execution amounts.
go install github.com/rrgmc/qdiimpl@latest
$ cd app/pkg/client
$ go run github.com/rrgmc/qdiimpl@latest -type=StorageClient
Writing file storageclient_qdii.go...
There is an option for each interface method called WithMETHOD
to set a function that will be called when
the method is called. If a method is called when a function is not set, the implementation panics with a useful
message.
Usage of qdiimpl:
qdiimpl [flags] -type T [directory]
Flags:
-data-type any
add a data member of this type (e.g.: any, `package.com/data.XData`)
-export-type
whether to export the generated type (default false)
-force-package-name string
force generated package name
-name-prefix string
interface name prefix
-name-suffix string
interface name suffix
-option-name-prefix string
option name prefix (WithXXXMethod)
-output string
output file name; default srcdir/<type>_qdii.go
-overwrite
overwrite file if exists
-same-package
if false will import source package and qualify the types (default true)
-sync
use mutex to prevent concurrent accesses (default true)
-tags string
comma-separated list of build tags to apply
-type string
type name; must be set
-type-package string
type package path if not the current directory
go run github.com/rrgmc/qdiimpl@latest -type=Reader -type-package=io -force-package-name=main
File: reader_qdii.go
// Code generated by "qdiimpl"; DO NOT EDIT.
package main
import (
"fmt"
"io"
"runtime"
"sync"
)
type ReaderContext struct {
ExecCount int
CallerFunc string
CallerFile string
CallerLine int
}
type Reader struct {
lock sync.Mutex
execCount map[string]int
fallback io.Reader
implRead func(qdCtx *ReaderContext, p []byte) (n int, err error)
}
var _ io.Reader = (*Reader)(nil)
type ReaderOption func(*Reader)
func NewReader(options ...ReaderOption) io.Reader {
ret := &Reader{execCount: map[string]int{}}
for _, opt := range options {
opt(ret)
}
return ret
}
// Read implements [io.Reader.Read].
func (d *Reader) Read(p []byte) (n int, err error) {
if d.implRead == nil && d.fallback != nil {
return d.fallback.Read(p)
}
return d.implRead(d.createContext("Read", d.implRead == nil), p)
}
func (d *Reader) getCallerFuncName(skip int) (funcName string, file string, line int) {
counter, file, line, success := runtime.Caller(skip)
if !success {
panic("runtime.Caller failed")
}
return runtime.FuncForPC(counter).Name(), file, line
}
func (d *Reader) checkCallMethod(methodName string, implIsNil bool) (count int) {
if implIsNil {
panic(fmt.Errorf("[Reader] method '%s' not implemented", methodName))
}
d.lock.Lock()
defer d.lock.Unlock()
d.execCount[methodName]++
return d.execCount[methodName]
}
func (d *Reader) createContext(methodName string, implIsNil bool) *ReaderContext {
callerFunc, callerFile, callerLine := d.getCallerFuncName(3)
return &ReaderContext{ExecCount: d.checkCallMethod(methodName, implIsNil), CallerFunc: callerFunc, CallerFile: callerFile, CallerLine: callerLine}
}
// Options
func WithFallback(fallback io.Reader) ReaderOption {
return func(d *Reader) {
d.fallback = fallback
}
}
// WithRead implements [io.Reader.Read].
func WithRead(implRead func(qdCtx *ReaderContext, p []byte) (n int, err error)) ReaderOption {
return func(d *Reader) {
d.implRead = implRead
}
}
Usage:
func main() {
reader := NewReader(
WithReaderRead(func(qdCtx *ReaderContext, p []byte) (n int, err error) {
n = copy(p, []byte("test"))
return n, nil
}),
)
readInterface(reader)
}
func readInterface(r io.Reader) {
b := make([]byte, 10)
n, err := r.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("%d: %v\n", n, b)
}
By default, the implementation struct name will have the same name as the source interface.
If the implementation will be generated in the same folder as the source interface, add the -name-prefix=QD
option
to prefix all generated data with a "QD" prefix.
You may also need to use the -option-name-prefix=QD
options to make function option names be prefixed,
so WithRead()
would become WithQDRead()
.
Each method is passed a "QDContext" which contains these fields:
ExecCount
: times this method was called since the execution start.CallerFunc
: fully-qualified function name that called the interface method.CallerFile
: source file name of the function that called the interface method.CallerLine
: line number of the source file of the function that called the interface method.Data
: a custom data field set byWithData
option. (only whendata-type
command line parameter is set)
Use these properties to help detect where the method was called from and return different responses if needed.
Rangel Reale (rangelreale@gmail.com)