├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── _examples ├── benchmark_test.go └── main.go ├── go.mod ├── go.sum ├── internal └── must │ └── not.go └── mediator ├── errors.go ├── mediator.go ├── mediator_test.go └── pipeline.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .idea 8 | .vscode 9 | *.csv 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | *.csv 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | depguard: 3 | list-type: blacklist 4 | packages: 5 | # logging is allowed only by logutils.Log, logrus 6 | # is allowed to use only in logutils package 7 | - github.com/sirupsen/logrus 8 | packages-with-error-message: 9 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 10 | dupl: 11 | threshold: 100 12 | funlen: 13 | lines: 100 14 | statements: 50 15 | gci: 16 | local-prefixes: github.com/golangci/golangci-lint 17 | goconst: 18 | min-len: 2 19 | min-occurrences: 2 20 | gocritic: 21 | enabled-tags: 22 | - diagnostic 23 | - experimental 24 | - opinionated 25 | - performance 26 | - style 27 | disabled-checks: 28 | - dupImport # https://github.com/go-critic/go-critic/issues/845 29 | - ifElseChain 30 | - octalLiteral 31 | - whyNoLint 32 | - wrapperFunc 33 | gocyclo: 34 | min-complexity: 15 35 | goimports: 36 | local-prefixes: github.com/golangci/golangci-lint 37 | gomnd: 38 | settings: 39 | mnd: 40 | # don't include the "operation" and "assign" 41 | checks: argument,case,condition,return 42 | govet: 43 | check-shadowing: true 44 | settings: 45 | printf: 46 | funcs: 47 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 48 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 49 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 50 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 51 | lll: 52 | line-length: 140 53 | maligned: 54 | suggest-new: true 55 | misspell: 56 | locale: US 57 | nolintlint: 58 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 59 | allow-unused: false # report any unused nolint directives 60 | require-explanation: false # don't require an explanation for nolint directives 61 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 62 | 63 | linters: 64 | disable-all: true 65 | enable: 66 | - bodyclose 67 | - deadcode 68 | - depguard 69 | - dogsled 70 | - dupl 71 | - errcheck 72 | - exportloopref 73 | - exhaustive 74 | - funlen 75 | - gochecknoinits 76 | - goconst 77 | - gocritic 78 | - gocyclo 79 | - gomnd 80 | - goprintffuncname 81 | - gosec 82 | - gosimple 83 | - govet 84 | - ineffassign 85 | - lll 86 | - misspell 87 | - nakedret 88 | - noctx 89 | - nolintlint 90 | - rowserrcheck 91 | - staticcheck 92 | - structcheck 93 | - stylecheck 94 | - typecheck 95 | - unconvert 96 | - unparam 97 | - unused 98 | - varcheck 99 | - whitespace 100 | - revive 101 | 102 | # don't enable: 103 | # - asciicheck 104 | # - scopelint 105 | # - gochecknoglobals 106 | # - gocognit 107 | # - godot 108 | # - godox 109 | # - goerr113 110 | # - interfacer 111 | # - maligned 112 | # - nestif 113 | # - prealloc 114 | # - testpackage 115 | # - revive 116 | # - wsl 117 | 118 | issues: 119 | # Excluding configuration per-path, per-linter, per-text and per-source 120 | exclude-rules: 121 | - path: _test\.go 122 | linters: 123 | - gomnd 124 | # TODO must be removed after the release of the next version (v1.41.0) 125 | - path: pkg/commands/run.go 126 | linters: 127 | - gomnd 128 | # TODO must be removed after the release of the next version (v1.41.0) 129 | - path: pkg/golinters/nolintlint/nolintlint.go 130 | linters: 131 | - gomnd 132 | # TODO must be removed after the release of the next version (v1.41.0) 133 | - path: pkg/printers/tab.go 134 | linters: 135 | - gomnd 136 | 137 | run: 138 | skip-dirs: 139 | - test/testdata_etc 140 | - internal/cache 141 | - internal/renameio 142 | - internal/robustio 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Emre Yazıcı 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # go-mediator 3 | 4 | Simple mediator implementation in go.
5 | 6 | In-process messaging with behaviours. 7 | 8 | 9 | ## Commands 10 | 11 | 12 | 13 | type CreateOrderCommand struct { 14 | 15 | Id string `validate:"required,min=10"` 16 | 17 | } 18 | 19 | func (CreateOrderCommand) Key() string { return "CreateOrderCommand" } 20 | 21 | 22 | 23 | type CreateOrderCommandHandler struct { 24 | 25 | } 26 | 27 | func NewCreateOrderCommandHandler() CreateOrderCommandHandler { 28 | 29 | return CreateOrderCommandHandler{} 30 | 31 | } 32 | 33 | func (CreateOrderCommandHandler) Handle(ctx context.Context, msg mediator.Message) error { 34 | 35 | //Do something 36 | 37 | return nil 38 | 39 | } 40 | 41 | ## Behaviours 42 | 43 | 44 | 45 | ***PipelineBehaviour interface implementation usage*** 46 | 47 | 48 | 49 | type Logger struct{} 50 | 51 | func NewLogger() *Logger { return &Logger{} } 52 | 53 | func (l *Logger) Process(ctx context.Context, msg mediator.Message, next mediator.Next) error { 54 | 55 | log.Println("Pre Process!") 56 | 57 | result := next(ctx) 58 | 59 | log.Println("Post Process") 60 | 61 | return result 62 | 63 | } 64 | 65 | m, err := mediator.New(mediator.WithBehaviour(behaviour.NewLogger())) 66 | 67 | 68 | 69 | ***Func based usage*** 70 | 71 | m, err := mediator.New(mediator.WithBehaviourFunc(func(ctx context.Context, msg mediator.Message, next mediator.Next) error { 72 | 73 | log.Println("Pre Process!") 74 | 75 | next(ctx) 76 | 77 | log.Println("Post Process") 78 | 79 | return nil 80 | 81 | })) 82 | 83 | 84 | 85 | ## Usages 86 | 87 | 88 | 89 | m, err := mediator.New( 90 | mediator.WithBehaviour(behaviour.NewLogger()), 91 | mediator.WithBehaviour(behaviour.NewValidator()), 92 | mediator.WithHandler(FakeCommand{}, NewFakeCommandCommandHandler(r)), 93 | ) 94 | 95 | cmd := FakeCommand{ 96 | Name: "Amsterdam", 97 | } 98 | 99 | ctx := context.Background() 100 | 101 | err := m.Send(ctx, cmd) 102 | 103 | 104 | 105 | ***Func based usage*** 106 | 107 | m, err := mediator.New(mediator.WithBehaviourFunc(func(ctx context.Context, cmd mediator.Message, next mediator.Next) error { 108 | log.Println("Pre Process - 1!") 109 | 110 | next(ctx) 111 | 112 | log.Println("Post Process - 1") 113 | return nil 114 | })) 115 | 116 | ## Examples 117 | 118 | [Simple](https://github.com/eyazici90/go-mediator/tree/master/_examples) 119 | 120 | 121 | 122 | TBD... 123 | -------------------------------------------------------------------------------- /_examples/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/eyazici90/go-mediator/mediator" 8 | ) 9 | 10 | func BenchmarkMediator(b *testing.B) { 11 | m, _ := mediator.New(mediator.WithHandler(&FakeCommand{}, NewFakeCommandHandler())) 12 | 13 | cmd := &FakeCommand{Name: "Emre"} 14 | ctx := context.TODO() 15 | 16 | b.ReportAllocs() 17 | b.ResetTimer() 18 | for i := 0; i < b.N; i++ { 19 | _ = m.Send(ctx, cmd) 20 | } 21 | } 22 | 23 | func BenchmarkHandler(b *testing.B) { 24 | handler := NewFakeCommandHandler() 25 | 26 | cmd := &FakeCommand{Name: "Emre"} 27 | ctx := context.TODO() 28 | 29 | b.ReportAllocs() 30 | b.ResetTimer() 31 | for i := 0; i < b.N; i++ { 32 | _ = handler.Handle(ctx, cmd) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /_examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | 8 | "github.com/eyazici90/go-mediator/internal/must" 9 | "github.com/eyazici90/go-mediator/mediator" 10 | ) 11 | 12 | func main() { 13 | m, err := mediator.New( 14 | mediator.WithBehaviourFunc( 15 | func(ctx context.Context, cmd mediator.Message, next mediator.Next) error { 16 | log.Println("Pre Process - 1") 17 | _ = next(ctx) 18 | log.Println("Post Process - 1") 19 | 20 | return nil 21 | }), mediator.WithBehaviourFunc( 22 | func(ctx context.Context, cmd mediator.Message, next mediator.Next) error { 23 | log.Println("Pre Process- 2") 24 | _ = next(ctx) 25 | log.Println("Post Process - 2") 26 | 27 | return nil 28 | }), 29 | mediator.WithHandler(&FakeCommand{}, NewFakeCommandHandler())) 30 | 31 | must.NotFail(err) 32 | 33 | cmd := &FakeCommand{ 34 | Name: "Emre", 35 | } 36 | ctx := context.Background() 37 | 38 | _ = m.Send(ctx, cmd) 39 | } 40 | 41 | type FakeCommand struct { 42 | Name string 43 | } 44 | 45 | func (*FakeCommand) Key() int { return 1 } 46 | 47 | type FakeCommandHandler struct{} 48 | 49 | func NewFakeCommandHandler() FakeCommandHandler { 50 | return FakeCommandHandler{} 51 | } 52 | 53 | func (FakeCommandHandler) Handle(_ context.Context, command mediator.Message) error { 54 | cmd := command.(*FakeCommand) 55 | if cmd.Name == "" { 56 | return errors.New("name is empty") 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eyazici90/go-mediator 2 | 3 | go 1.23 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /internal/must/not.go: -------------------------------------------------------------------------------- 1 | package must 2 | 3 | func NotFail(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mediator/errors.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | type errType int 4 | 5 | const ( 6 | ErrHandlerNotFound errType = iota 7 | ErrInvalidArg 8 | 9 | errCount 10 | ) 11 | 12 | func (e errType) Error() string { 13 | if e < 0 || e >= errCount { 14 | panic("invalid err number") 15 | } 16 | return errDescriptions[e] 17 | } 18 | 19 | var _ = [1]int{}[len(errDescriptions)-int(errCount)] 20 | 21 | var errDescriptions = [...]string{ 22 | ErrHandlerNotFound: "handler could not be found", 23 | ErrInvalidArg: "invalid arg", 24 | } 25 | -------------------------------------------------------------------------------- /mediator/mediator.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import "context" 4 | 5 | const maxSize = 64 6 | 7 | type Option func(m *Mediator) error 8 | 9 | type ( 10 | Handler interface { 11 | Handle(context.Context, Message) error 12 | } 13 | PipelineBehaviour interface { 14 | Process(context.Context, Message, Next) error 15 | } 16 | Message interface { 17 | Key() int 18 | } 19 | ) 20 | 21 | type Mediator struct { 22 | pipe *pipeline 23 | chain func(ctx context.Context, msg Message, next Next) error 24 | } 25 | 26 | func New(opts ...Option) (*Mediator, error) { 27 | m := &Mediator{ 28 | pipe: &pipeline{ 29 | handlers: make([]Handler, maxSize), 30 | }, 31 | } 32 | for _, opt := range opts { 33 | if err := opt(m); err != nil { 34 | return nil, err 35 | } 36 | } 37 | m.chain = m.pipe.bhs.merge() 38 | return m, nil 39 | } 40 | 41 | func WithBehaviour(behavior PipelineBehaviour) Option { 42 | return func(m *Mediator) error { 43 | return m.pipe.useBehavior(behavior) 44 | } 45 | } 46 | 47 | func WithBehaviourFunc(fn func(context.Context, Message, Next) error) Option { 48 | return func(m *Mediator) error { 49 | return m.pipe.use(fn) 50 | } 51 | } 52 | 53 | func WithHandler(req Message, rh Handler) Option { 54 | return func(m *Mediator) error { 55 | return m.pipe.registerHandler(req, rh) 56 | } 57 | } 58 | 59 | func (m *Mediator) Send(ctx context.Context, msg Message) error { 60 | if m.chain != nil { 61 | return m.chain(ctx, msg, func(ctx context.Context) error { 62 | return m.send(ctx, msg) 63 | }) 64 | } 65 | return m.send(ctx, msg) 66 | } 67 | 68 | func (m *Mediator) send(ctx context.Context, msg Message) error { 69 | key := msg.Key() 70 | handler, err := m.pipe.findHandler(key) 71 | if err != nil { 72 | return err 73 | } 74 | return handler.Handle(ctx, msg) 75 | } 76 | -------------------------------------------------------------------------------- /mediator/mediator_test.go: -------------------------------------------------------------------------------- 1 | package mediator_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/eyazici90/go-mediator/mediator" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMediator_should_dispatch_msg_when_send(t *testing.T) { 12 | cmd := &fakeCommand{ 13 | name: "Amsterdam", 14 | } 15 | handler := &fakeCommandHandler{} 16 | 17 | m, _ := mediator.New( 18 | mediator.WithHandler(&fakeCommand{}, handler), 19 | ) 20 | err := m.Send(context.Background(), cmd) 21 | assert.NoError(t, err) 22 | assert.Equal(t, cmd, handler.captured) 23 | } 24 | 25 | func TestMediator_should_execute_behavior_when_send(t *testing.T) { 26 | var got mediator.Message 27 | behavior := func(ctx context.Context, msg mediator.Message, next mediator.Next) error { 28 | got = msg 29 | return next(ctx) 30 | } 31 | 32 | cmd := &fakeCommand{ 33 | name: "Amsterdam", 34 | } 35 | handler := &fakeCommandHandler{} 36 | 37 | m, _ := mediator.New( 38 | mediator.WithBehaviourFunc(behavior), 39 | mediator.WithHandler(&fakeCommand{}, handler), 40 | ) 41 | err := m.Send(context.Background(), cmd) 42 | assert.NoError(t, err) 43 | assert.Equal(t, cmd, got) 44 | } 45 | 46 | type fakeCommand struct { 47 | name string 48 | } 49 | 50 | func (*fakeCommand) Key() int { return 1 } 51 | 52 | type fakeCommandHandler struct { 53 | captured mediator.Message 54 | } 55 | 56 | func (f *fakeCommandHandler) Handle(_ context.Context, msg mediator.Message) error { 57 | f.captured = msg 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /mediator/pipeline.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import "context" 4 | 5 | type ( 6 | Next func(ctx context.Context) error 7 | ) 8 | 9 | type ( 10 | behavior func(context.Context, Message, Next) error 11 | behaviors []behavior 12 | ) 13 | 14 | func (b behaviors) merge() behavior { 15 | var result func(context.Context, Message, Next) error 16 | 17 | for _, v := range b { 18 | v := v 19 | if result == nil { 20 | result = v 21 | continue 22 | } 23 | seed := result 24 | result = func(ctx context.Context, msg Message, next Next) error { 25 | return seed(ctx, msg, func(ctx context.Context) error { 26 | return v(ctx, msg, next) 27 | }) 28 | } 29 | } 30 | 31 | return result 32 | } 33 | 34 | type pipeline struct { 35 | bhs behaviors 36 | handlers []Handler 37 | } 38 | 39 | func (p *pipeline) useBehavior(behavior PipelineBehaviour) error { 40 | if behavior == nil { 41 | return ErrInvalidArg 42 | } 43 | return p.use(behavior.Process) 44 | } 45 | 46 | func (p *pipeline) use(call func(context.Context, Message, Next) error) error { 47 | if call == nil { 48 | return ErrInvalidArg 49 | } 50 | p.bhs = append(p.bhs, call) 51 | return nil 52 | } 53 | 54 | func (p *pipeline) registerHandler(req Message, h Handler) error { 55 | if req == nil || h == nil { 56 | return ErrInvalidArg 57 | } 58 | key := req.Key() 59 | p.handlers[key] = h 60 | 61 | return nil 62 | } 63 | 64 | func (p *pipeline) findHandler(key int) (Handler, error) { 65 | v := p.handlers[key] 66 | if v == nil { 67 | return nil, ErrHandlerNotFound 68 | } 69 | return v, nil 70 | } 71 | --------------------------------------------------------------------------------