-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
432 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.