diff --git a/Makefile b/Makefile index 80073e7..719624a 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,12 @@ test: bench: go test -bench=. + +m: + rm -rf build/*.go + mkdir -p build + cgmerge + mv _merged.go build/ritsu.go + go fmt build/ritsu.go + sed -i '' -e 's/package main/package ritsu/' build/ritsu.go + diff --git a/README.md b/README.md index bd2ef66..9601a89 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Dew is a command bus library for Go, designed to enhance developer experience an ## Features -- **Lightweight**: Clocks around 600 LOC with minimalistic design. +- **Lightweight**: Clocks around 450 LOC with minimalistic design. - **Pragmatic and Ergonomic**: Focused on developer experience and productivity. - **Production Ready**: 100% test coverage. - **Zero Dependencies**: No external dependencies. diff --git a/command.go b/command.go index 92edd38..7976854 100644 --- a/command.go +++ b/command.go @@ -35,7 +35,7 @@ type CommandHandler[T Command] interface { // NewAction creates an object that can be dispatched. // It panics if the handler is not found. func NewAction[T Action](bus Bus, cmd *T) CommandHandler[T] { - h, mx := resolveHandler[T](ACTION, bus) + h, mx := resolveHandler[T](bus) return command[T]{ mux: mx, cmd: cmd, @@ -46,7 +46,7 @@ func NewAction[T Action](bus Bus, cmd *T) CommandHandler[T] { // NewQuery creates an object that can be dispatched. // It panics if the handler is not found. func NewQuery[T QueryAction](bus Bus, cmd *T) CommandHandler[T] { - h, mx := resolveHandler[T](QUERY, bus) + h, mx := resolveHandler[T](bus) return command[T]{ mux: mx, cmd: cmd, @@ -73,28 +73,9 @@ func (c command[T]) Mux() *Mux { return c.mux } -// resolveHandler locates a handler for a given operation type and command type within the provided Bus instance. -// It constructs a key from the command's reflect.Type, then searches the Mux's tree structure for a corresponding node. -// -// Parameters: -// - typ: The reflect.Type of the command for which a handler is being sought. -// - op: The operation type (ACTION or QUERY) under which the handler should be classified. -// - bus: The Bus instance where handlers are registered and organized. -// -// Returns: -// - *node: A pointer to the node struct representing the handler if found. -// - error: An error if no handler could be found for the provided type and operation. -// -// Example: -// -// handlerNode, err := resolveHandler(reflect.TypeOf(myCommand), ACTION, myBus) -// if err != nil { -// log.Fatalf("Handler resolution failed: %v", err) -// } func convertInterface[T any](i any) T { var v T - vp := unsafe.Pointer(&v) - reflect.NewAt(reflect.TypeOf(v), vp).Elem().Set(reflect.ValueOf(i)) + reflect.NewAt(reflect.TypeOf(v), unsafe.Pointer(&v)).Elem().Set(reflect.ValueOf(i)) return v } @@ -104,10 +85,12 @@ type entry struct { m *Mux } +// storeCache stores the handler in the cache. func storeCache[T Command](cache *syncMap, t reflect.Type, mx *Mux, handlerFunc HandlerFunc[T]) { cache.Store(t, entry{t: t, m: mx, p: unsafe.Pointer(&handlerFunc)}) } +// loadHandlerCache loads the handler from the cache. func loadHandlerCache[T Command](typ reflect.Type, mx *Mux) (HandlerFunc[T], *Mux, bool) { if v, ok := mx.cache.Load(typ); ok { e := v.(entry) @@ -117,23 +100,29 @@ func loadHandlerCache[T Command](typ reflect.Type, mx *Mux) (HandlerFunc[T], *Mu } // resolveHandler returns the handler and mux for the given command. -func resolveHandler[T Command](op OpType, bus Bus) (HandlerFunc[T], *Mux) { +func resolveHandler[T Command](bus Bus) (HandlerFunc[T], *Mux) { typ := typeFor[T]() mx := bus.(*Mux) - handler, mxx, ok := loadHandlerCache[T](typ, mx) + h, mxx, ok := loadHandlerCache[T](typ, mx) if ok { - return handler, mxx + return h, mxx } - key := keyForType(typ) - n := mx.tree.findRoute(op, key) - if n != nil { - h := n.handler.handler - hh := convertInterface[HandlerFunc[T]](h.handler) - storeCache[T](mx.cache, typ, h.mux, hh) - return hh, h.mux + entry, ok := mx.entries.Load(typ) + if ok { + hh := entry.(*handler) + hhh := convertInterface[HandlerFunc[T]](hh.handler) + storeCache[T](mx.cache, typ, hh.mux, hhh) + return hhh, hh.mux } - panic(fmt.Sprintf("handler not found for %s", key)) + panic(fmt.Sprintf("handler not found for %s/%s", typ.PkgPath(), typ.String())) +} + +type handler struct { + // handler is the function to call. + handler any + // mux is the mux that the handler belongs to. + mux *Mux } diff --git a/docs/source/index.rst b/docs/source/index.rst index 0b789b8..c675134 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Features * - Feature - Description * - 🚀 Lightweight - - Clocks around 600 LOC with minimalistic design. + - Clocks around 450 LOC with minimalistic design. * - 🌹 Pragmatic and Ergonomic - Focused on developer experience and productivity. * - ⚓️ Production Ready diff --git a/mux.go b/mux.go index ee4e39c..cd934aa 100644 --- a/mux.go +++ b/mux.go @@ -22,8 +22,8 @@ type Mux struct { parent *Mux inline bool lock sync.RWMutex + entries *sync.Map handler [ALL]Middleware - tree *node middlewares [mAll][]middleware mHandlers [mAll]func(ctx Context, fn mHandlerFunc) error cache *syncMap @@ -64,7 +64,7 @@ type mHandlerFunc func(ctx Context) error // newMux returns a newly initialized Mux object that implements the dispatcher interface. func newMux() *Mux { - mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux := &Mux{entries: &sync.Map{}, pool: &sync.Pool{}} mux.pool.New = func() interface{} { return NewContext() } @@ -228,7 +228,7 @@ func (mx *Mux) child() Bus { parent: mx, inline: true, middlewares: mws, - tree: mx.tree, + entries: mx.entries, cache: mx.cache, } } @@ -289,9 +289,9 @@ func (mx *Mux) Register(handler any) { if isHandlerMethod(mtdTyp) { cmdTyp := mtdTyp.Type.In(2).Elem() if cmdTyp.Implements(reflect.TypeOf((*Action)(nil)).Elem()) { - mx.addHandler(ACTION, cmdTyp, reflect.ValueOf(handler).Method(i).Interface()) + mx.addHandler(cmdTyp, reflect.ValueOf(handler).Method(i).Interface()) } else if cmdTyp.Implements(reflect.TypeOf((*QueryAction)(nil)).Elem()) { - mx.addHandler(QUERY, cmdTyp, reflect.ValueOf(handler).Method(i).Interface()) + mx.addHandler(cmdTyp, reflect.ValueOf(handler).Method(i).Interface()) } } } @@ -310,8 +310,8 @@ func (mx *Mux) setupHandler() { } } -func (mx *Mux) addHandler(op OpType, t reflect.Type, h any) { - mx.tree.insert(op, keyForType(t), &handler{handler: h, mux: mx}) +func (mx *Mux) addHandler(t reflect.Type, h any) { + mx.entries.Store(t, &handler{handler: h, mux: mx}) } // isHandlerMethod checks if the method is a Executor method. @@ -383,11 +383,6 @@ func filterMiddleware(op OpType, middlewares []middleware) []middleware { return mws } -// keyForType returns the key for the given type. -func keyForType(typ reflect.Type) string { - return fmt.Sprintf("%s:%s", typ.PkgPath(), typ.String()) -} - // typeFor returns the reflect.Type for the given type. func typeFor[T any]() reflect.Type { var t T diff --git a/tree.go b/tree.go deleted file mode 100644 index 6eea0bc..0000000 --- a/tree.go +++ /dev/null @@ -1,219 +0,0 @@ -package dew - -import ( - "sort" - "strings" -) - -type node struct { - // prefix is the common prefix we ignore. - prefix string - - // handler on the leaf node. - handler handlerEntry - - // child nodes should be stored in-order for iteration, - // in groups of the node type. - children nodes - - // first byte of the prefix. - label byte -} - -type handlerEntry struct { - handler *handler - op OpType -} - -func (h *handlerEntry) insert(op OpType, hh *handler) { - if h.handler != nil { - panic("dew: multiple handlers registered for the same command") - } - h.handler = hh - h.op = op -} - -func (h *handlerEntry) isEmpty() bool { - return h.handler == nil -} - -type handler struct { - // handler is the function to call. - handler any - // mux is the mux that the handler belongs to. - mux *Mux -} - -func (n *node) insert(op OpType, key string, handler *handler) *node { - var parent *node - search := key - - for { - // Handle key exhaustion - if len(search) == 0 { - n.setHandler(op, handler) - return n - } - - // We're going to be searching for a wild node next, - // in this case, we need to get the tail - var label = search[0] - - // Look for the edge to attach to - parent = n - n = n.getEdge(label) - - // No edge, create one - if n == nil { - // handler leaf node - hn := parent.addChild(&node{label: label, prefix: search}) - hn.setHandler(op, handler) - return hn - } - - // Determine the longest prefix of the search key on match. - commonPrefixLen := longestPrefixLen(search, n.prefix) - if commonPrefixLen == len(n.prefix) { - // the common prefix is as long as the current node's prefix we're attempting to insert. - // keep the search going. - search = search[commonPrefixLen:] - continue - } - - // Split the node - child := &node{ - prefix: search[:commonPrefixLen], - } - parent.replaceChild(search[0], child) - - // Restore the existing node - n.label = n.prefix[commonPrefixLen] - n.prefix = n.prefix[commonPrefixLen:] - child.addChild(n) - - // If the new key is a subset, set the method/handler on this node and finish. - search = search[commonPrefixLen:] - - // Create a new edge for the node - subchild := &node{ - label: search[0], - prefix: search, - } - hn := child.addChild(subchild) - hn.setHandler(op, handler) - return hn - } -} - -func (n *node) setHandler(op OpType, h *handler) { - n.handler.insert(op, h) -} - -// Recursive edge traversal by checking all nodeTyp groups along the way. -// It's like searching through a multi-dimensional radix trie. -func (n *node) findRoute(op OpType, key string) *node { - nn := n - search := key - - var xn *node - xsearch := search - - var label byte - if search != "" { - label = search[0] - } - - xn = nn.children.findEdge(label) - if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { - return nil - } - xsearch = xsearch[len(xn.prefix):] - - if len(xsearch) == 0 && xn.isLeaf() { - if xn.handler.handler != nil && xn.handler.op == op { - return xn - } - } - - // recursively find the next node. - return xn.findRoute(op, xsearch) -} - -func (n *node) isLeaf() bool { - return !n.handler.isEmpty() -} - -func (n *node) getEdge(label byte) *node { - nds := n.children - for i := 0; i < len(nds); i++ { - if nds[i].label == label { - return nds[i] - } - } - return nil -} - -// addChild append the new `child` node to the tree. -func (n *node) addChild(child *node) *node { - n.children = append(n.children, child) - n.children.Sort() - return child -} - -func (n *node) replaceChild(label byte, child *node) { - for i := 0; i < len(n.children); i++ { - if n.children[i].label == label { - n.children[i] = child - n.children[i].label = label - return - } - } - panic("dew: replacing missing child") -} - -type nodes []*node - -// Sort the list of nodes by label -func (ns nodes) Sort() { sort.Sort(ns) } -func (ns nodes) Len() int { return len(ns) } -func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } -func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } - -func (ns nodes) findEdge(label byte) *node { - if len(ns) == 0 { - return nil - } - num := len(ns) - idx := 0 - i, j := 0, num-1 - for i <= j { - idx = i + (j-i)/2 - if label > ns[idx].label { - i = idx + 1 - } else if label < ns[idx].label { - j = idx - 1 - } else { - i = num // breaks cond - } - } - if ns[idx].label != label { - return nil - } - return ns[idx] -} - -// longestPrefixLen finds the length of the shared prefix -// of two strings -func longestPrefixLen(k1, k2 string) int { - maxL := len(k1) - if l := len(k2); l < maxL { - maxL = l - } - var i int - for i = 0; i < maxL; i++ { - if k1[i] != k2[i] { - break - } - } - return i -} diff --git a/tree_test.go b/tree_test.go deleted file mode 100644 index 7042dcc..0000000 --- a/tree_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package dew - -import "testing" - -func TestTree(t *testing.T) { - tr := &node{} - createUser := &handler{} - createUser2 := &handler{} - createAccount := &handler{} - findUser := &handler{} - findUserByUserID := &handler{} - findUserByUserName := &handler{} - - tr.insert(ACTION, "CreateUser", createUser) - tr.insert(ACTION, "CreateUser2", createUser2) - tr.insert(ACTION, "CreateAccount", createAccount) - tr.insert(QUERY, "FindUser", findUser) - tr.insert(QUERY, "FindUserByUserID", findUserByUserID) - tr.insert(QUERY, "FindUserByUserName", findUserByUserName) - - tests := []struct { - o OpType - k string - h *handler - }{ - {ACTION, "CreateUser", createUser}, - {ACTION, "CreateUser2", createUser2}, - {QUERY, "CreateUser", nil}, // not found - {ACTION, "CreateAccount", createAccount}, - {QUERY, "FindUser", findUser}, - {QUERY, "FindUserByUserID", findUserByUserID}, - {QUERY, "FindUserByUserName", findUserByUserName}, - } - - for _, tt := range tests { - n := tr.findRoute(tt.o, tt.k) - if n == nil && tt.h != nil { - t.Fatalf("expected %s, got nil", tt.k) - } - if !(n == nil && (tt.h == nil)) && n.handler.handler != tt.h { - t.Errorf("exected %p, got %p", tt.h, n.handler.handler) - } - } -} - -func TestTree_Panic_DuplicateHandler(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("expected a panic") - } - }() - - tr := &node{} - tr.insert(ACTION, "CreateUser", &handler{}) - tr.insert(ACTION, "CreateUser", &handler{}) -} - -func TestTree_Panic_ReplacingMissingChild(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("expected a panic") - } - }() - tr := &node{} - tr.replaceChild(byte('a'), &node{}) -}