Skip to content

Commit

Permalink
add box
Browse files Browse the repository at this point in the history
  • Loading branch information
aacebo committed Nov 16, 2024
1 parent 9172a86 commit 6cc9553
Show file tree
Hide file tree
Showing 4 changed files with 432 additions and 0 deletions.
24 changes: 24 additions & 0 deletions box/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Box

a dependency injection library

# Usage

```go
b := box.New()
b.Put(&ServiceA{}, &ServiceB{})

fn, err := b.Inject(func (a *ServiceA, b *Service B) {
fmt.Println(a, b)
})

if err != nil {
panic(err)
}

fn()
```

# Benchmarks

- coming soon!
187 changes: 187 additions & 0 deletions box/box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package box

import (
"context"
"encoding/json"
"errors"
"fmt"
"iter"
"maps"
"reflect"
"sync"
"time"
)

type Box struct {
context.Context

mu sync.RWMutex
items map[string]any
deadline time.Time
err error
}

func New() *Box {
return &Box{
mu: sync.RWMutex{},
items: map[string]any{},
}
}

func (self *Box) Len() int {
return len(self.items)
}

func (self *Box) Put(values ...any) {
self.mu.Lock()
defer self.mu.Unlock()

for _, value := range values {
t := reflect.ValueOf(value).Type()
self.items[t.String()] = value
}
}

func (self *Box) PutByKey(key string, value any) {
self.mu.Lock()
defer self.mu.Unlock()
self.items[key] = value
}

func (self *Box) Keys() iter.Seq[string] {
return func(yield func(string) bool) {
self.mu.RLock()
defer self.mu.RUnlock()

for key := range self.items {
if !yield(key) {
return
}
}
}
}

func (self *Box) Values() iter.Seq[any] {
return func(yield func(any) bool) {
self.mu.RLock()
defer self.mu.RUnlock()

for _, value := range self.items {
if !yield(value) {
return
}
}
}
}

func (self *Box) Items() iter.Seq2[string, any] {
return func(yield func(string, any) bool) {
self.mu.RLock()
defer self.mu.RUnlock()

for key, value := range self.items {
if !yield(key, value) {
return
}
}
}
}

func (self *Box) Deadline() (deadline time.Time, ok bool) {
self.mu.RLock()
defer self.mu.RUnlock()
return self.deadline, !self.deadline.IsZero()
}

func (self *Box) Done() <-chan struct{} {
return nil
}

func (self *Box) Err() error {
self.mu.RLock()
defer self.mu.RUnlock()
return self.err
}

func (self *Box) Value(key any) any {
self.mu.RLock()
defer self.mu.RUnlock()
return self.items[reflect.ValueOf(key).String()]
}

func (self *Box) Fork() *Box {
box := New()
box.items = maps.Clone(self.items)
return box
}

func (self *Box) Merge(box *Box) {
self.mu.Lock()
defer self.mu.Unlock()
maps.Copy(self.items, box.items)
}

func (self *Box) Inject(handler any) (func() (any, error), error) {
self.mu.RLock()
defer self.mu.RUnlock()

value := reflect.ValueOf(handler)

if !value.IsValid() {
return nil, errors.New("handler must be a function")
}

t := value.Type()

if t.Kind() != reflect.Func {
return nil, errors.New("handler must be a function")
}

in := make([]reflect.Value, t.NumIn())

for i := 0; i < t.NumIn(); i++ {
paramType := t.In(i)
item, exists := self.items[paramType.String()]

if !exists {
return nil, fmt.Errorf(
"no injectable value found for '%s'",
paramType.Name(),
)
}

in[i] = reflect.ValueOf(item)
}

return func() (any, error) {
ret := value.Call(in)

if len(ret) == 0 {
return nil, nil
}

if len(ret) == 1 {
return ret[0].Interface(), nil
}

if len(ret) == 2 {
return ret[0].Interface(), ret[1].Interface().(error)
}

return nil, fmt.Errorf(
"expected function to return at most 2 values, received %d",
len(ret),
)
}, nil
}

func (self *Box) String() string {
self.mu.RLock()
defer self.mu.RUnlock()
b, _ := json.Marshal(self.items)
return string(b)
}

func (self *Box) MarshalJSON() ([]byte, error) {
return json.Marshal(self.items)
}
174 changes: 174 additions & 0 deletions box/box_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package box_test

import (
"testing"

"github.com/thegogod/rum/box"
)

type A struct {
Hello string
}

type B struct {
World string
}

type C struct {
Test string
}

func TestBox(t *testing.T) {
t.Run("should inject", func(t *testing.T) {
b := box.New()
b.Put(&A{"world"}, &B{"hello"})

fn, err := b.Inject(func(a *A, b *B) {
if a.Hello != "world" || b.World != "hello" {
t.Fatal(a, b)
}
})

if err != nil {
t.Fatal(err)
}

fn()
})

t.Run("should error when handler nil", func(t *testing.T) {
b := box.New()
_, err := b.Inject(nil)

if err == nil {
t.FailNow()
}
})

t.Run("should error when handler not function", func(t *testing.T) {
b := box.New()
_, err := b.Inject("test")

if err == nil {
t.FailNow()
}
})

t.Run("should error when called with wrong argument types", func(t *testing.T) {
b := box.New()
b.Put(&A{"world"}, &A{"hello"})

_, err := b.Inject(func(a *A, b *B) {
if a.Hello != "world" || b.World != "hello" {
t.Fatal(a, b)
}
})

if err == nil {
t.FailNow()
}
})

t.Run("should error when called with wrong number of arguments", func(t *testing.T) {
b := box.New()
b.Put(&A{"world"})

_, err := b.Inject(func(a *A, b *B) {
if a.Hello != "world" || b.World != "hello" {
t.Fatal(a, b)
}
})

if err == nil {
t.FailNow()
}
})

t.Run("should get value", func(t *testing.T) {
b := box.New()
b.PutByKey("test", &A{"world"})

if _, ok := b.Value("test").(*A); !ok {
t.Fatal("expected singleton value")
}
})

t.Run("should get value by type", func(t *testing.T) {
b := box.New()
b.Put(&A{"world"})

a := box.Get[*A](b)

if a == nil {
t.Fatalf("expected singleton value")
}
})

t.Run("should get value by key", func(t *testing.T) {
b := box.New()
b.PutByKey("test", &A{"world"})

a := box.GetPath[*A](b, "test")

if a == nil {
t.Fatalf("expected singleton value")
}
})

t.Run("should get value by path", func(t *testing.T) {
b := box.New()
a := box.New()
a.PutByKey("hello", &A{Hello: "world"})
b.PutByKey("test", a)

c := box.GetPath[*A](b, "test", "hello")

if c == nil {
t.Fatalf("expected singleton value")
}
})

t.Run("should not have deadline", func(t *testing.T) {
b := box.New()

if _, ok := b.Deadline(); ok {
t.Fatalf("expected no deadline")
}
})

t.Run("should not have error", func(t *testing.T) {
b := box.New()

if err := b.Err(); err != nil {
t.Fatal(err)
}
})

t.Run("should fork", func(t *testing.T) {
b := box.New()
b.Put(&A{"world"}, &B{"hello"})

fork := b.Fork()
fork.Put(&C{"test"})

if b.Len() != 2 {
t.Fatalf("first box should have %d items, received %d", 2, b.Len())
}

if fork.Len() != 3 {
t.Fatalf("second box should have %d items, received %d", 3, fork.Len())
}
})
}

func BenchmarkBox(b *testing.B) {
container := box.New()
container.Put(&A{"a"}, &B{"b"}, &C{"c"})
fn, _ := container.Inject(func(a *A, b *B, c *C) {

})

for i := 0; i < b.N; i++ {
fn()
}
}
Loading

0 comments on commit 6cc9553

Please sign in to comment.