lifetime

package
v0.0.0-...-155e729 Latest Latest
Warning

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

Go to latest
Published: May 2, 2024 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package lifetime provides a dependency injection framework called Lifetime.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Export

func Export[ConcreteComponentType any, Component any, InitParams any](
	lt *Lifetime[Component, InitParams],
	Params InitParams,
) ConcreteComponentType

Export initializes and returns the component of ConcreteComponentType from the lifetime.

ConcreteComponentType must be an a registered component of the lifetime. Params is passed to all Init functions that are being called.

Export may be safely called concurrently with other calls retrieving components.

func ExportSlice

func ExportSlice[ConcreteComponentType any, Component any, InitParams any](
	lt *Lifetime[Component, InitParams],
	Params InitParams,
) []ConcreteComponentType

ExportSlice initializes and returns all components that are a ConcreteComponentType from the lifetime.

ConcreteComponentType must be an interface that implements Component. Params is passed to all Init functions that are being called.

See Lifetime regarding order of the exported slice.

ExportSlice may be safely called concurrently with other calls retrieving components.

func Place

func Place[Concrete any, Component any, InitParams any](context *Registry[Component, InitParams])

Place is like Register, except that the Init function is always nil.

As such, the same restrictions as for Place apply. Place may only be called from within a call to lifetime.Lifetime.Register. Place may be safely called concurrently, even with calls to Register.

func Register

func Register[Concrete any, Component any, InitParams any](context *Registry[Component, InitParams], Init func(Concrete, InitParams))

Register registers a concrete component with a registry.

During component initialization, the component is first initialized to its' zero value. Then the Init parameter is called with the concrete component. During the call to Init the dependencies are not yet initialized or set. Init may be nil, in which case it is not called.

Register may only be called from within a call to lifetime.Lifetime.Register. Register may be safely called concurrently.

Types

type Lifetime

type Lifetime[Component any, InitParams any] struct {
	// Init is called on every component once it has been initialized, and all dependency references have been set.
	// There is no guarantee that the Init function has been called on dependent components.
	//
	// Init is called before any component is returned to a user.
	// There will only be one concurrent call to Init at any point.
	//
	// If Init is nil, it is not called.
	Init func(Component, InitParams)

	// Register is called by the Lifetime to register all components.
	// Register will be called at most once, and may not be nil.
	//
	// See [Registry] on how to register components.
	Register func(r *Registry[Component, InitParams])
	// contains filtered or unexported fields
}

Lifetime implements a dependency injection framework. Each type of component is treated as a singleton for purposes of a single lifetime.

Component must be an interface type, that should be implemented by various pointers to structs. Each type of struct is considered a singleton an initialized only once. By default components are initialized with their zero value. They may furthermore reference each other (even circularly). These references are set automatically.

For this purpose they may make use of a struct called "dependencies". Each field in this struct may be a pointer to a different component, or a slice of a specific component subtype. Components may also refer to other components using a field with an `inject: "auto"` struct tag.

Components must be registered using the Register function, see Registry for details. Components must be retrieved using lifetime.Lifetime.All, Export or ExportSlice.

When using slices of components (e.g. in dependencies or using the All or ExportSlice methods) their order is undefined by default. This means that multiple lifetimes (even with the same Component and Init functions) may return components in a different order. It is however guaranteed that the same lifetime struct always returns components in the same order.

Order of a specific slice type can be fixed by giving the slice element a method named "Rank${Typ}" with a signature func()T. T must be of kind int, uint, float or string. Slices of this type will then be sorted ascending by the appropriate "<" operator.

See the examples for concrete details.

Example (AIntro)

Introductory example on how to use a lifetime with two components.

//spellchecker:words lifetime
package main

//spellchecker:words github pkglib lifetime
import (
	"fmt"

	"github.com/tkw1536/pkglib/lifetime"
)

// Component is used as a type for components for a lifetime.
type Component interface {
	isComponent()
}

// Company is a Component
type Company struct {
	// Company declares its' dependencies using an embedded "dependencies" struct.
	// This can simply refer to other components and will be set automatically.
	dependencies struct {
		CEO *CEO
	}
}

// SayHello says hello from the company.
func (c *Company) SayHello() {
	// the CEO dependency will be automatically injected at runtime.
	// so the code can just call methods on it.
	c.dependencies.CEO.SayHello()
	fmt.Println("Hello from the company")
}

func (*Company) isComponent() {}

// CEO is another component
type CEO struct{}

func (*CEO) isComponent() {}

// SayHello says hello from the CEO.
// Notice the non-pointer receiver, meaning it will panic when the CEO is nil.
func (CEO) SayHello() {
	fmt.Println("Hello from the CEO")
}

// Introductory example on how to use a lifetime with two components.
func main() {

	// Create a new lifetime using the Component type
	lt := &lifetime.Lifetime[Component, struct{}]{
		// Register must register all components.
		// In this case we register the company and the CEO.
		Register: func(r *lifetime.Registry[Component, struct{}]) {
			lifetime.Place[*Company](r)
			lifetime.Place[*CEO](r)
		},
	}

	// To initialize and use a single component, we make use of the Export function.
	// Here it is invoked to retrieve a Company.
	company := lifetime.Export[*Company](lt, struct{}{})
	company.SayHello()

}
Output:

Hello from the CEO
Hello from the company
Example (BInjectTag)

Demonstrates the use of an inject that to declare dependencies.

//spellchecker:words lifetime
package main

//spellchecker:words github pkglib lifetime
import (
	"fmt"

	"github.com/tkw1536/pkglib/lifetime"
)

// House is a component with a public window.
type House struct {
	// Window is directly injected (outside of a dependencies struct).
	// This is achieved with the `inject:"true"` tag.
	Window *Window `inject:"true"`
}

func (*House) isComponent() {}

// Window is a component that has an open method.
type Window struct {
}

func (window *Window) isComponent() {}

// Open opens the window.
func (window *Window) Open() {
	fmt.Println("opening the window")
}

// Demonstrates the use of an inject that to declare dependencies.
func main() {
	// Same as before, register all components.
	lt := &lifetime.Lifetime[Component, struct{}]{
		Register: func(context *lifetime.Registry[Component, struct{}]) {
			lifetime.Place[*House](context)
			lifetime.Place[*Window](context)
		},
	}

	// we can now retrieve the house component.
	// The window is set automatically.
	house := lifetime.Export[*House](lt, struct{}{})
	house.Window.Open()

}
Output:

opening the window
Example (CMutual)

Demonstrates that components may be mutually dependent.

//spellchecker:words lifetime
package main

//spellchecker:words github pkglib lifetime
import (
	"fmt"

	"github.com/tkw1536/pkglib/lifetime"
)

// Declare two mutually dependent Odd and Even components.

type Odd struct {
	dependencies struct {
		Even *Even
	}
}

type Even struct {
	dependencies struct {
		Odd *Odd
	}
}

func (Odd) isComponent()  {}
func (Even) isComponent() {}

// Now declare two functions IsOdd and IsEven on their respective components.
// These make use of each other.

func (odd *Odd) IsOdd(value uint) bool {
	if value == 0 {
		return true
	}
	return !odd.dependencies.Even.IsEven(value - 1)
}

func (even *Even) IsEven(value uint) bool {
	if value == 0 {
		return true
	}
	return !even.dependencies.Odd.IsOdd(value - 1)
}

// Demonstrates that components may be mutually dependent.
func main() {
	// Again register both components.
	lt := &lifetime.Lifetime[Component, struct{}]{
		Register: func(context *lifetime.Registry[Component, struct{}]) {
			lifetime.Place[*Even](context)
			lifetime.Place[*Odd](context)
		},
	}

	// retrieve the Even component, the mutual dependencies are set correct.
	even := lifetime.Export[*Even](lt, struct{}{})
	fmt.Printf("42 is even: %t\n", even.IsEven(42))
	fmt.Printf("69 is even: %t\n", even.IsEven(69))

}
Output:

42 is even: true
69 is even: false
Example (DInitParam)

Demonstrates the use of an InitParam within a Lifetime.

//spellchecker:words lifetime
package main

//spellchecker:words github pkglib lifetime
import (
	"fmt"

	"github.com/tkw1536/pkglib/lifetime"
)

// Box is a component that depends on a secret.
type Box struct {
	dependencies struct {
		Secret *Secret
	}
}

func (*Box) isComponent() {}

// RevealSecret reveals the secret.
func (b *Box) RevealSecret() {
	fmt.Println("The secret is:", b.dependencies.Secret.value)
}

// Secret is a component that holds a value.
type Secret struct {
	value int
}

func (*Secret) isComponent() {}

// Demonstrates the use of an InitParam within a Lifetime.
func main() {
	// Declare a lifetime, same as before.
	// Notice that we now pass an additional parameter of type int.
	lt := &lifetime.Lifetime[Component, int]{
		Register: func(context *lifetime.Registry[Component, int]) {
			lifetime.Place[*Box](context)

			// We use the Register function to perform additional initialization.
			// Here, we store the passed parameter into the secret value.
			lifetime.Register(context, func(s *Secret, secret int) {
				s.value = secret
			})
		},
	}

	// Again retrieve the a component using the Export function.
	// This time, pass the additional parameter into all calls to Init.
	box := lifetime.Export[*Box](lt, 108)
	box.RevealSecret()

}
Output:

The secret is: 108
Example (ESlices)

Demonstrates the use of slices in dependencies.

//spellchecker:words lifetime
package main

//spellchecker:words slices github pkglib lifetime
import (
	"fmt"
	"slices"

	"github.com/tkw1536/pkglib/lifetime"
)

// ColorComponent a subtype of component that implements the color method.
type ColorComponent interface {
	Component
	Color() string
}

// Declare two color components Red and Green.

type Red struct{}

func (Red) isComponent()  {}
func (Red) Color() string { return "red" }

type Green struct{}

func (Green) isComponent()  {}
func (Green) Color() string { return "green" }

// Declare the wheel component
type Wheel struct {
	dependencies struct {
		// Wheel depends on all ColorComponents.
		// Like other components, these are also initialized automatically.
		Colors []ColorComponent
	}
}

func (Wheel) isComponent() {}

// Wheel itself is also a special color component.
func (Wheel) Color() string {
	return "rainbow"
}

// Colors returns the list of known colors in alphabetical order.
func (wheel *Wheel) Colors() []string {
	// retrieve all the colors
	colors := make([]string, 0, len(wheel.dependencies.Colors))
	for _, c := range wheel.dependencies.Colors {
		colors = append(colors, c.Color())
	}

	// since there is not a defined order for components, we sort them!
	slices.Sort(colors)
	return colors
}

// Demonstrates the use of slices in dependencies.
func main() {
	// Register components as normal.
	lt := &lifetime.Lifetime[Component, struct{}]{
		Register: func(context *lifetime.Registry[Component, struct{}]) {
			lifetime.Place[*Wheel](context)
			lifetime.Place[*Red](context)
			lifetime.Place[*Green](context)
		},
	}

	// retrieve the Wheel component and use it as expected.
	wheel := lifetime.Export[*Wheel](lt, struct{}{})
	fmt.Printf("wheel knows the following colors: %v\n", wheel.Colors())

}
Output:

wheel knows the following colors: [green rainbow red]
Example (FExportSlice)

Demonstrates the use of the ExportSlice function. Reuses types from Example E.

// Create a new lifetime which registers the same examples as in the previous example.
lt := &lifetime.Lifetime[Component, struct{}]{
	Register: func(context *lifetime.Registry[Component, struct{}]) {
		lifetime.Place[*Wheel](context)
		lifetime.Place[*Red](context)
		lifetime.Place[*Green](context)
	},
}

// this time retrieve multiple components using the ExportSlice function.
colors := lifetime.ExportSlice[ColorComponent](lt, struct{}{})

// sort them according to their color
slices.SortFunc(colors, func(a, b ColorComponent) int {
	return strings.Compare(a.Color(), b.Color())
})

// and print their colors
for _, c := range colors {
	fmt.Println(c.Color())
}
Output:

green
rainbow
red
Example (GExportSliceOrder)

Demonstrates the use of order when exporting slices

//spellchecker:words lifetime
package main

//spellchecker:words github pkglib lifetime
import (
	"fmt"

	"github.com/tkw1536/pkglib/lifetime"
)

// RankComponent is anything with a rank method
type RankComponent interface {
	Component
	Rank() string

	// a special function Rank${TypeName} can be used to sort slices of this type.
	//
	// The method must be named exactly like this, and have a signature func()T where
	// T is of kind float, int or string.
	// Slices are sorted increasingly using the appropriate "<" operator.
	RankRankComponent() int64
}

// Declare two color components Red and Green.

type Captain struct{}

func (Captain) isComponent() {}
func (Captain) RankRankComponent() int64 {
	return 0
}
func (Captain) Rank() string {
	return "Captain"
}

type Admiral struct{}

func (Admiral) isComponent() {}
func (Admiral) RankRankComponent() int64 {
	return 1
}
func (Admiral) Rank() string {
	return "Admiral"
}

// Demonstrates the use of order when exporting slices
func main() {
	// Register components as normal.
	lt := &lifetime.Lifetime[Component, struct{}]{
		Register: func(context *lifetime.Registry[Component, struct{}]) {
			lifetime.Place[*Admiral](context)
			lifetime.Place[*Captain](context)
		},
	}

	// export the ranks using ExportSlice.
	// The order is now guaranteed by the RankComponentWeight() function.
	ranks := lifetime.ExportSlice[RankComponent](lt, struct{}{})
	for _, r := range ranks {
		fmt.Println(r.Rank())
	}

}
Output:

Captain
Admiral
Example (HAllOrder)

Demonstrates that sorting slices applies to the Component type also. See also Example G.

//spellchecker:words lifetime
package main

//spellchecker:words reflect github pkglib lifetime
import (
	"fmt"
	"reflect"

	"github.com/tkw1536/pkglib/lifetime"
)

// Shape is a component type with a RankShape() method to rank it
type Shape interface {
	RankShape() string
}

type Square struct{}

func (Square) RankShape() string {
	return "0"
}

type Circle struct{}

func (Circle) RankShape() string {
	return "1"
}

type Triangle struct{}

func (Triangle) RankShape() string {
	return "2"
}

// Demonstrates that sorting slices applies to the Component type also.
// See also Example G.
func main() {

	// Create a lifetime with the square, circle and triangle components
	lt := &lifetime.Lifetime[Shape, struct{}]{
		Register: func(context *lifetime.Registry[Shape, struct{}]) {
			lifetime.Place[*Square](context)
			lifetime.Place[*Circle](context)
			lifetime.Place[*Triangle](context)
		},
	}

	// Retrieve all components and print their names.
	// The order is guaranteed to be consistent here.
	for _, shape := range lt.All(struct{}{}) {
		name := reflect.TypeOf(shape).Elem().Name()
		fmt.Println(name)
	}

}
Output:

Square
Circle
Triangle
Example (IInit)

Demonstrates the use of the Init hook. Reuses types from Example E.

// for purposes of this example keep a list of initialized colors.
var colors []string

lt := &lifetime.Lifetime[ColorComponent, struct{}]{
	// Init is called for each component after it has been initialized.
	// Here we just store that the color has been seen.
	Init: func(c ColorComponent, s struct{}) {
		colors = append(colors, c.Color())
	},
	Register: func(context *lifetime.Registry[ColorComponent, struct{}]) {
		lifetime.Place[*Wheel](context)
		lifetime.Place[*Red](context)
		lifetime.Place[*Green](context)
	},
}

// All initializes and retrieves all components.
// This will call the Init function above.
lt.All(struct{}{})

// Sort the colors we saw and print them out
slices.Sort(colors)
fmt.Printf("Initialized colors: %s\n", colors)
Output:

Initialized colors: [green rainbow red]

func (*Lifetime[Component, InitParams]) All

func (lt *Lifetime[Component, InitParams]) All(Params InitParams) []Component

All initializes and returns all registered components from the lifetime. Params is passed to all Init functions that are being called.

See Lifetime regarding order of the exported slice.

Export may be safely called concurrently with other calls retrieving components.

type Registry

type Registry[Component any, InitParams any] struct {
	// contains filtered or unexported fields
}

Registry allows registering components with a lifetime using lifetime.Lifetime.Register. The order in which components are registered is independent of their dependencies.

The Register function should register each component used within the lifetime. It should only consist of calls to Register and Place. It must not maintain a reference to the registry beyond the function call.

Directories

Path Synopsis
interal
lreflect
Package lreflect provides reflect extensions for use by the lifetime package.
Package lreflect provides reflect extensions for use by the lifetime package.
souls
Package souls implements component storage retrieval using the [Souls] struct.
Package souls implements component storage retrieval using the [Souls] struct.

Jump to

Keyboard shortcuts

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