builder.go

Summary

Maintainability
A
0 mins
Test Coverage
package di

import (
    "errors"
    "fmt"
)

// Builder can be used to create a Container.
// The Builder should be created with NewBuilder.
// Then you can add definitions with the Add method,
// and finally build the Container with the Build method.
type Builder struct {
    definitions DefMap
    scopes      ScopeList
}

// NewBuilder is the only way to create a working Builder.
// It initializes a Builder with a list of scopes.
// The scopes are ordered from the most generic to the most specific.
// If no scope is provided, the default scopes are used:
// [App, Request, SubRequest]
// It can return an error if the scopes are not valid.
func NewBuilder(scopes ...string) (*Builder, error) {
    if len(scopes) == 0 {
        scopes = []string{App, Request, SubRequest}
    }

    if err := checkScopes(scopes); err != nil {
        return nil, err
    }

    return &Builder{
        definitions: DefMap{},
        scopes:      scopes,
    }, nil
}

func checkScopes(scopes []string) error {
    if len(scopes) == 0 {
        return errors.New("at least one scope is required")
    }

    for i, scope := range scopes {
        if scope == "" {
            return errors.New("a scope can not be an empty string")
        }
        if ScopeList(scopes[i+1:]).Contains(scope) {
            return fmt.Errorf("at least two scopes are identical")
        }
    }

    return nil
}

// Scopes returns the list of available scopes.
func (b *Builder) Scopes() ScopeList {
    return ScopeList(b.scopes).Copy()
}

// Definitions returns a map with the all the objects definitions
// registered with the Add method.
// The key of the map is the name of the Definition.
func (b *Builder) Definitions() DefMap {
    return b.definitions.Copy()
}

// IsDefined returns true if there is a definition with the given name.
func (b *Builder) IsDefined(name string) bool {
    _, ok := b.definitions[name]
    return ok
}

// Add adds one or more definitions in the Builder.
// It returns an error if a definition can not be added.
// If a definition with the same name has already been added,
// it will be replaced by the new one, as if the first one never existed.
func (b *Builder) Add(defs ...Def) error {
    for _, def := range defs {
        if err := b.add(def); err != nil {
            return err
        }
    }

    return nil
}

func (b *Builder) add(def Def) error {
    if def.Name == "" {
        return errors.New("name can not be empty")
    }

    // note that an empty scope is allowed
    // it will be replaced in the Build method by the most generic scope
    if def.Scope != "" && !b.scopes.Contains(def.Scope) {
        return fmt.Errorf("scope `%s` is not allowed", def.Scope)
    }

    if def.Build == nil {
        return errors.New("Build can not be nil")
    }

    b.definitions[def.Name] = def

    return nil
}

// Set is a shortcut to add a definition for an already built object.
func (b *Builder) Set(name string, obj interface{}) error {
    return b.add(Def{
        Name: name,
        Build: func(ctn Container) (interface{}, error) {
            return obj, nil
        },
    })
}

// Build creates a Container in the most generic scope
// with all the definitions registered in the Builder.
func (b *Builder) Build() Container {
    if err := checkScopes(b.scopes); err != nil {
        return nil
    }

    defs := b.Definitions()

    for name, def := range defs {
        if def.Scope == "" {
            def.Scope = b.scopes[0]
            defs[name] = def
        }
    }

    return &container{
        containerCore: &containerCore{
            scopes:       b.scopes,
            scope:        b.scopes[0],
            definitions:  defs,
            parent:       nil,
            children:     map[*containerCore]struct{}{},
            objects:      map[objectKey]interface{}{},
            dependencies: newGraph(),
        },
    }
}