dingo

package module
v4.2.0 Latest Latest
Warning

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

Go to latest
Published: Dec 24, 2021 License: MIT Imports: 12 Imported by: 21

README

DINGO

Generation of dependency injection containers for go programs (golang).

Dingo is a code generator. It generates dependency injection containers based on sarulabs/di.

It is better than sarulabs/di alone because:

  • Generated containers have typed methods to retrieve each object. You do not need to cast them before they can be used. That implies less runtime errors.
  • Definitions are easy to write. Some dependencies can be guessed, allowing shorter definitions.

The disadvantage is that the code must be generated. But this can be compensated by the use of a file watcher.

Table of contents

Build Status GoDoc codebeat badge goreport

Dependencies

This module depends on github.com/sarulabs/di/v2. You will need it to generate and use the dependency injection container.

Similarities with di

Dingo is very similar to sarulabs/di as it mainly a wrapper around it. This documentation mostly covers the differences between the two libraries. You probably should read the di documentation before going further.

Setup

Code structure

You will have to write the service definitions and register them in a Provider. The recommended structure to organize the code is the following:

- services/
    - provider/
        - provider.go
    - servicesA.go
    - servicesB.go

In the service files, you can write the service definitions:

// servicesA.go
package services

import (
    "github.com/sarulabs/dingo/v4"
)

var ServicesADefs = []dingo.Def{
    {
        Name: "definition-1",
        // ...
    },
    {
        Name: "definition-2",
        // ...
    },
}

In the provider file, the definitions are registered with the Load method.

// provider.go
package provider

import (
    "github.com/sarulabs/dingo/v4"
    services "YOUR_OWN_SERVICES_PACKAGE"
)

// Redefine your own provider by overriding the Load method of the dingo.BaseProvider.
type Provider struct {
    dingo.BaseProvider
}

func (p *Provider) Load() error {
    if err := p.AddDefSlice(services.ServicesADefs); err != nil {
        return err
    }
    if err := p.AddDefSlice(services.ServicesBDefs); err != nil {
        return err
    }
    return nil
}

An example of this can be found in the tests directory.

Generating the code

You will need to create your own command to generate the container. You can adapt the following code:

package main

import (
    "fmt"
    "os"

    "github.com/sarulabs/dingo/v4"
    provider "YOUR_OWN_PROVIDER_PACKAGE"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("usage: go run main.go path/to/output/directory")
        os.Exit(1)
    }

    err := dingo.GenerateContainer((*provider.Provider)(nil), os.Args[1])
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
}

Running the following command will generate the code in the path/to/generated/code directory:

go run main.go path/to/generated/code
Custom package name

If you want to customize the package name for the generated code you can use dingo.GenerateContainerWithCustomPkgName instead of dingo.GenerateContainer.

// The default package name is "dic", but you can replace it with the name of your choosing.
// Go files will be generated in os.Args[1]+"/dic/".
err := dingo.GenerateContainerWithCustomPkgName((*provider.Provider)(nil), os.Args[1], "dic")

Definitions

Name and scope

Dingo definitions are not that different from sarulabs/di definitions.

They have a name and a scope. For more information about scopes, refer to the documentation of sarulabs/di.

The default scopes are di.App, di.Request and di.SubRequest.

The Unshared field is also available (see sarulabs/di unshared objects).

Build based on a structure

Def.Build can be a pointer to a structure. It defines the type of the registered object.

type MyObject struct{}

dingo.Def{
    Name: "my-object",
    Build: (*MyObject)(nil),
}

You can use a nil pointer, like (*MyObject)(nil), but it is not mandatory. &MyObject{} is also valid.

If your object has fields that must be initialized when the object is created, you can configure them with Def.Params.

type OtherObject struct {
    FieldA *MyObject
    FieldB string
    FieldC int
}

dingo.Def{
    Name: "other-object",
    Build: (*OtherObject)(nil),
    Params: dingo.Params{
        "FieldA": dingo.Service("my-object"),
        "FieldB": "value",
    },
}

dingo.Params is a map[string]interface{}. The key is the name of the field. The value is the one that the associated field should take.

You can use dingo.Service to use another object registered in the container.

Some fields can be omitted like FieldC. In this case, the field will have the default value 0. But it may have a different behaviour. See the parameters section to understand why.

Build based on a function

Def.Build can also be a function. Using a pointer to a structure is a simple way to declare an object, but it lacks flexibility.

To declare my-object you could have written:

dingo.Def{
    Name: "my-object",
    Build: func() (*MyObject, error) {
        return &MyObject{}, nil
    },
}

It is similar to the Build function of sarulabs/di, but without the container as an input parameter, and with *MyObject instead of interface{} in the output.

To build the other-object definition, you need to use the my-object definition. This can be achieved with Def.Params:

dingo.Def{
    Name: "other-object",
    Build: func(myObject *MyObject) (*OtherObject, error) {
        return &OtherObject{
            FieldA: myObject,
            FieldB: "value",
        }, nil
    },
    Params: dingo.Params{
        "0": dingo.Service("my-object"),
    },
}

The build function can actually take as many input parameters as needed. In Def.Params you can define their values.

The key is the index of the input parameter.

To make things easier, there is the NewFuncParams that can generate the Params map without having to specify the keys:

dingo.Def{
    Name: "other-object",
    Build: func(myObject *MyObject, s string) (*OtherObject, error) {
        return &OtherObject{
            FieldA: myObject,
            FieldB: s,
        }, nil
    },
    // Same as dingo.Params{"0": dingo.Service("my-object"), "1": "value"}
    Params: dingo.NewFuncParams(
        dingo.Service("my-object"),
        "value"
    ),
}

Parameters

As explained before, the key of Def.Params is either the field name (for build structures) or the index of the input parameter (for build functions).

When an item is not defined in Def.Params, there are different situations:

  • If there is exactly one service of this type also defined in the container, its value is used.
  • If there is none, the default value for this type is used.
  • If there are more than one service with this type, the container can not be compiled. You have to specify the value for this parameter.

With these properties, it is possible to avoid writing some parameters. They will be automatically filled. This way you can have shorter definitions:

dingo.Def{
    Name: "other-object",
    Build: func(myObject *MyObject) (*OtherObject, error) {
        return &OtherObject{
            FieldA: myObject, // no need to write the associated parameter
            FieldB: "value",
        }, nil
    },
}

It works well for specific structures. But for basic types it can become a little bit risky. Thus it is better to only store pointers to structures in the container and avoid types like string or int.

It is possible to force the default value for a parameter, instead of using the associated object. You have to set the parameter with dingo.AutoFill(false):

dingo.Def{
    Name: "other-object",
    Build: (*OtherObject)(nil),
    Params: dingo.Params{
        // *MyObject is already defined in the container,
        // so you have to use Autofill(false) to avoid
        // using this instance.
        "FieldA": dingo.AutoFill(false),
    },
}

Close function

Close functions are identical to those of sarulabs/di. But they are typed. No need to cast the object anymore.

dingo.Def{
    Name: "my-object",
    Build: func() (*MyObject, error) {
        return &MyObject{}, nil
    },
    Close: func(obj *MyObject) error {
        // Close object.
        return nil
    }
}

Avoid automatic filling

Each definition in the container is a candidate to automatically fill another (if its parameters are not specified).

You can avoid that with Def.NotForAutoFill:

dingo.Def{
    Name: "my-object",
    Build: (*MyObject)(nil),
    NotForAutoFill: true,
}

This can be useful if you have more than one object of a given type, but one should be used by default to automatically fill the other definitions. Use Def.NotForAutoFill on the definition you do not want to use automatically.

Generated container

Basic container

The container is generated in the dic package inside the destination directory. The container is more or less similar to the one from sarulabs/di.

It implements this interface:

interface {
    Scope() string
    Scopes() []string
    ParentScopes() []string
    SubScopes() []string
    Parent() *Container
    SubContainer() (*Container, error)
    SafeGet(name string) (interface{}, error)
    Get(name string) interface{}
    Fill(name string, dst interface{}) error
    UnscopedSafeGet(name string) (interface{}, error)
    UnscopedGet(name string) interface{}
    UnscopedFill(name string, dst interface{}) error
    Clean() error
    DeleteWithSubContainers() error
    Delete() error
    IsClosed() bool
}

To create the container, there is the NewContainer function:

func NewContainer(scopes ...string) (*Container, error)

You need to specify the scopes. By default di.App, di.Request and di.SubRequest are used.

A NewBuilder function is also available. It allows you to redefine some services (Add and Set methods) before generating the container with its Build method. It is not recommended but can be useful for testing.

Additional methods

For each object, four other methods are generated. These methods are typed so it is probably the one you will want to use.

They match the SafeGet, Get, UnscopedSafeGet and UnscopedGet methods. They have the name of the definition as suffix.

For example for the my-object definition:

interface {
    SafeGetMyObject() (*MyObject, error)
    GetMyObject() *MyObject
    UnscopedSafeGetMyObject() (*MyObject, error)
    UnscopedGetMyObject() *MyObject
}

my-object has been converted to MyObject.

The name conversion follow these rules:

  • only letters and digits are kept
  • it starts with an uppercase character
  • after a character that is not a letter or a digit, there is another uppercase character

For example --apple--orange--2--PERRY-- would become AppleOrange2PERRY.

Note that you can not have a name beginning by a digit.

C function

There is also a C function in the dic package. Its role is to turn an interface into a *Container.

By default, the C function can:

  • cast a container into a *Container if it is possible
  • retrieve a *Container from the context of an *http.Request (the key being dingo.ContainerKey("dingo"))

This function can be redefined to fit your use case:

dic.C = func(i interface{}) *Container {
    // Find and return the container.
}

The C function is used in retrieval functions.

Retrieval functions

For each definition, another function is generated.

Its name is the formatted definition name. It takes an interface as input parameter, and returns the object.

For my-object, the generated function would be:

func MyObject(i interface{}) *MyObject {
    // ...
}

The generated function uses the C function to retrieve the container from the given interface. Then it builds the object.

It can be useful if you do not have the *Container but only an interface wrapping the container:

type MyContainer interface {
    // Only basic untyped methods.
    Get(string) interface{}
}

func (c MyContainer) {
    obj := c.Get("my-object").(*MyObject)

    // or

    obj := c.(*dic.Container).GetMyObject()

    // can be replaced by

    obj := dic.MyObject(c)
}

It can also be useful in an http handler. If you add a middleware to store the container in the request context with:

// Create a new request req, which is like request r but with the container in its context.
ctx := context.WithValue(r.Context(), dingo.ContainerKey("dingo"), container)
req := r.WithContext(ctx)

Then you can use it in the handler:

func (w http.ResponseWriter, r *http.Request) {
    // The function can find the container in r thanks to dic.C.
    // That is why it can create the object.
    obj := dic.MyObject(r)
}

Upgrade from v3

  • You need to register the definitions in a Provider. The dingo binary is also not available anymore as it would depends on the Provider now. So you have to write it yourself. See the Setup section.
  • dingo.App, dingo.Request and dingo.SubRequest have been removed. Use di.App, di.Request and di.SubRequest instead.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefNameIsAllowed

func DefNameIsAllowed(name string) error

DefNameIsAllowed returns an error if the definition name is not allowed.

func FormatDefName

func FormatDefName(name string) string

FormatDefName is the function used to turn the definition name into something that can be used in the generated container.

func FormatPkgName

func FormatPkgName(name string) string

FormatPkgName formats a package name by keeping only the letters.

func GenerateContainer

func GenerateContainer(provider Provider, outputDirectory string) error

GenerateContainer generates a depedency injection container. The definitions are loaded from the Provider. The code is generated in the outputDirectory.

func GenerateContainerWithCustomPkgName added in v4.1.0

func GenerateContainerWithCustomPkgName(provider Provider, outputDirectory, pkgName string) error

GenerateContainerWithCustomPkgName works like GenerateContainer but let you customize the package name in which files are generated. The package name must be a valid package name or the generation may fail unexpectedly.

Types

type AutoFill

type AutoFill bool

AutoFill can be used as Params value to avoid autofill (default is Autofill(true)). If a structure field is not in the Params map, the container will try to use a service from the container that has the same type. Setting the entry to AutoFill(false) will let the field empty in the structure.

type BaseProvider

type BaseProvider struct {
	// contains filtered or unexported fields
}

BaseProvider implements the Provider interface. It contains no definition, but you can use this to create your own Provider by redefining the Load method.

func (*BaseProvider) Add

func (p *BaseProvider) Add(i interface{}) error

Add adds definitions inside the Provider. Allowed types are: dingo.Def, *dingo.Def, []dingo.Def, []*dingo.Def func() dingo.Def, func() *dingo.Def, func() []dingo.Def, func() []*dingo.Def

func (*BaseProvider) AddDef

func (p *BaseProvider) AddDef(def Def) error

AddDef is the same as Add, but only for Def.

func (*BaseProvider) AddDefPtr

func (p *BaseProvider) AddDefPtr(def *Def) error

AddDefPtr is the same as Add, but only for *Def.

func (*BaseProvider) AddDefPtrSlice

func (p *BaseProvider) AddDefPtrSlice(defs []*Def) error

AddDefPtrSlice is the same as Add, but only for []*Def.

func (*BaseProvider) AddDefSlice

func (p *BaseProvider) AddDefSlice(defs []Def) error

AddDefSlice is the same as Add, but only for []Def.

func (*BaseProvider) Get

func (p *BaseProvider) Get(name string) (*Def, error)

Get returns the definition for a given service. If the definition does not exist, an error is returned.

func (*BaseProvider) Load

func (p *BaseProvider) Load() error

Load registers the service definitions. You need to override this method to add the service definitions. You can use the Add method to add a definition inside the provider.

func (*BaseProvider) Names

func (p *BaseProvider) Names() []string

Names returns the names of the definitions. The names are sorted by alphabetical order.

type ContainerKey

type ContainerKey string

ContainerKey is a type that can be used as key in a context.Context. For example it can be use if you want to store a container in the Context of an http.Request. It is used in the generated C function.

type Def

type Def struct {
	// Name is the key that is used to retrieve the object from the container.
	Name string
	// Scope determines in which container the object is stored.
	// Typical scopes are "app" and "request".
	Scope string
	// NotForAutoFill should be set to true if you
	// do not want to use this service automatically
	// as a dependency in other services.
	NotForAutoFill bool
	// Build defines the service constructor. It can be either:
	// - a pointer to a structure: (*MyStruct)(nil)
	// - a factory function: func(any, any, ...) (any, error)
	Build interface{}
	// Params are used to assist the service constructor.
	Params Params
	// Close should be a function: func(any) error.
	// With any being the type of the service.
	Close interface{}
	// Unshared is false by default. That means that the object is only created once in a given container.
	// They are singleton and the same instance will be returned each time "Get", "SafeGet" or "Fill" is called.
	// If you want to retrieve a new object every time, "Unshared" needs to be set to true.
	Unshared bool
	// Description is a text that describes the service.
	// If provided, the description is used in the comments of the generated code.
	Description string
}

Def is the structure containing a service definition.

type ParamInfo

type ParamInfo struct {
	Name                 string
	Index                string
	ServiceName          string
	Type                 reflect.Type
	TypeString           string
	UndefinedStructParam bool
	Def                  *ScannedDef
}

ParamInfo contains the parsed information about a parameter.

type ParamScanner

type ParamScanner struct {
	// contains filtered or unexported fields
}

ParamScanner helps the Scanner. It scans information about params.

func (*ParamScanner) Scan

func (s *ParamScanner) Scan(scan *Scan) error

Scan updates the given Scan with data about params.

type Params

type Params map[string]interface{}

Params are used to assist the service constructor. If the Def.Build field is a pointer to a structure, the keys of the map should be among the names of the structure fields. These fields will be filled with the associated value in the map. If the Def.Build field is a function, it works the same way. But the key of the map should be the index of the function parameters (e.g.: "0", "1", ...).

key=fieldName¦paramIndex value=any¦dingo.Service|dingo.AutoFill

func NewFuncParams added in v4.2.0

func NewFuncParams(params ...interface{}) Params

NewFuncParams creates a Params instance where the key of the map, is the index of the given parameter. It is usefull when using a Build function, to provide all the function parameters. e.g.: NewFuncParams("a", "b", "c") return Params{"0": "a", "1": "b", "2": "c"}

type Provider

type Provider interface {
	Load() error
	Names() []string
	Get(name string) (*Def, error)
}

Provider is the interface used to store the definitions. The provider is used while generating the dependency injection container, but also while executing the code of the container.

type Scan

type Scan struct {
	TypeManager          *TypeManager
	ImportsWithoutParams map[string]string
	Defs                 []*ScannedDef
	ProviderPackage      string
	ProviderName         string
}

Scan contains the parsed information about the service definitions.

type ScannedDef

type ScannedDef struct {
	Def              *Def
	Name             string
	FormattedName    string
	Scope            string
	ObjectType       reflect.Type
	ObjectTypeString string
	BuildIsFunc      bool
	BuildTypeString  string
	Params           map[string]*ParamInfo
	CloseTypeString  string
	Unshared         bool
}

ScannedDef contains the parsed information about a service definition.

func (*ScannedDef) BuildDependsOnRawDef

func (def *ScannedDef) BuildDependsOnRawDef() bool

BuildDependsOnRawDef returns true if the service constructor needs the definition contained in the Provider.

func (*ScannedDef) GenerateComment added in v4.1.1

func (def *ScannedDef) GenerateComment() string

GenerateComment returns the text used in the comments of the generated code.

func (*ScannedDef) GenerateCommentDescription added in v4.1.1

func (def *ScannedDef) GenerateCommentDescription() string

GenerateCommentDescription returns the description as it should be printed in the generated comments.

func (*ScannedDef) GenerateCommentParams added in v4.1.1

func (def *ScannedDef) GenerateCommentParams() string

GenerateCommentParams returns the params as they should be printed in the generated comments.

func (*ScannedDef) GenerateCommentScope added in v4.1.1

func (def *ScannedDef) GenerateCommentScope() string

GenerateCommentScope returns the scope as it should be printed in the generated comments.

func (*ScannedDef) ParamsString

func (def *ScannedDef) ParamsString() string

ParamsString returns the parameters as they should appear in a structure inside a go file.

type Scanner

type Scanner struct {
	Provider     Provider
	ParamScanner ParamScanner
	// contains filtered or unexported fields
}

Scanner analyzes the definitions provided by a Provider.

func (*Scanner) Scan

func (s *Scanner) Scan() (*Scan, error)

Scan creates the Scan for the Scanner definitions.

type Service

type Service string

Service can be used as Params value. It means that the field (or parameter) should be replaced by an other service. This service should be retrieved from the container.

type TypeManager

type TypeManager struct {
	// contains filtered or unexported fields
}

TypeManager maintains a list of all the import paths that are used in the types that it has registered. It associates a unique alias to all the import paths.

func (*TypeManager) Imports

func (tm *TypeManager) Imports() map[string]string

Imports returns a map with all the imports that are used in the registered types. The key is the import path and the value is the alias that has been given by the TypeManager.

func (*TypeManager) Register

func (tm *TypeManager) Register(t reflect.Type) (string, error)

Register adds a new type in the TypeManager.

Directories

Path Synopsis
tests
app

Jump to

Keyboard shortcuts

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