substate

package module
v0.0.0-...-fc4e2b7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2021 License: MIT Imports: 13 Imported by: 0

README

Substate

Go Go Report Card Go Doc

Substate provides the gensubstate tool which generates implementations of Dependency Injection interfaces.

Installation

Run go install github.com/nickcorin/substate/cmd/gensubstate.

NOTE: Make sure that your $GOBIN has been added to $PATH.

What are "Dependency Injection Interfaces"?

Consider that you're writing an application that contains some global state object which holds your client dependencies. It could look something like this:

// state.go

package state

type State struct {
    fooClient foo.Client  // Some interface to communicate with Foo.
    barClient bar.Client  // Some interface to communicate with Bar.
}


func New() (*State, error) {
    var s State

    ...

    fooClient, err := foo.NewClient()
    if err != nil {
        return nil, err
    }
    s.fooClient = fooClient

    ...

    return s, nil
}

func (s *State) FooClient() foo.Client {
    return s.fooClient
}

...

Providing these dependencies to your packages might look something like this:

// main.go

package main

import (
    "log"

    "project/bar"
    "project/foo"
    "project/state"
)

func main() {
    s, err := state.New()
    if err != nil {
        log.Fatal(err)
    }

    if err := foo.InteractWithBar(s); err != nil {
        log.Fatal(err)
    }

    if err := bar.InteractWithFoo(s); err != nil {
        log.Fatal(err)
    }
}
// foo.go

package foo

import (
    "project/state"
)

func InteractWithBar(s *state.State) error {
    return s.BarClient().Ping()
}
// bar.go

package bar

import (
    "project/state"
)

func InteractWithFoo(s *state.State) error {
    return s.FooClient().Ping()
}

The problem with this is that you're providing a FooClient to the foo package and a BarClient to the bar package. You've also introduced a plethora of transitive dependencies to each package - by importing the state package you're also effectively importing anything that it imports. This import list can become quite large since the state package will end up importing all your application's dependencies.

Additionally, to unit test this package you will need to import state and things can quickly become quite convoluted.

Introducing Substate.

In each sub-package that would normally require state, define a Substate interface which contains a subset of the accessor methods implemented on State.

// substate.go

package foo

import (
    "project/bar"
)

//go:generate gensubstate

type Substate interface {
    BarClient() bar.Client
}
// foo.go

package foo

func InteractWithFoo(s Substate) error {
    return s.BarClient().Ping()
}

You have now removed the need to import state anywhere within foo, while not needing to change the code in the main package since the methods in Substate are a subset of the methods implemented on the global State struct.

Great! But what about testing?

Using the gensubstate tool, running go generate on the foo package generates this file:

// substate_gen.go

// Code generated by gensubstate at foo/substate.go; DO NOT EDIT.

package foo

import (
    "testing"

    "project/bar"
)

// NewSubstateForTesting returns an implementation of Substate which can be used
// for testing.
func NewSubstateForTesting(_ *testing.TB, injectors ...Injector) *substate {
	var s substate

	for _, injector := range injectors {
		injector.Inject(&s)
	}

	return &s
}

type Injector interface {
	Inject(*substate)
}

// InjectorFunc defines a convenience type making it easy to implement
// Injectors.
type InjectorFunc func(*substate)

// Inject implements the Injector interface.
func (fn InjectorFunc) Inject(s *substate) {
	fn(s)
}

// WithBarClient returns an Injector which sets the barclient on substate.
func WithBarClient(barClient BarClient) InjectorFunc {
	return func(s *substate) {
		s.barClient = barClient
	}
}

type substate struct {
	barClient bar.Client
}

// BarClient implements the Substate interface.
func (s *substate) BarClient() bar.Client {
	return s.barClient
}

This might look quite confusing at first, that's okay!

Essentially, an unexposed struct is generated which implements your Substate interface along with some utility functions.

Let's see what a unit test might look like using this:

// foo_test.go

package foo_test

import (
    "testing"

    "github.com/stretchr/testify"

    "project/foo"
    "project/bar"
)

func TestInteractWithBar(t *testing.T) {
    mockClient := bar.NewMockClient()

    // Provide the Mock client to Substate using the generated InjectorFunc.
    s := foo.NewSubstateForTesting(t, foo.WithBarClient(mockClient))

    // You can now test InteractWithBar!
    err := foo.InteractWithBar(s)
    require.NoError(t, err)
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrFunctionReturnArgument is returned when the Substate interface
	// contains a method which returns a function.
	ErrFunctionReturnArgument = errors.New("function return args are not supported")

	// ErrMultipleReturnArguments is returned when the Substate interface
	// contains a method with multiple return arguments.
	ErrMultipleReturnArguments = errors.New("multiple return args are not supported")

	// ErrSubstateNotFound is returned when gensubstate is run in a file which
	// doesn't contain a Substate interface.
	ErrSubstateNotFound = errors.New("no substate interface found")
)

Functions

func Generate

func Generate(src, dest, typeName string) error

Generate reads the source file and generates a Substate implementation on a concrete type in order to be used to testing.

Types

type Field

type Field struct {
	// The name of the attribute within the generated struct.
	Name string

	// The name of the method in the Substate interface which should be
	// implemented on the generated struct.
	Method string

	// The Go type of the field.
	Type string

	// String of arguments the method expects.
	Params string

	// String of arguments the method returns.
	Results string
}

Field contains template data about a method in the Substate interface.

type Import

type Import struct {
	Alias string
	Path  string
}

Import contains metadata for a package import.

type TemplateData

type TemplateData struct {
	// The source of the go:generate directive.
	Source string

	// The package in which the output file will be written.
	Package string

	// The name of the generated type.
	TypeName string

	Imports []string
	Fields  []Field
}

TemplateData contains all data which is required by the template.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL