├── .github └── workflows │ └── go.yaml ├── .gitignore ├── .golangci.toml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── commandbus.go ├── commandbus_test.go ├── go.mod ├── go.sum └── middleware ├── base_test.go ├── dispatcher.go ├── dispatcher_test.go ├── logger.go ├── logger_test.go ├── opencensus.go └── opencensus_test.go /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | 9 | - name: Set up Go 1.14 10 | uses: actions/setup-go@v1 11 | with: 12 | go-version: 1.14 13 | id: go 14 | 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v1 17 | 18 | - name: Configure git for private modules 19 | env: 20 | TOKEN: ${{ secrets.GITHUBACCESSTOKEN }} 21 | run: git config --global url."https://lana-dev:${TOKEN}@github.com".insteadOf "https://github.com" 22 | 23 | - name: Install Linter 24 | run: wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.24.0 25 | 26 | - name: Install dependencies 27 | env: 28 | GOPROXY: https://proxy.golang.org,direct 29 | GOPRIVATE: github.com/lana 30 | run: go mod download 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | 35 | - name: Run Linter 36 | run: ./bin/golangci-lint run ./... 37 | 38 | - name: Run Tests 39 | run: go test -race ./... 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.* 2 | cover.* 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.golangci.toml: -------------------------------------------------------------------------------- 1 | [run] 2 | timeout = "120s" 3 | 4 | [output] 5 | format = "colored-line-number" 6 | 7 | [linters] 8 | enable = [ 9 | "gocyclo", "unconvert", "goimports", "unused", "varcheck", 10 | "vetshadow", "misspell", "nakedret", "errcheck", "golint", "ineffassign", 11 | "deadcode", "goconst", "vet", "unparam", "gofmt" 12 | ] 13 | 14 | [issues] 15 | exclude-use-default = false 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.12.x" 5 | - "1.13.x" 6 | - master 7 | env: 8 | - GO111MODULE=on 9 | 10 | script: 11 | - make test 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Vinicius Moraes 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | 3 | # Run tests and generates html coverage file 4 | cover: test 5 | @go tool cover -html=./coverage.text -o ./cover.html 6 | @rm ./coverage.text 7 | .PHONY: cover 8 | 9 | # Run tests 10 | test: 11 | @go test -v -coverprofile=./coverage.text -covermode=atomic $(shell go list ./...) 12 | .PHONY: test 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GO CommandBus 2 | 3 | [![Build Status](https://img.shields.io/travis/lana/go-commandbus/master.svg?style=flat-square)](https://travis-ci.org/lana/go-commandbus) 4 | [![Codecov branch](https://img.shields.io/codecov/c/github/lana/go-commandbus/master.svg?style=flat-square)](https://codecov.io/gh/lana/go-commandbus) 5 | [![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/lana/go-commandbus) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/lana/go-commandbus?style=flat-square)](https://goreportcard.com/report/github.com/lana/go-commandbus) 7 | [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/lana/go-commandbus/blob/master/LICENSE) 8 | 9 | A slight and pluggable command-bus for Go. 10 | 11 | ## Install 12 | 13 | Use go get. 14 | ```sh 15 | $ go get github.com/lana/go-commandbus 16 | ``` 17 | 18 | Then import the package into your own code: 19 | ``` 20 | import "github.com/lana/go-commandbus" 21 | ``` 22 | 23 | ## Usage 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | "log" 30 | 31 | "github.com/lana/go-commandbus" 32 | ) 33 | 34 | type CreateUser struct { 35 | Name string 36 | } 37 | 38 | func CreateHandler(ctx context.Context, cmd *CreateUser) error { 39 | log.Printf("user %s created", cmd.Name) 40 | 41 | return nil 42 | } 43 | 44 | func main() { 45 | bus := commandbus.New() 46 | 47 | err := bus.Register(&CreateUser{}, CreateHandler) 48 | 49 | if err != nil { 50 | log.Error(err) 51 | os.Exit(1) 52 | } 53 | 54 | err = bus.Execute(context.Background(), &CreateUser{"go-commandbus"}) 55 | 56 | if err != nil { 57 | log.Error(err) 58 | os.Exit(1) 59 | } 60 | } 61 | ``` 62 | 63 | ## License 64 | 65 | This project is released under the MIT licence. See [LICENSE](https://github.com/lana/go-commandbus/blob/master/LICENSE) for more details. 66 | -------------------------------------------------------------------------------- /commandbus.go: -------------------------------------------------------------------------------- 1 | package commandbus 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | // HandlerFunc defines a function to execute the command handler. 11 | // This function type is only used by `MiddlewareFunc`. 12 | type HandlerFunc func(context.Context, interface{}) error 13 | 14 | // MiddlewareFunc defines a function to process middleware. 15 | // it receives the next Handler must return another handler 16 | type MiddlewareFunc func(h HandlerFunc) HandlerFunc 17 | 18 | type handlers map[string]interface{} 19 | 20 | // CommandBus is the definition of how command should be handled 21 | type CommandBus interface { 22 | // Register assign a command to a command handle for future executions. 23 | Register(interface{}, interface{}) error 24 | 25 | // Handlers returns all registered handlers. 26 | Handlers() handlers 27 | 28 | // Use adds middleware to the chain. 29 | Use(...MiddlewareFunc) 30 | 31 | // Execute send a given Command to its assigned command handler. 32 | Execute(context.Context, interface{}) error 33 | } 34 | 35 | type bus struct { 36 | handlers handlers 37 | middleware []MiddlewareFunc 38 | } 39 | 40 | // New creates a new command bus. 41 | func New() CommandBus { 42 | return &bus{ 43 | handlers: make(handlers), 44 | middleware: make([]MiddlewareFunc, 0), 45 | } 46 | } 47 | 48 | // Register assign a command to a command handle for future executions. 49 | func (b *bus) Register(cmd interface{}, fn interface{}) error { 50 | if err := b.validate(cmd); err != nil { 51 | return err 52 | } 53 | 54 | cmdName := reflect.TypeOf(cmd).String() 55 | 56 | if reflect.TypeOf(fn).Kind() != reflect.Func { 57 | return fmt.Errorf("%s is not a function", reflect.TypeOf(fn)) 58 | } 59 | 60 | if _, err := b.handler(cmdName); err == nil { 61 | return errors.New("command already assigned to a handler") 62 | } 63 | 64 | b.handlers[cmdName] = fn 65 | 66 | return nil 67 | } 68 | 69 | // Handlers returns all registered handlers. 70 | func (b *bus) Handlers() handlers { 71 | return b.handlers 72 | } 73 | 74 | // Use adds middleware to the chain. 75 | func (b *bus) Use(middleware ...MiddlewareFunc) { 76 | b.middleware = append(b.middleware, middleware...) 77 | } 78 | 79 | // Execute send a given Command to its assigned command handler. 80 | func (b *bus) Execute(ctx context.Context, cmd interface{}) error { 81 | if err := b.validate(cmd); err != nil { 82 | return err 83 | } 84 | 85 | h := applyMiddleware(b.executor, b.middleware...) 86 | 87 | return h(ctx, cmd) 88 | } 89 | 90 | func (b *bus) handler(cmdName string) (interface{}, error) { 91 | if h, ok := b.handlers[cmdName]; ok { 92 | return h, nil 93 | } 94 | 95 | return nil, errors.New("handler not found for command") 96 | } 97 | 98 | func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc { 99 | for i := len(middleware) - 1; i >= 0; i-- { 100 | h = middleware[i](h) 101 | } 102 | 103 | return h 104 | } 105 | 106 | func (b *bus) executor(ctx context.Context, cmd interface{}) error { 107 | handler, err := b.handler(reflect.TypeOf(cmd).String()) 108 | 109 | if err != nil { 110 | return err 111 | } 112 | 113 | fn := reflect.ValueOf(handler) 114 | args := make([]reflect.Value, 0) 115 | 116 | for _, arg := range []interface{}{ctx, cmd} { 117 | args = append(args, reflect.ValueOf(arg)) 118 | } 119 | 120 | res := make([]interface{}, 0) 121 | 122 | for _, r := range fn.Call(args) { 123 | res = append(res, r.Interface()) 124 | } 125 | 126 | if len(res) < 1 { 127 | return nil 128 | } 129 | 130 | r := res[:1][0] 131 | 132 | if err, ok := r.(error); ok { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (b *bus) validate(cmd interface{}) error { 140 | value := reflect.ValueOf(cmd) 141 | 142 | if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct { 143 | return errors.New("only pointer to commands are allowed") 144 | } 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /commandbus_test.go: -------------------------------------------------------------------------------- 1 | package commandbus 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | type Cmd struct { 11 | c int 12 | } 13 | 14 | func CmdHandler(ctx context.Context, cmd *Cmd) error { 15 | cmd.c++ 16 | return nil 17 | } 18 | 19 | type ErrorCmd struct { 20 | err error 21 | } 22 | 23 | func ErrorCmdHandler(ctx context.Context, cmd *ErrorCmd) error { 24 | return cmd.err 25 | } 26 | 27 | type NoReturnCmd struct{} 28 | 29 | func NoReturnHandler(ctx context.Context, cmd *NoReturnCmd) { 30 | } 31 | 32 | func TestRegisterHandlers(t *testing.T) { 33 | bus := New() 34 | 35 | if err := bus.Register(&Cmd{}, CmdHandler); err != nil { 36 | t.Errorf("Failed to register command: %v", err) 37 | } 38 | 39 | if len(bus.Handlers()) != 1 { 40 | t.Errorf("Expected to have 1 command, got %d", len(bus.Handlers())) 41 | } 42 | 43 | for _, handler := range bus.Handlers() { 44 | if reflect.ValueOf(handler) != reflect.ValueOf(CmdHandler) { 45 | t.Error("Registered handler is different from the expected one") 46 | } 47 | } 48 | 49 | // invalid handler non-pointer 50 | fooCmd := struct{}{} 51 | invalidHandler := struct{}{} 52 | 53 | if err := bus.Register(fooCmd, invalidHandler); err == nil { 54 | t.Error("Invalid handler was accepted. Register must only accept pointers") 55 | } 56 | 57 | // invalid handler non-func 58 | invalidHandlerFunc := &struct{}{} 59 | 60 | if err := bus.Register(&Cmd{}, invalidHandlerFunc); err == nil { 61 | t.Error("Invalid handler was accepted. Register must only accept functions") 62 | } 63 | 64 | // duplicated 65 | if err := bus.Register(&Cmd{}, CmdHandler); err == nil { 66 | t.Error("Bus must not accept duplicated commands") 67 | } 68 | } 69 | 70 | func TestExecuteRegisteredHandler(t *testing.T) { 71 | bus := New() 72 | 73 | if err := bus.Register(&Cmd{}, CmdHandler); err != nil { 74 | t.Errorf("Failed to register command: %v", err) 75 | } 76 | 77 | c := &Cmd{} 78 | expected := 10 79 | 80 | for i := 0; i < expected; i++ { 81 | if err := bus.Execute(context.Background(), c); err != nil { 82 | t.Errorf("Failed to execute command: %v", err) 83 | } 84 | } 85 | 86 | if c.c != expected { 87 | t.Errorf("Execution number is wrong. Expected %d, got %d", expected, c.c) 88 | } 89 | 90 | // try to execute an unregistered command 91 | invalidCommand := struct{}{} 92 | 93 | if err := bus.Execute(context.Background(), invalidCommand); err == nil { 94 | t.Error("Invalid command was executed without errors") 95 | } 96 | 97 | if err := bus.Register(&NoReturnCmd{}, NoReturnHandler); err != nil { 98 | t.Errorf("Failed to register command: %v", err) 99 | } 100 | 101 | if err := bus.Execute(context.Background(), &NoReturnCmd{}); err != nil { 102 | t.Errorf("failed to execute command: expected %v, got %v", nil, err) 103 | } 104 | } 105 | 106 | func TestExecutionReturnedError(t *testing.T) { 107 | bus := New() 108 | 109 | if err := bus.Register(&ErrorCmd{}, ErrorCmdHandler); err != nil { 110 | t.Errorf("Failed to register command: %v", err) 111 | } 112 | 113 | cmd := &ErrorCmd{err: errors.New("this is an error")} 114 | err := bus.Execute(context.Background(), cmd) 115 | 116 | if err != cmd.err { 117 | t.Errorf("Failed to assert command error. Expected \"%v\", got \"%v\"", cmd.err, err) 118 | } 119 | 120 | if err := bus.Execute(context.Background(), &struct{}{}); err == nil { 121 | t.Errorf("failed to assert execute error. Expected an error, got: %v", err) 122 | } 123 | } 124 | 125 | func TestExecutionWithMiddleware(t *testing.T) { 126 | bus := New() 127 | 128 | if err := bus.Register(&Cmd{}, CmdHandler); err != nil { 129 | t.Errorf("Failed to register command: %v", err) 130 | } 131 | 132 | // this middleware will be executed before the handler, 133 | // making the command counter equals to 11. 134 | bus.Use(func(next HandlerFunc) HandlerFunc { 135 | return func(ctx context.Context, cmd interface{}) error { 136 | if c, ok := cmd.(*Cmd); ok { 137 | c.c += 10 // add 10 to the command value 138 | } 139 | 140 | return next(ctx, cmd) 141 | } 142 | }) 143 | 144 | c := &Cmd{} 145 | expected := 11 146 | if err := bus.Execute(context.Background(), c); err != nil { 147 | t.Errorf("Failed to execute command: %v", err) 148 | } 149 | 150 | if c.c != expected { 151 | t.Errorf("Execution number is wrong. Expected %d, got %d", expected, c.c) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lana/go-commandbus 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/lana/go-dispatcher v0.1.0 7 | github.com/pkg/errors v0.8.1 8 | go.opencensus.io v0.22.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 4 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 5 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= 6 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 7 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 8 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 11 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 12 | github.com/lana/go-dispatcher v0.1.0 h1:Tor5BOIIBgi6lORSekQ4iGnUT0ZgOyp9aAsjkafiPAg= 13 | github.com/lana/go-dispatcher v0.1.0/go.mod h1:XOti+iBtQkexhCyENKgp55l/i2ZUFsEtkySTAAWUNNA= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= 17 | go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 20 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 21 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 22 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 23 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 24 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 25 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 26 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 27 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 28 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 29 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 36 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 38 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 39 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 40 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 41 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 42 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 43 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 44 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 45 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 46 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 47 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 48 | -------------------------------------------------------------------------------- /middleware/base_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // CreateUser command for testing. 10 | type CreateUser struct { 11 | Err bool 12 | } 13 | 14 | // next command handler for testing. 15 | func next(ctx context.Context, cmd interface{}) error { 16 | v := cmd.(*CreateUser) 17 | 18 | if v.Err { 19 | return errors.New("command was failed") 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /middleware/dispatcher.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/lana/go-commandbus" 10 | "github.com/lana/go-dispatcher" 11 | ) 12 | 13 | const ( 14 | // DispatcherReceived event raised when a dispatcher has been received. 15 | DispatcherReceived = "received" 16 | 17 | // DispatcherExecuted event raised when a dispatcher has been executed. 18 | DispatcherExecuted = "executed" 19 | ) 20 | 21 | type event struct { 22 | name string 23 | cmd interface{} 24 | } 25 | 26 | // Type returns the event type. 27 | func (e event) Type() dispatcher.EventType { 28 | name := reflect.TypeOf(e.cmd).Elem().Name() 29 | name = fmt.Sprintf("%s.%s", strings.ToLower(name), e.name) 30 | 31 | return dispatcher.EventType(name) 32 | } 33 | 34 | // Data returns the event command data. 35 | func (e event) Data() interface{} { 36 | return e.cmd 37 | } 38 | 39 | // Dispatcher will dispatch events when a command was received and executed. 40 | func Dispatcher(d dispatcher.Dispatcher) commandbus.MiddlewareFunc { 41 | return func(next commandbus.HandlerFunc) commandbus.HandlerFunc { 42 | return func(ctx context.Context, cmd interface{}) error { 43 | d.Dispatch(ctx, event{DispatcherReceived, cmd}) 44 | 45 | if err := next(ctx, cmd); err != nil { 46 | return err 47 | } 48 | 49 | d.Dispatch(ctx, event{DispatcherExecuted, cmd}) 50 | 51 | return nil 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /middleware/dispatcher_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/lana/go-dispatcher" 9 | ) 10 | 11 | func TestDispatcher(t *testing.T) { 12 | d := dispatcher.New() 13 | d.On("createuser.received", func(ctx context.Context, e dispatcher.Event) { 14 | if _, ok := (e.Data()).(*CreateUser); !ok { 15 | t.Errorf("invalid data retrieved: got %v", reflect.TypeOf(e.Data())) 16 | } 17 | }) 18 | 19 | mw := Dispatcher(d) 20 | ctx := context.Background() 21 | 22 | if err := mw(next)(ctx, &CreateUser{true}); err == nil { 23 | t.Errorf("failed to execute the middleware: expected an error and got %v", err) 24 | } 25 | 26 | if err := mw(next)(ctx, &CreateUser{}); err != nil { 27 | t.Errorf("failed to execute the middleware: got %v and want %v", err, nil) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "reflect" 8 | 9 | "github.com/lana/go-commandbus" 10 | ) 11 | 12 | // Logger middleware to log the event command. 13 | func Logger(next commandbus.HandlerFunc) commandbus.HandlerFunc { 14 | return func(ctx context.Context, cmd interface{}) error { 15 | if err := next(ctx, cmd); err != nil { 16 | return err 17 | } 18 | 19 | n := reflect.TypeOf(cmd).String() 20 | buf, _ := json.Marshal(cmd) 21 | log.Printf("Executing command \"%s\" with params: %s", n, buf) 22 | 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /middleware/logger_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "log" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestLogger(t *testing.T) { 12 | ctx := context.Background() 13 | buf := new(bytes.Buffer) 14 | 15 | log.SetOutput(buf) 16 | 17 | if err := Logger(next)(ctx, &CreateUser{true}); err == nil { 18 | t.Errorf("failed to execute the middleware: expected an error and got %v", err) 19 | } 20 | 21 | if err := Logger(next)(ctx, &CreateUser{}); err != nil { 22 | t.Errorf("failed to execute the middleware: got %v and want %v", err, nil) 23 | } 24 | 25 | if !strings.Contains(buf.String(), "Executing command") { 26 | t.Errorf("invalid log message: %v", buf.String()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /middleware/opencensus.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | commandbus "github.com/lana/go-commandbus" 10 | "github.com/pkg/errors" 11 | "go.opencensus.io/stats" 12 | "go.opencensus.io/stats/view" 13 | "go.opencensus.io/tag" 14 | "go.opencensus.io/trace" 15 | ) 16 | 17 | var ( 18 | // Measures 19 | ocExecCount = stats.Int64("executions", "Number of command executions", stats.UnitDimensionless) 20 | ocErrCount = stats.Int64("errors", "Number of command errors", stats.UnitDimensionless) 21 | 22 | // Tag keys 23 | commandName = tag.MustNewKey("command.name") 24 | 25 | // OpenCensusExecCount view. 26 | OpenCensusExecCount = &view.View{ 27 | Name: "execution_count", 28 | Measure: ocExecCount, 29 | Description: "Number of command executions", 30 | Aggregation: view.Count(), 31 | } 32 | 33 | // OpenCensusErrCount view. 34 | OpenCensusErrCount = &view.View{ 35 | Name: "error_count", 36 | Measure: ocErrCount, 37 | Description: "Number of command error", 38 | Aggregation: view.Count(), 39 | } 40 | 41 | // OpenCensusExecCountByCommand view. 42 | OpenCensusExecCountByCommand = &view.View{ 43 | Name: "execution_count_by_command", 44 | Measure: ocExecCount, 45 | Description: "Number of command executions by command", 46 | TagKeys: []tag.Key{commandName}, 47 | Aggregation: view.Count(), 48 | } 49 | 50 | // OpenCensusErrCountByCommand view. 51 | OpenCensusErrCountByCommand = &view.View{ 52 | Name: "error_count_by_command", 53 | Measure: ocErrCount, 54 | Description: "Number of command error by command", 55 | TagKeys: []tag.Key{commandName}, 56 | Aggregation: view.Count(), 57 | } 58 | ) 59 | 60 | // OpenCensusConfig defines the config for OpenCensus middleware. 61 | type OpenCensusConfig struct { 62 | // Views it is a OpenCensus views list. 63 | Views []*view.View 64 | } 65 | 66 | // DefaultOpenCensusConfig is the default OpenCensus middleware config. 67 | var DefaultOpenCensusConfig = OpenCensusConfig{ 68 | Views: []*view.View{ 69 | OpenCensusExecCount, 70 | OpenCensusErrCount, 71 | OpenCensusExecCountByCommand, 72 | OpenCensusErrCountByCommand, 73 | }, 74 | } 75 | 76 | // OpenCensus returns a middleware that collect the command metrics. 77 | func OpenCensus() commandbus.MiddlewareFunc { 78 | return OpenCensusWithConfig(DefaultOpenCensusConfig) 79 | } 80 | 81 | // OpenCensusWithConfig returns a OpenCensus middleware with config. 82 | // See: `OpenCensus()`. 83 | func OpenCensusWithConfig(cfg OpenCensusConfig) commandbus.MiddlewareFunc { 84 | // Defaults 85 | if len(cfg.Views) == 0 { 86 | cfg.Views = DefaultOpenCensusConfig.Views 87 | } 88 | 89 | if err := view.Register(cfg.Views...); err != nil { 90 | panic(fmt.Sprintf("middleware error when register views: %v", err)) 91 | } 92 | 93 | return func(next commandbus.HandlerFunc) commandbus.HandlerFunc { 94 | return func(ctx context.Context, cmd interface{}) error { 95 | name := reflect.TypeOf(cmd).Elem().Name() 96 | 97 | ctx, span := trace.StartSpan(ctx, strings.ToLower(name)) 98 | defer span.End() 99 | 100 | ctx, err := tag.New(ctx, tag.Upsert(commandName, name)) 101 | 102 | if err != nil { 103 | return errors.Wrap(err, "unable to create the tag") 104 | } 105 | 106 | stats.Record(ctx, ocExecCount.M(1)) 107 | 108 | if err := next(ctx, cmd); err != nil { 109 | span.SetStatus( 110 | trace.Status{ 111 | Code: trace.StatusCodeUnknown, 112 | Message: err.Error(), 113 | }, 114 | ) 115 | 116 | stats.Record(ctx, ocErrCount.M(1)) 117 | 118 | return err 119 | } 120 | 121 | return nil 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /middleware/opencensus_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestOpenCensus(t *testing.T) { 9 | ctx := context.Background() 10 | 11 | mw := OpenCensus() 12 | 13 | if err := mw(next)(ctx, &CreateUser{true}); err == nil { 14 | t.Errorf("failed to execute the middleware: expected an error and got %v", err) 15 | } 16 | 17 | if err := mw(next)(ctx, &CreateUser{}); err != nil { 18 | t.Errorf("failed to execute the middleware: got %v and want %v", err, nil) 19 | } 20 | 21 | mw = OpenCensusWithConfig(OpenCensusConfig{}) 22 | 23 | if err := mw(next)(ctx, &CreateUser{}); err != nil { 24 | t.Errorf("failed to execute the middleware: got %v and want %v", err, nil) 25 | } 26 | } 27 | --------------------------------------------------------------------------------