Skip to content

Commit

Permalink
✨ (HOOKS) Create code to introduce hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Vitor Hugo committed Sep 8, 2023
1 parent 67cdfa2 commit f8bb2b9
Show file tree
Hide file tree
Showing 9 changed files with 527 additions and 107 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
61 changes: 61 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,git
# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,git

### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig

# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt

### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide

# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,git
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 🌩 Zeus - Simple Dependency Injection Container

![GitHub](https://img.shields.io/github/license/otoru/zeus)
![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/otoru/zeus)
[![codecov](https://codecov.io/gh/Otoru/zeus/graph/badge.svg?token=Yfkyp5NZsY)](https://codecov.io/gh/Otoru/zeus)

Zeus is a sleek and efficient dependency injection container for Go. Easily register "factories" (functions that create instances of types) and let zeus resolve those dependencies at runtime.
Expand All @@ -21,6 +22,10 @@ Register your dependencies and let zeus handle the rest.

Zeus detects and reports cycles in your dependencies to prevent runtime errors.

### 🪝 Hooks

Zeus supports lifecycle hooks, allowing you to execute functions at the start and end of your application. This is especially useful for setups and teardowns, like establishing a database connection or gracefully shutting down services.

## 🚀 Getting Started

### Installation
Expand Down Expand Up @@ -56,6 +61,55 @@ err := c.Run(func(s string) error {
})
```

### Using Hooks

Zeus allows you to register hooks that run at the start and end of your application. This is useful for setting up and tearing down resources.

```go
c := zeus.New()

// Servoce is a dummy service that depends on Hooks.
type Service struct{}

c.Provide(func(h zeus.Hooks) *Service {
h.OnStart(func() error {
fmt.Println("Starting up...")
return nil
})

h.OnStop(func() error {
fmt.Println("Shutting down...")
return nil
})
return &Service{}
})

c.Run(func(s *Service) {
fmt.Println("Main function running with the service!")
})

// Outputs:
// Starting up...
// Main function running with the service!
// Shutting down...

```

### Error Handling

Zeus uses `ErrorSet` to aggregate multiple errors. This is especially useful when multiple errors occur during the lifecycle of your application, such as during dependency resolution or hook execution.

An ErrorSet can be returned from the Run method. Here's how you can handle it:

```go
err := c.Run(func() { /* ... */ })
if es, ok := err.(*zeus.ErrorSet); ok {
for _, e := range es.Errors() {
fmt.Println(e)
}
}
```

## 🤝 Contributing

Contributions are warmly welcomed! Please open a PR or an issue if you find any problems or have enhancement suggestions.
110 changes: 68 additions & 42 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package zeus
import (
"reflect"

"github.com/otoru/zeus/errs"
"github.com/otoru/zeus/hooks"
"golang.org/x/exp/slices"
)

// Container holds the registered factories for dependency resolution.
type Container struct {
providers map[reflect.Type]reflect.Value
instances map[reflect.Type]reflect.Value
hooks Hooks
}

// New initializes and returns a new instance of the Container.
Expand All @@ -18,56 +21,24 @@ type Container struct {
//
// c := zeus.New()
func New() *Container {
hooks := new(hooks.HooksImpl)
providers := make(map[reflect.Type]reflect.Value, 0)
instances := make(map[reflect.Type]reflect.Value, 0)

container := new(Container)
container.hooks = hooks
container.providers = providers
container.instances = instances

return container
}

// Provide registers a factory function for dependency resolution.
// It ensures that the factory is a function, has a valid return type, and checks for duplicate factories.
// Returns an error if any of these conditions are not met.
//
// Example:
//
// c := zeus.New()
// c.Provide(func() int { return 42 })
func (c *Container) Provide(factory interface{}) error {
factoryType := reflect.TypeOf(factory)

if factoryType.Kind() != reflect.Func {
return NotAFunctionError{}
}

if numOut := factoryType.NumOut(); numOut < 1 || numOut > 2 {
return InvalidFactoryReturnError{NumReturns: numOut}
}

if factoryType.NumOut() == 2 && factoryType.Out(1).Name() != "error" {
return UnexpectedReturnTypeError{TypeName: factoryType.Out(1).Name()}
}

serviceType := factoryType.Out(0)

if _, exists := c.providers[serviceType]; exists {
return FactoryAlreadyProvidedError{TypeName: serviceType.Name()}
}

c.providers[serviceType] = reflect.ValueOf(factory)

return nil
}

// resolve attempts to resolve a dependency of the given type.
// It checks for cyclic dependencies and ensures that all dependencies can be resolved.
// Returns the resolved value and any error encountered during resolution.
func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value, error) {
if slices.Contains(stack, t) {
return reflect.Value{}, CyclicDependencyError{TypeName: t.Name()}
return reflect.Value{}, errs.CyclicDependencyError{TypeName: t.Name()}
}

if instance, exists := c.instances[t]; exists {
Expand All @@ -77,14 +48,20 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value
provider, ok := c.providers[t]

if !ok {
return reflect.Value{}, DependencyResolutionError{TypeName: t.Name()}
return reflect.Value{}, errs.DependencyResolutionError{TypeName: t.Name()}
}

providerType := provider.Type()
dependencies := make([]reflect.Value, providerType.NumIn())

for i := range dependencies {
argType := providerType.In(i)

if argType.Implements(reflect.TypeOf((*Hooks)(nil)).Elem()) {
dependencies[i] = reflect.ValueOf(c.hooks)
continue
}

argValue, err := c.resolve(argType, append(stack, t))

if err != nil {
Expand All @@ -105,6 +82,40 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value
return results[0], nil
}

// Provide registers a factory function for dependency resolution.
// It ensures that the factory is a function, has a valid return type, and checks for duplicate factories.
// Returns an error if any of these conditions are not met.
//
// Example:
//
// c := zeus.New()
// c.Provide(func() int { return 42 })
func (c *Container) Provide(factory interface{}) error {
factoryType := reflect.TypeOf(factory)

if factoryType.Kind() != reflect.Func {
return errs.NotAFunctionError{}
}

if numOut := factoryType.NumOut(); numOut < 1 || numOut > 2 {
return errs.InvalidFactoryReturnError{NumReturns: numOut}
}

if factoryType.NumOut() == 2 && factoryType.Out(1).Name() != "error" {
return errs.UnexpectedReturnTypeError{TypeName: factoryType.Out(1).Name()}
}

serviceType := factoryType.Out(0)

if _, exists := c.providers[serviceType]; exists {
return errs.FactoryAlreadyProvidedError{TypeName: serviceType.Name()}
}

c.providers[serviceType] = reflect.ValueOf(factory)

return nil
}

// Run executes the provided function by resolving and injecting its dependencies.
// It ensures that the function has a valid signature and that all dependencies can be resolved.
// Returns an error if the function signature is invalid or if dependencies cannot be resolved.
Expand All @@ -117,18 +128,20 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value
// fmt.Println(i) // Outputs: 42
// })
func (c *Container) Run(fn interface{}) error {
errorSet := &errs.ErrorSet{}

fnType := reflect.TypeOf(fn)

if fnType.Kind() != reflect.Func {
return NotAFunctionError{}
return errs.NotAFunctionError{}
}

if numOut := fnType.NumOut(); numOut > 1 {
return InvalidFactoryReturnError{NumReturns: numOut}
return errs.InvalidFactoryReturnError{NumReturns: numOut}
}

if fnType.NumOut() == 1 && fnType.Out(0).Name() != "error" {
return UnexpectedReturnTypeError{TypeName: fnType.Out(0).Name()}
return errs.UnexpectedReturnTypeError{TypeName: fnType.Out(0).Name()}
}

dependencies := make([]reflect.Value, fnType.NumIn())
Expand All @@ -138,17 +151,30 @@ func (c *Container) Run(fn interface{}) error {
argValue, err := c.resolve(argType, nil)

if err != nil {
return err
errorSet.Add(err)
break
}

dependencies[i] = argValue
}

if !errorSet.IsEmpty() {
return errorSet.Result()
}

if err := c.hooks.Start(); err != nil {
errorSet.Add(err)
}

results := reflect.ValueOf(fn).Call(dependencies)

if fnType.NumOut() == 1 && !results[0].IsNil() {
return results[0].Interface().(error)
errorSet.Add(results[0].Interface().(error))
}

return nil
if err := c.hooks.Stop(); err != nil {
errorSet.Add(err)
}

return errorSet.Result()
}
Loading

0 comments on commit f8bb2b9

Please sign in to comment.