Skip to content

Repository files navigation

qdiimpl - Quick'n'Dirty Interface Implementation (for Golang)

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.

Install

go install github.com/rrgmc/qdiimpl@latest

Usage

$ 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.

Command line parameters

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

Samples

io.Reader

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 {
    methodName     string
    execCount      int
    callerFunc     string
    callerFile     string
    callerLine     int
    isNotSupported bool
}

// NotSupported should be called if the current callback don't support the passed arguments.
// The function return values will be ignored.
func (c *ReaderContext) NotSupported() {
    c.isNotSupported = true
}

func (c *ReaderContext) MethodName() string {
    return c.methodName
}

func (c *ReaderContext) ExecCount() int {
    return c.execCount
}

func (c *ReaderContext) CallerFunc() string {
    return c.callerFunc
}

func (c *ReaderContext) CallerFile() string {
    return c.callerFile
}

func (c *ReaderContext) CallerLine() int {
    return c.callerLine
}

type Reader struct {
    lock                   sync.Mutex
    execCount              map[string]int
    fallback               io.Reader
    onMethodNotImplemented func(qdCtx *ReaderContext, hasCallbacks bool) error
    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) {
    const methodName = "Read"
    for _, impl := range d.implRead {
        qctx := d.createContext(methodName)
        r0, r1 := impl(qctx, p)
        if !qctx.isNotSupported {
            d.addCallMethod(methodName)
            return r0, r1
        }
    }
    if d.fallback != nil {
        d.addCallMethod(methodName)
        return d.fallback.Read(p)
    }
    panic(d.methodNotImplemented(d.createContext(methodName), len(d.implRead) > 0))
}

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) addCallMethod(methodName string) {
    d.lock.Lock()
    defer d.lock.Unlock()
    d.execCount[methodName]++
}

func (d *Reader) createContext(methodName string) *ReaderContext {
    callerFunc, callerFile, callerLine := d.getCallerFuncName(3)
    d.lock.Lock()
    defer d.lock.Unlock()
    return &ReaderContext{
        methodName: methodName,
        execCount:  d.execCount[methodName],
        callerFunc: callerFunc,
        callerFile: callerFile,
        callerLine: callerLine,
    }
}

func (d *Reader) methodNotImplemented(qdCtx *ReaderContext, hasCallbacks bool) error {
    if d.onMethodNotImplemented != nil {
        if err := d.onMethodNotImplemented(qdCtx, hasCallbacks); err != nil {
            return err
        }
    }
    msg := "not implemented"
    if hasCallbacks {
        msg = "not supported by any callbacks"
    }
    return fmt.Errorf("[Reader] method '%s' %s", qdCtx.MethodName(), msg)
}

// Options

func WithFallback(fallback io.Reader) ReaderOption {
    return func(d *Reader) {
        d.fallback = fallback
    }
}

func WithOnMethodNotImplemented(m func(qdCtx *ReaderContext, hasCallbacks bool) error) ReaderOption {
    return func(d *Reader) {
        d.onMethodNotImplemented = m
    }
}

// WithRead implements [io.Reader.Read].
func WithRead(implRead func(qdCtx *ReaderContext, p []byte) (n int, err error)) ReaderOption {
    return func(d *Reader) {
        d.implRead = append(d.implRead, implRead)
    }
}

Usage:

func main() {
    reader := NewReader(
        WithRead(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)
}

Details

Naming

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().

QD Context

Each method is passed a "QDContext", which implements the qdii.Context interface:

  • MethodName(): name of the interface method that was called.
  • 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.
  • NotSupported(): signals that this handler cannot serve the call, so the return values are ignored and the next registered handler is tried.

It also has a Data field, a custom data value set by the WithData option (only when the data-type command line parameter is set).

Use these to help detect where the method was called from and return different responses if needed.

Author

Rangel Reale (rangelreale@gmail.com)

About

Quick'n'Dirty Interface Implementation

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages