├── errors.go ├── handler.go ├── dispatcher.go ├── README.md ├── handler_test.go ├── LICENSE ├── serialdispatcher.go ├── paralleldispatcher.go ├── serialdispatcher_test.go └── paralleldispatcher_test.go /errors.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "fmt" 4 | 5 | // NoHandlerFoundError is returned when no handler returns `true` for 6 | // the `CanHandle` call. 7 | type NoHandlerFoundError struct { 8 | Command interface{} 9 | } 10 | 11 | func (e *NoHandlerFoundError) Error() string { 12 | return fmt.Sprintf("No handler can handle %T", e.Command) 13 | } 14 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // Handler defines the interface for a command handler. 4 | type Handler interface { 5 | // CanHandle should return `true` whenever the given command can be handled 6 | // by this Handler, otherwise it should return `false` 7 | CanHandle(cmd interface{}) bool 8 | 9 | // Handle does all the *work* realted to the given command. 10 | // 11 | // It will only be called if CanHandle returns `true` for the given command. 12 | Handle(cmd interface{}, dispatcher Dispatcher) error 13 | } 14 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // Dispatcher defines the interface for a command dispatcher. 4 | type Dispatcher interface { 5 | // Dispatch dispatches the given command to all known handlers. 6 | // 7 | // If no handler returns `true` for `CanHandle` call it will return a 8 | // `NoHandlerFoundError`. 9 | Dispatch(cmd interface{}) error 10 | 11 | // DispatchOptional works the same way as `Dispatch` but it will not return an 12 | // error if all handler's `CanHandle` call returns `false`. 13 | DispatchOptional(cmd interface{}) error 14 | 15 | // AppendHandlers append the given handler to the list of known handlers. 16 | // 17 | // This is useful if you want to add more handler after creating a dispatcher. 18 | AppendHandlers(handlers ...Handler) 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/txgruppi/command) 2 | ![Codeship](https://img.shields.io/codeship/50000bb0-c216-0133-5b48-6a927043ead9.svg?style=flat-square) 3 | [![Codecov](https://img.shields.io/codecov/c/github/txgruppi/command.svg?style=flat-square)](https://codecov.io/github/txgruppi/command) 4 | [![Go Report Card](https://img.shields.io/badge/go_report-A+-brightgreen.svg?style=flat-square)](https://goreportcard.com/report/github.com/txgruppi/command) 5 | 6 | # Command 7 | 8 | Command pattern for Go with **thread safe serial and parallel dispatcher**. 9 | 10 | ## Installation 11 | 12 | ``` 13 | go get -u github.com/txgruppi/command 14 | ``` 15 | 16 | ## Tests 17 | 18 | ``` 19 | go get -u -t github.com/txgruppi/command 20 | cd $GOPATH/src/github.com/txgruppi/command 21 | go test ./... 22 | ``` 23 | 24 | ## License 25 | 26 | MIT 27 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "github.com/txgruppi/command" 5 | ) 6 | 7 | type Command uint16 8 | 9 | const ( 10 | CommandA Command = iota + 1 11 | CommandB 12 | CommandC 13 | ) 14 | 15 | func NewCanHandleCallbackForCommand(cmd Command) func(interface{}) bool { 16 | return func(icmd interface{}) bool { 17 | if icmd == nil { 18 | return false 19 | } 20 | if c, ok := icmd.(Command); ok { 21 | return c == cmd 22 | } 23 | return false 24 | } 25 | } 26 | 27 | type TestHandler struct { 28 | CanHandleCallback func(interface{}) bool 29 | HandleCallback func(interface{}, command.Dispatcher) error 30 | CanHandleCallCount int 31 | HandleCallCount int 32 | } 33 | 34 | func (h *TestHandler) CanHandle(cmd interface{}) bool { 35 | h.CanHandleCallCount++ 36 | if h.CanHandleCallback == nil { 37 | return false 38 | } 39 | return h.CanHandleCallback(cmd) 40 | } 41 | 42 | func (h *TestHandler) Handle(cmd interface{}, dispatcher command.Dispatcher) error { 43 | h.HandleCallCount++ 44 | if h.HandleCallback == nil { 45 | return nil 46 | } 47 | return h.HandleCallback(cmd, dispatcher) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tarcísio Gruppi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /serialdispatcher.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "sync" 4 | 5 | // NewSerialDispatcher creates a new PrallelDispatcher with the given handlers 6 | func NewSerialDispatcher(handlers []Handler) Dispatcher { 7 | return &SerialDispatcher{ 8 | handlers: handlers, 9 | mutex: sync.RWMutex{}, 10 | } 11 | } 12 | 13 | // SerialDispatcher is a command dispatcher wich will run all handlers in 14 | // parallel and wait all handlers to finish before returning. 15 | // 16 | // If any handler returns an error the dispatcher will stop execution and will 17 | // return that error. 18 | // 19 | // This dispatcher is *thread safe*. 20 | type SerialDispatcher struct { 21 | handlers []Handler 22 | mutex sync.RWMutex 23 | } 24 | 25 | // AppendHandlers implements `Dispatcher.AppendHandlers` 26 | func (d *SerialDispatcher) AppendHandlers(handlers ...Handler) { 27 | d.mutex.Lock() 28 | defer d.mutex.Unlock() 29 | 30 | Loop: 31 | for _, newHandler := range handlers { 32 | for _, existingHandler := range d.handlers { 33 | if newHandler == existingHandler { 34 | continue Loop 35 | } 36 | } 37 | d.handlers = append(d.handlers, newHandler) 38 | } 39 | } 40 | 41 | // Dispatch implements `Dispatcher.Dispatch` 42 | func (d *SerialDispatcher) Dispatch(cmd interface{}) (err error) { 43 | d.mutex.RLock() 44 | defer d.mutex.RUnlock() 45 | 46 | defer func() { 47 | if e := recover(); e != nil { 48 | err = e.(error) 49 | } 50 | }() 51 | 52 | found := false 53 | for _, handler := range d.handlers { 54 | if handler == nil { 55 | continue 56 | } 57 | if !handler.CanHandle(cmd) { 58 | continue 59 | } 60 | found = true 61 | if err = handler.Handle(cmd, d); err != nil { 62 | return 63 | } 64 | } 65 | 66 | if !found { 67 | return &NoHandlerFoundError{ 68 | Command: cmd, 69 | } 70 | } 71 | 72 | return 73 | } 74 | 75 | // DispatchOptional implements `Dispatcher.DispatchOptional` 76 | func (d *SerialDispatcher) DispatchOptional(cmd interface{}) (err error) { 77 | d.mutex.RLock() 78 | defer d.mutex.RUnlock() 79 | 80 | err = d.Dispatch(cmd) 81 | switch err.(type) { 82 | case *NoHandlerFoundError: 83 | return nil 84 | default: 85 | return err 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /paralleldispatcher.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "github.com/nproc/errorgroup-go" 8 | ) 9 | 10 | // NewParallelDispatcher creates a new PrallelDispatcher with the given handlers 11 | func NewParallelDispatcher(handlers []Handler) Dispatcher { 12 | return &ParallelDispatcher{ 13 | handlers: handlers, 14 | mutex: sync.RWMutex{}, 15 | } 16 | } 17 | 18 | // ParallelDispatcher is a command dispatcher wich will run all handlers in 19 | // parallel and wait all handlers to finish before returning. 20 | // 21 | // All errors returned by the handlers will be grouped in a 22 | // `errorgroup.ErrorGroup`. 23 | // 24 | // This dispatcher is *thread safe*. 25 | type ParallelDispatcher struct { 26 | handlers []Handler 27 | mutex sync.RWMutex 28 | } 29 | 30 | // AppendHandlers implements `Dispatcher.AppendHandlers` 31 | func (d *ParallelDispatcher) AppendHandlers(handlers ...Handler) { 32 | d.mutex.Lock() 33 | defer d.mutex.Unlock() 34 | 35 | Loop: 36 | for _, newHandler := range handlers { 37 | for _, existingHandler := range d.handlers { 38 | if newHandler == existingHandler { 39 | continue Loop 40 | } 41 | } 42 | d.handlers = append(d.handlers, newHandler) 43 | } 44 | } 45 | 46 | // Dispatch implements `Dispatcher.Dispatch` 47 | func (d *ParallelDispatcher) Dispatch(cmd interface{}) (err error) { 48 | d.mutex.RLock() 49 | defer d.mutex.RUnlock() 50 | 51 | defer func() { 52 | if e := recover(); e != nil { 53 | err = e.(error) 54 | } 55 | }() 56 | 57 | var found int32 58 | wg := &sync.WaitGroup{} 59 | errCh := make(chan error, len(d.handlers)) 60 | for _, handler := range d.handlers { 61 | wg.Add(1) 62 | go d.dispatch(wg, errCh, &found, handler, cmd) 63 | } 64 | 65 | wg.Wait() 66 | close(errCh) 67 | 68 | if found != 1 { 69 | return &NoHandlerFoundError{ 70 | Command: cmd, 71 | } 72 | } 73 | 74 | errs := []error{} 75 | for { 76 | e, ok := <-errCh 77 | if !ok { 78 | break 79 | } 80 | if e == nil { 81 | continue 82 | } 83 | errs = append(errs, e) 84 | } 85 | 86 | if len(errs) == 0 { 87 | return 88 | } 89 | 90 | err = errorgroup.New(errs) 91 | 92 | return 93 | } 94 | 95 | func (d *ParallelDispatcher) dispatch(wg *sync.WaitGroup, errCh chan error, found *int32, handler Handler, cmd interface{}) { 96 | var err error 97 | 98 | defer func() { 99 | if e := recover(); e != nil { 100 | err = e.(error) 101 | } 102 | errCh <- err 103 | wg.Done() 104 | }() 105 | 106 | if !handler.CanHandle(cmd) { 107 | return 108 | } 109 | 110 | atomic.StoreInt32(found, 1) 111 | 112 | err = handler.Handle(cmd, d) 113 | } 114 | 115 | // DispatchOptional implements `Dispatcher.DispatchOptional` 116 | func (d *ParallelDispatcher) DispatchOptional(cmd interface{}) (err error) { 117 | d.mutex.RLock() 118 | defer d.mutex.RUnlock() 119 | 120 | err = d.Dispatch(cmd) 121 | switch err.(type) { 122 | case *NoHandlerFoundError: 123 | return nil 124 | default: 125 | return err 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /serialdispatcher_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | "github.com/txgruppi/command" 11 | ) 12 | 13 | func TestSerialDispatcher(t *testing.T) { 14 | Convey("SerialDispatcher", t, func() { 15 | handlerA := &TestHandler{ 16 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandA), 17 | } 18 | handlerB := &TestHandler{ 19 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandB), 20 | } 21 | handlers := []command.Handler{handlerA, handlerB} 22 | dispatcher := command.NewSerialDispatcher(handlers) 23 | 24 | Convey("New", func() { 25 | Convey("it should create a dispatcher with the given handlers", func() { 26 | err := dispatcher.Dispatch(CommandA) 27 | So(err, ShouldBeNil) 28 | So(handlerA.CanHandleCallCount, ShouldEqual, 1) 29 | So(handlerB.CanHandleCallCount, ShouldEqual, 1) 30 | So(handlerA.HandleCallCount, ShouldEqual, 1) 31 | So(handlerB.HandleCallCount, ShouldEqual, 0) 32 | 33 | err = dispatcher.Dispatch(CommandB) 34 | So(err, ShouldBeNil) 35 | So(handlerA.CanHandleCallCount, ShouldEqual, 2) 36 | So(handlerB.CanHandleCallCount, ShouldEqual, 2) 37 | So(handlerA.HandleCallCount, ShouldEqual, 1) 38 | So(handlerB.HandleCallCount, ShouldEqual, 1) 39 | }) 40 | }) 41 | 42 | Convey("AppendHandlers", func() { 43 | Convey("it should append the given handler to the existing handlers", func() { 44 | handlerC := &TestHandler{ 45 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandC), 46 | } 47 | dispatcher.AppendHandlers(handlerC) 48 | 49 | err := dispatcher.Dispatch(CommandA) 50 | So(err, ShouldBeNil) 51 | So(handlerA.CanHandleCallCount, ShouldEqual, 1) 52 | So(handlerB.CanHandleCallCount, ShouldEqual, 1) 53 | So(handlerC.CanHandleCallCount, ShouldEqual, 1) 54 | So(handlerA.HandleCallCount, ShouldEqual, 1) 55 | So(handlerB.HandleCallCount, ShouldEqual, 0) 56 | So(handlerC.HandleCallCount, ShouldEqual, 0) 57 | 58 | err = dispatcher.Dispatch(CommandB) 59 | So(err, ShouldBeNil) 60 | So(handlerA.CanHandleCallCount, ShouldEqual, 2) 61 | So(handlerB.CanHandleCallCount, ShouldEqual, 2) 62 | So(handlerC.CanHandleCallCount, ShouldEqual, 2) 63 | So(handlerA.HandleCallCount, ShouldEqual, 1) 64 | So(handlerB.HandleCallCount, ShouldEqual, 1) 65 | So(handlerC.HandleCallCount, ShouldEqual, 0) 66 | 67 | err = dispatcher.Dispatch(CommandC) 68 | So(err, ShouldBeNil) 69 | So(handlerA.CanHandleCallCount, ShouldEqual, 3) 70 | So(handlerB.CanHandleCallCount, ShouldEqual, 3) 71 | So(handlerC.CanHandleCallCount, ShouldEqual, 3) 72 | So(handlerA.HandleCallCount, ShouldEqual, 1) 73 | So(handlerB.HandleCallCount, ShouldEqual, 1) 74 | So(handlerC.HandleCallCount, ShouldEqual, 1) 75 | }) 76 | }) 77 | 78 | Convey("Dispatch", func() { 79 | Convey("it should dispatch the command and return the handler's return value", func() { 80 | expected := errors.New("This is the expected error") 81 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return expected } 82 | err := dispatcher.Dispatch(CommandA) 83 | So(err.Error(), ShouldEqual, expected.Error()) 84 | }) 85 | 86 | Convey("it should dispatch the command and return the handler's return value only if the command can be handled", func() { 87 | notExpected := errors.New("This error should not be returned") 88 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return notExpected } 89 | err := dispatcher.Dispatch(CommandB) 90 | So(err, ShouldBeNil) 91 | }) 92 | 93 | Convey("it should return an error if no handler can handle the given command", func() { 94 | err := dispatcher.Dispatch(CommandC) 95 | So(err, ShouldNotBeNil) 96 | So(err.Error(), ShouldEqual, "No handler can handle command_test.Command") 97 | }) 98 | 99 | Convey("it should recover from panic", func() { 100 | canHandleErr := errors.New("CanHandle panic") 101 | handleErr := errors.New("Handle panic") 102 | handlerA.CanHandleCallback = func(interface{}) bool { panic(canHandleErr) } 103 | err := dispatcher.Dispatch(CommandA) 104 | So(err.Error(), ShouldEqual, canHandleErr.Error()) 105 | handlerA.CanHandleCallback = nil 106 | handlerB.HandleCallback = func(interface{}, command.Dispatcher) error { panic(handleErr) } 107 | err = dispatcher.Dispatch(CommandB) 108 | So(err.Error(), ShouldEqual, handleErr.Error()) 109 | }) 110 | 111 | Convey("it should wait a handler to finish before continue (serial dispatch)", func() { 112 | callOrder := []int{} 113 | lock := sync.Mutex{} 114 | handlerA.CanHandleCallback = func(interface{}) bool { 115 | time.Sleep(10 * time.Millisecond) 116 | lock.Lock() 117 | defer lock.Unlock() 118 | callOrder = append(callOrder, 1) 119 | return false 120 | } 121 | handlerB.CanHandleCallback = func(interface{}) bool { 122 | lock.Lock() 123 | defer lock.Unlock() 124 | callOrder = append(callOrder, 2) 125 | return true 126 | } 127 | err := dispatcher.Dispatch(CommandB) 128 | So(err, ShouldBeNil) 129 | So(callOrder, ShouldResemble, []int{1, 2}) 130 | }) 131 | }) 132 | 133 | Convey("DispatchOptional", func() { 134 | Convey("it should dispatch the command and return the handler's return value", func() { 135 | expected := errors.New("This is the expected error") 136 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return expected } 137 | err := dispatcher.DispatchOptional(CommandA) 138 | So(err.Error(), ShouldEqual, expected.Error()) 139 | }) 140 | 141 | Convey("it should return nil if no handler can handle the given command", func() { 142 | err := dispatcher.DispatchOptional(CommandC) 143 | So(err, ShouldBeNil) 144 | }) 145 | }) 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /paralleldispatcher_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/nproc/errorgroup-go" 10 | . "github.com/smartystreets/goconvey/convey" 11 | "github.com/txgruppi/command" 12 | ) 13 | 14 | func TestParallelDispatcher(t *testing.T) { 15 | Convey("ParallelDispatcher", t, func() { 16 | handlerA := &TestHandler{ 17 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandA), 18 | } 19 | handlerB := &TestHandler{ 20 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandB), 21 | } 22 | handlers := []command.Handler{handlerA, handlerB} 23 | dispatcher := command.NewParallelDispatcher(handlers) 24 | 25 | Convey("New", func() { 26 | Convey("it should create a dispatcher with the given handlers", func() { 27 | err := dispatcher.Dispatch(CommandA) 28 | So(err, ShouldBeNil) 29 | So(handlerA.CanHandleCallCount, ShouldEqual, 1) 30 | So(handlerB.CanHandleCallCount, ShouldEqual, 1) 31 | So(handlerA.HandleCallCount, ShouldEqual, 1) 32 | So(handlerB.HandleCallCount, ShouldEqual, 0) 33 | 34 | err = dispatcher.Dispatch(CommandB) 35 | So(err, ShouldBeNil) 36 | So(handlerA.CanHandleCallCount, ShouldEqual, 2) 37 | So(handlerB.CanHandleCallCount, ShouldEqual, 2) 38 | So(handlerA.HandleCallCount, ShouldEqual, 1) 39 | So(handlerB.HandleCallCount, ShouldEqual, 1) 40 | }) 41 | }) 42 | 43 | Convey("AppendHandlers", func() { 44 | Convey("it should append the given handler to the existing handlers", func() { 45 | handlerC := &TestHandler{ 46 | CanHandleCallback: NewCanHandleCallbackForCommand(CommandC), 47 | } 48 | dispatcher.AppendHandlers(handlerC) 49 | 50 | err := dispatcher.Dispatch(CommandA) 51 | So(err, ShouldBeNil) 52 | So(handlerA.CanHandleCallCount, ShouldEqual, 1) 53 | So(handlerB.CanHandleCallCount, ShouldEqual, 1) 54 | So(handlerC.CanHandleCallCount, ShouldEqual, 1) 55 | So(handlerA.HandleCallCount, ShouldEqual, 1) 56 | So(handlerB.HandleCallCount, ShouldEqual, 0) 57 | So(handlerC.HandleCallCount, ShouldEqual, 0) 58 | 59 | err = dispatcher.Dispatch(CommandB) 60 | So(err, ShouldBeNil) 61 | So(handlerA.CanHandleCallCount, ShouldEqual, 2) 62 | So(handlerB.CanHandleCallCount, ShouldEqual, 2) 63 | So(handlerC.CanHandleCallCount, ShouldEqual, 2) 64 | So(handlerA.HandleCallCount, ShouldEqual, 1) 65 | So(handlerB.HandleCallCount, ShouldEqual, 1) 66 | So(handlerC.HandleCallCount, ShouldEqual, 0) 67 | 68 | err = dispatcher.Dispatch(CommandC) 69 | So(err, ShouldBeNil) 70 | So(handlerA.CanHandleCallCount, ShouldEqual, 3) 71 | So(handlerB.CanHandleCallCount, ShouldEqual, 3) 72 | So(handlerC.CanHandleCallCount, ShouldEqual, 3) 73 | So(handlerA.HandleCallCount, ShouldEqual, 1) 74 | So(handlerB.HandleCallCount, ShouldEqual, 1) 75 | So(handlerC.HandleCallCount, ShouldEqual, 1) 76 | }) 77 | }) 78 | 79 | Convey("Dispatch", func() { 80 | Convey("it should dispatch the command and return the handler's return value", func() { 81 | expected := errors.New("This is the expected error") 82 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return expected } 83 | err := dispatcher.Dispatch(CommandA) 84 | So(err.Error(), ShouldEqual, expected.Error()) 85 | }) 86 | 87 | Convey("it should dispatch the command and return the handler's return value only if the command can be handled", func() { 88 | notExpected := errors.New("This error should not be returned") 89 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return notExpected } 90 | err := dispatcher.Dispatch(CommandB) 91 | So(err, ShouldBeNil) 92 | }) 93 | 94 | Convey("it should return an error if no handler can handle the given command", func() { 95 | err := dispatcher.Dispatch(CommandC) 96 | So(err, ShouldNotBeNil) 97 | So(err.Error(), ShouldEqual, "No handler can handle command_test.Command") 98 | }) 99 | 100 | Convey("it should recover from panic", func() { 101 | canHandleErr := errors.New("CanHandle panic") 102 | handleErr := errors.New("Handle panic") 103 | handlerA.CanHandleCallback = func(interface{}) bool { panic(canHandleErr) } 104 | err := dispatcher.Dispatch(CommandB) 105 | So(err.Error(), ShouldEqual, canHandleErr.Error()) 106 | handlerA.CanHandleCallback = nil 107 | handlerB.HandleCallback = func(interface{}, command.Dispatcher) error { panic(handleErr) } 108 | err = dispatcher.Dispatch(CommandB) 109 | So(err.Error(), ShouldEqual, handleErr.Error()) 110 | }) 111 | 112 | Convey("it should wait a handler to finish before continue (parallel dispatch)", func() { 113 | callOrder := []int{} 114 | lock := sync.Mutex{} 115 | handlerA.CanHandleCallback = func(interface{}) bool { 116 | time.Sleep(10 * time.Millisecond) 117 | lock.Lock() 118 | defer lock.Unlock() 119 | callOrder = append(callOrder, 1) 120 | return false 121 | } 122 | handlerB.CanHandleCallback = func(interface{}) bool { 123 | lock.Lock() 124 | defer lock.Unlock() 125 | callOrder = append(callOrder, 2) 126 | return true 127 | } 128 | err := dispatcher.Dispatch(CommandB) 129 | So(err, ShouldBeNil) 130 | So(callOrder, ShouldResemble, []int{2, 1}) 131 | }) 132 | 133 | Convey("it should group errors", func() { 134 | errA := errors.New("Error A") 135 | errB := errors.New("Error B") 136 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { panic(errA) } 137 | handlerB.HandleCallback = func(interface{}, command.Dispatcher) error { return errB } 138 | handlerB.CanHandleCallback = NewCanHandleCallbackForCommand(CommandA) 139 | err := dispatcher.Dispatch(CommandA) 140 | So(err, ShouldNotBeNil) 141 | So(err, ShouldHaveSameTypeAs, &errorgroup.ErrorGroup{}) 142 | errGroup := err.(*errorgroup.ErrorGroup) 143 | So(len(errGroup.Errors), ShouldEqual, 2) 144 | So(errGroup.Errors, ShouldContain, errA) 145 | So(errGroup.Errors, ShouldContain, errB) 146 | }) 147 | }) 148 | 149 | Convey("DispatchOptional", func() { 150 | Convey("it should dispatch the command and return the handler's return value", func() { 151 | expected := errors.New("This is the expected error") 152 | handlerA.HandleCallback = func(interface{}, command.Dispatcher) error { return expected } 153 | err := dispatcher.DispatchOptional(CommandA) 154 | So(err.Error(), ShouldEqual, expected.Error()) 155 | }) 156 | 157 | Convey("it should return nil if no handler can handle the given command", func() { 158 | err := dispatcher.DispatchOptional(CommandC) 159 | So(err, ShouldBeNil) 160 | }) 161 | }) 162 | }) 163 | } 164 | --------------------------------------------------------------------------------