mediate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +140 -0
- data/.vscode/extensions.json +3 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +21 -0
- data/README.md +293 -0
- data/Rakefile +16 -0
- data/lib/mediate/error_handler.rb +44 -0
- data/lib/mediate/error_handler_state.rb +32 -0
- data/lib/mediate/errors/no_handler_error.rb +18 -0
- data/lib/mediate/errors/request_handler_already_exists_error.rb +19 -0
- data/lib/mediate/mediator.rb +195 -0
- data/lib/mediate/notification.rb +9 -0
- data/lib/mediate/notification_handler.rb +38 -0
- data/lib/mediate/postrequest_behavior.rb +38 -0
- data/lib/mediate/prerequest_behavior.rb +37 -0
- data/lib/mediate/request.rb +76 -0
- data/lib/mediate/request_handler.rb +40 -0
- data/lib/mediate/version.rb +5 -0
- data/lib/mediate.rb +45 -0
- data/mediate.gemspec +34 -0
- data/sig/mediate.rbs +4 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 06e45660926271c9435a4c2dd1634974b3873091f12160b907833cab8c9b7abf
|
|
4
|
+
data.tar.gz: 8b1fe7523830f57f8e2499bf8c3575ffc0eef7716478a554f683947a8958dd95
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6f29b77858c6361cbdc54ccc79656d470dd2feb274fe341746fa66d9fc33e34a7c1906bc8df859dfbb0dbf68fa73fc5f1b7819a2bb889565c91388abfbd8d090
|
|
7
|
+
data.tar.gz: 20c32a19443169eb74e41524b8ae9a342a023a2f54a306d182c3419988dbc5479a739c5866012d29a83e1cb1aeace714f6677efaab7c6368d90082fa0655c879
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.6
|
|
3
|
+
Gemspec/DeprecatedAttributeAssignment: # new in 1.30
|
|
4
|
+
Enabled: true
|
|
5
|
+
Gemspec/RequireMFA: # new in 1.23
|
|
6
|
+
Enabled: true
|
|
7
|
+
Layout/LineContinuationLeadingSpace: # new in 1.31
|
|
8
|
+
Enabled: true
|
|
9
|
+
Layout/LineContinuationSpacing: # new in 1.31
|
|
10
|
+
Enabled: true
|
|
11
|
+
Layout/LineEndStringConcatenationIndentation: # new in 1.18
|
|
12
|
+
Enabled: true
|
|
13
|
+
Layout/LineLength:
|
|
14
|
+
Max: 120
|
|
15
|
+
Layout/SpaceBeforeBrackets: # new in 1.7
|
|
16
|
+
Enabled: true
|
|
17
|
+
Lint/AmbiguousAssignment: # new in 1.7
|
|
18
|
+
Enabled: true
|
|
19
|
+
Lint/AmbiguousOperatorPrecedence: # new in 1.21
|
|
20
|
+
Enabled: true
|
|
21
|
+
Lint/AmbiguousRange: # new in 1.19
|
|
22
|
+
Enabled: true
|
|
23
|
+
Lint/ConstantOverwrittenInRescue: # new in 1.31
|
|
24
|
+
Enabled: true
|
|
25
|
+
Lint/DeprecatedConstants: # new in 1.8
|
|
26
|
+
Enabled: true
|
|
27
|
+
Lint/DuplicateBranch: # new in 1.3
|
|
28
|
+
Enabled: true
|
|
29
|
+
Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
|
|
30
|
+
Enabled: true
|
|
31
|
+
Lint/EmptyBlock: # new in 1.1
|
|
32
|
+
Enabled: true
|
|
33
|
+
Lint/EmptyClass: # new in 1.3
|
|
34
|
+
Enabled: false
|
|
35
|
+
Lint/EmptyInPattern: # new in 1.16
|
|
36
|
+
Enabled: true
|
|
37
|
+
Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
|
|
38
|
+
Enabled: true
|
|
39
|
+
Lint/LambdaWithoutLiteralBlock: # new in 1.8
|
|
40
|
+
Enabled: true
|
|
41
|
+
Lint/NoReturnInBeginEndBlocks: # new in 1.2
|
|
42
|
+
Enabled: true
|
|
43
|
+
Lint/NonAtomicFileOperation: # new in 1.31
|
|
44
|
+
Enabled: true
|
|
45
|
+
Lint/NumberedParameterAssignment: # new in 1.9
|
|
46
|
+
Enabled: true
|
|
47
|
+
Lint/OrAssignmentToConstant: # new in 1.9
|
|
48
|
+
Enabled: true
|
|
49
|
+
Lint/RedundantDirGlobSort: # new in 1.8
|
|
50
|
+
Enabled: true
|
|
51
|
+
Lint/RefinementImportMethods: # new in 1.27
|
|
52
|
+
Enabled: true
|
|
53
|
+
Lint/RequireRelativeSelfPath: # new in 1.22
|
|
54
|
+
Enabled: true
|
|
55
|
+
Lint/SymbolConversion: # new in 1.9
|
|
56
|
+
Enabled: true
|
|
57
|
+
Lint/ToEnumArguments: # new in 1.1
|
|
58
|
+
Enabled: true
|
|
59
|
+
Lint/TripleQuotes: # new in 1.9
|
|
60
|
+
Enabled: true
|
|
61
|
+
Lint/UnexpectedBlockArity: # new in 1.5
|
|
62
|
+
Enabled: true
|
|
63
|
+
Lint/UnmodifiedReduceAccumulator: # new in 1.1
|
|
64
|
+
Enabled: true
|
|
65
|
+
Lint/UselessRuby2Keywords: # new in 1.23
|
|
66
|
+
Enabled: true
|
|
67
|
+
Metrics/BlockLength:
|
|
68
|
+
IgnoredMethods: ['describe', 'context']
|
|
69
|
+
Metrics/ClassLength:
|
|
70
|
+
Max: 150
|
|
71
|
+
Naming/BlockForwarding: # new in 1.24
|
|
72
|
+
Enabled: true
|
|
73
|
+
Security/CompoundHash: # new in 1.28
|
|
74
|
+
Enabled: true
|
|
75
|
+
Security/IoMethods: # new in 1.22
|
|
76
|
+
Enabled: true
|
|
77
|
+
Style/ArgumentsForwarding: # new in 1.1
|
|
78
|
+
Enabled: true
|
|
79
|
+
Style/CollectionCompact: # new in 1.2
|
|
80
|
+
Enabled: true
|
|
81
|
+
Style/DocumentDynamicEvalDefinition: # new in 1.1
|
|
82
|
+
Enabled: true
|
|
83
|
+
Style/EndlessMethod: # new in 1.8
|
|
84
|
+
Enabled: true
|
|
85
|
+
Style/EnvHome: # new in 1.29
|
|
86
|
+
Enabled: true
|
|
87
|
+
Style/FetchEnvVar: # new in 1.28
|
|
88
|
+
Enabled: true
|
|
89
|
+
Style/FileRead: # new in 1.24
|
|
90
|
+
Enabled: true
|
|
91
|
+
Style/FileWrite: # new in 1.24
|
|
92
|
+
Enabled: true
|
|
93
|
+
Style/HashConversion: # new in 1.10
|
|
94
|
+
Enabled: true
|
|
95
|
+
Style/HashExcept: # new in 1.7
|
|
96
|
+
Enabled: true
|
|
97
|
+
Style/IfWithBooleanLiteralBranches: # new in 1.9
|
|
98
|
+
Enabled: true
|
|
99
|
+
Style/InPatternThen: # new in 1.16
|
|
100
|
+
Enabled: true
|
|
101
|
+
Style/MapCompactWithConditionalBlock: # new in 1.30
|
|
102
|
+
Enabled: true
|
|
103
|
+
Style/MapToHash: # new in 1.24
|
|
104
|
+
Enabled: true
|
|
105
|
+
Style/MultilineInPatternThen: # new in 1.16
|
|
106
|
+
Enabled: true
|
|
107
|
+
Style/NegatedIfElseCondition: # new in 1.2
|
|
108
|
+
Enabled: true
|
|
109
|
+
Style/NestedFileDirname: # new in 1.26
|
|
110
|
+
Enabled: true
|
|
111
|
+
Style/NilLambda: # new in 1.3
|
|
112
|
+
Enabled: true
|
|
113
|
+
Style/NumberedParameters: # new in 1.22
|
|
114
|
+
Enabled: true
|
|
115
|
+
Style/NumberedParametersLimit: # new in 1.22
|
|
116
|
+
Enabled: true
|
|
117
|
+
Style/ObjectThen: # new in 1.28
|
|
118
|
+
Enabled: true
|
|
119
|
+
Style/OpenStructUse: # new in 1.23
|
|
120
|
+
Enabled: true
|
|
121
|
+
Style/QuotedSymbols: # new in 1.16
|
|
122
|
+
Enabled: true
|
|
123
|
+
Style/RedundantArgument: # new in 1.4
|
|
124
|
+
Enabled: true
|
|
125
|
+
Style/RedundantInitialize: # new in 1.27
|
|
126
|
+
Enabled: true
|
|
127
|
+
Style/RedundantSelfAssignmentBranch: # new in 1.19
|
|
128
|
+
Enabled: true
|
|
129
|
+
Style/SelectByRegexp: # new in 1.22
|
|
130
|
+
Enabled: true
|
|
131
|
+
Style/StringChars: # new in 1.12
|
|
132
|
+
Enabled: true
|
|
133
|
+
Style/StringLiterals:
|
|
134
|
+
Enabled: true
|
|
135
|
+
EnforcedStyle: double_quotes
|
|
136
|
+
Style/StringLiteralsInInterpolation:
|
|
137
|
+
Enabled: true
|
|
138
|
+
EnforcedStyle: double_quotes
|
|
139
|
+
Style/SwapValues: # new in 1.1
|
|
140
|
+
Enabled: true
|
data/Gemfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
# Specify your gem's dependencies in mediate.gemspec
|
|
6
|
+
gemspec
|
|
7
|
+
|
|
8
|
+
gem "rake", "~> 13.0"
|
|
9
|
+
|
|
10
|
+
gem "rspec", "~> 3.0"
|
|
11
|
+
|
|
12
|
+
gem "rubocop", "~> 1.21"
|
|
13
|
+
|
|
14
|
+
gem "yard", "~> 0.9.28"
|
|
15
|
+
|
|
16
|
+
gem "concurrent-ruby", "~> 1.1", require: "concurrent"
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
mediate (0.1.0)
|
|
5
|
+
concurrent-ruby (~> 1.1)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
ast (2.4.2)
|
|
11
|
+
concurrent-ruby (1.1.10)
|
|
12
|
+
diff-lcs (1.5.0)
|
|
13
|
+
json (2.6.2)
|
|
14
|
+
parallel (1.22.1)
|
|
15
|
+
parser (3.1.2.0)
|
|
16
|
+
ast (~> 2.4.1)
|
|
17
|
+
rainbow (3.1.1)
|
|
18
|
+
rake (13.0.6)
|
|
19
|
+
regexp_parser (2.5.0)
|
|
20
|
+
rexml (3.2.5)
|
|
21
|
+
rspec (3.11.0)
|
|
22
|
+
rspec-core (~> 3.11.0)
|
|
23
|
+
rspec-expectations (~> 3.11.0)
|
|
24
|
+
rspec-mocks (~> 3.11.0)
|
|
25
|
+
rspec-core (3.11.0)
|
|
26
|
+
rspec-support (~> 3.11.0)
|
|
27
|
+
rspec-expectations (3.11.0)
|
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
29
|
+
rspec-support (~> 3.11.0)
|
|
30
|
+
rspec-mocks (3.11.1)
|
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
32
|
+
rspec-support (~> 3.11.0)
|
|
33
|
+
rspec-support (3.11.0)
|
|
34
|
+
rubocop (1.31.2)
|
|
35
|
+
json (~> 2.3)
|
|
36
|
+
parallel (~> 1.10)
|
|
37
|
+
parser (>= 3.1.0.0)
|
|
38
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
39
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
40
|
+
rexml (>= 3.2.5, < 4.0)
|
|
41
|
+
rubocop-ast (>= 1.18.0, < 2.0)
|
|
42
|
+
ruby-progressbar (~> 1.7)
|
|
43
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
44
|
+
rubocop-ast (1.19.1)
|
|
45
|
+
parser (>= 3.1.1.0)
|
|
46
|
+
ruby-progressbar (1.11.0)
|
|
47
|
+
unicode-display_width (2.2.0)
|
|
48
|
+
webrick (1.7.0)
|
|
49
|
+
yard (0.9.28)
|
|
50
|
+
webrick (~> 1.7.0)
|
|
51
|
+
|
|
52
|
+
PLATFORMS
|
|
53
|
+
arm64-darwin-21
|
|
54
|
+
ruby
|
|
55
|
+
x86_64-linux
|
|
56
|
+
|
|
57
|
+
DEPENDENCIES
|
|
58
|
+
concurrent-ruby (~> 1.1)
|
|
59
|
+
mediate!
|
|
60
|
+
rake (~> 13.0)
|
|
61
|
+
rspec (~> 3.0)
|
|
62
|
+
rubocop (~> 1.21)
|
|
63
|
+
yard (~> 0.9.28)
|
|
64
|
+
|
|
65
|
+
BUNDLED WITH
|
|
66
|
+
2.3.17
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 TODO: Write your name
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Mediate
|
|
2
|
+
|
|
3
|
+
A simple mediator implementation for Ruby inspired by [Mediatr](https://github.com/jbogard/MediatR).
|
|
4
|
+
|
|
5
|
+
Decouple application components by sending a request through the mediator and receiving a response from a handler, instead of directly calling methods on imported classes.
|
|
6
|
+
|
|
7
|
+
Supports request/response, notifications (i.e., events), pre- and post-request handler decorators, and error handling.
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [Requests](#requests)
|
|
12
|
+
- [Implicit handler declaration](#implicit-handler-declaration)
|
|
13
|
+
- [Request polymorphism](#request-polymorphism)
|
|
14
|
+
- [Pre- and post-request behaviors](#pre--and-post-request-behaviors)
|
|
15
|
+
- [Notifications](#notifications)
|
|
16
|
+
- [Error handlers](#error-handlers)
|
|
17
|
+
- [Testing](#testing)
|
|
18
|
+
- [Testing implicit request handlers](#testing-implicit-request-handlers)
|
|
19
|
+
- [Using with Rails](#using-with-rails)
|
|
20
|
+
- [Development](#development)
|
|
21
|
+
- [Contributing](#contributing)
|
|
22
|
+
- [License](#license)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Add this to your Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem "mediate"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
And run:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
bundle
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
There are two types of messages that can be sent through the mediator:
|
|
41
|
+
|
|
42
|
+
- Requests (`Mediate::Request`) have exactly one handler (`Mediate::RequestHandler`), which returns a response.
|
|
43
|
+
- Notifications (`Mediate::Notification`) are `publish`ed to zero or more handlers (`Mediate::NotificationHandler`). Nothing is returned to the caller.
|
|
44
|
+
|
|
45
|
+
### Requests
|
|
46
|
+
|
|
47
|
+
To define a request, declare a class that inherits from `Mediate::Request`.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
class Ping < Mediate::Request
|
|
51
|
+
attr_reader :message
|
|
52
|
+
|
|
53
|
+
def initialize(message)
|
|
54
|
+
@message = message
|
|
55
|
+
super()
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To register a handler for it, declare a class that inherits from `Mediate::RequestHandler`, call the class method `handles` passing the class of requests that it handles, and implement the `handle` method.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class PingHandler < Mediate::RequestHandler
|
|
64
|
+
handles Ping
|
|
65
|
+
|
|
66
|
+
def handle(request)
|
|
67
|
+
"Received: #{request.message}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To send a request, pass it to `Mediate.dispatch`. The mediator will resolve the registered handler according to the request type and return the result of its `handle` method.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
response = Mediate.dispatch(Ping.new('hello'))
|
|
76
|
+
puts response # 'Received: hello'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The only requirement for `RequestHandler`s, besides implementing the `handle` method, is that __they should have a constructor that can be called without arguments__. This applies to all `*Handler` and `*Behavior` classes. For example, the following would work because all constructor parameters have default values.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class PingHandler < Mediate::RequestHandler
|
|
83
|
+
handles Ping
|
|
84
|
+
|
|
85
|
+
def initialize(service = SomeService.new)
|
|
86
|
+
@service = service
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle(request)
|
|
90
|
+
@service.call("Received: #{request.message}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note that only one handler can be registered for a particular request class; attempting to register another handler for `Ping` would raise a `RequestHandlerAlreadyExistsError`.
|
|
96
|
+
|
|
97
|
+
#### Implicit handler declaration
|
|
98
|
+
|
|
99
|
+
For simple handlers, you can skip the explicit `RequestHandler` declaration above and instead pass a block to `Request.handle_with`.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class Ping < Mediate::Request
|
|
103
|
+
attr_reader :message
|
|
104
|
+
|
|
105
|
+
def initialize(message)
|
|
106
|
+
@message = message
|
|
107
|
+
super()
|
|
108
|
+
end
|
|
109
|
+
# This will have the same behavior as the PingHandler declaration above.
|
|
110
|
+
handle_with { |request| "Received: #{request.message}" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
response = Mediate.dispatch(Ping.new('hello'))
|
|
114
|
+
puts response # 'Received: hello'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Behind the scenes, this defines a `Ping::Handler` class that calls the given block in its `handle` method. For testing purposes, you can get an instance of this handler class by calling `Mediate::Request.create_implicit_handler` (see [Testing implicit request handlers](#testing-implicit-request-handlers)).
|
|
118
|
+
|
|
119
|
+
#### Request polymorphism
|
|
120
|
+
|
|
121
|
+
The mediator resolves handlers by moving up the request's inheritance chain until it finds a registered handler for that class. For example, subclasses of `Ping` would be handled by `PingHandler`.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
class SubPing < Ping; end
|
|
125
|
+
puts Mediate.dispatch(SubPing.new('howdy')) # 'Received: howdy'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Unless we registered a handler for `SubPing` explicitly.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
class SubPing < Ping
|
|
132
|
+
handle_with { |request| "Received from SubPing: #{request.message}" }
|
|
133
|
+
end
|
|
134
|
+
puts Mediate.dispatch(SubPing.new('howdy')) # 'Received from SubPing: howdy'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Pre- and post-request behaviors
|
|
138
|
+
|
|
139
|
+
For certain cases, you will want code to run before or after a request is handled, e.g., logging, authorization, validation, backwards compatibility, etc. Effectively, these act as decorators for your request handler(s). You can register `Mediate::PrerequestBehavior`s and `Mediate::PostrequestBehavior`s for this purpose.
|
|
140
|
+
|
|
141
|
+
Behaviors will run for any request that is or inherits from the request class registered. For example, if you wanted a behavior to run for every request, you could register it with `handles Mediate::Request`. Unlike request handlers, multiple behaviors can be registered for the same request class.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class PreLoggingBehavior < Mediate::PrerequestBehavior
|
|
145
|
+
handles Mediate::Request # This will be called before all request handlers
|
|
146
|
+
|
|
147
|
+
def initialize(logger = Logger)
|
|
148
|
+
@logger = logger
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def handle(request)
|
|
152
|
+
@logger.info("Received request: #{request}")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class PingValidator < Mediate::PrerequestBehavior
|
|
157
|
+
handles Ping # Will be called before Ping requests or any subclasses of Ping
|
|
158
|
+
|
|
159
|
+
def handle(request)
|
|
160
|
+
raise "Ping is missing message" if request.message.nil?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class PostLoggingBehavior < Mediate::PostrequestBehavior
|
|
165
|
+
handles Mediate::Request # Will be called after all request handlers
|
|
166
|
+
|
|
167
|
+
def initialize(logger = Logger)
|
|
168
|
+
@logger = logger
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def handle(request, result)
|
|
172
|
+
@logger.info("Request: #{request} resulted in #{result}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Notifications
|
|
178
|
+
|
|
179
|
+
Notifications are messages that can be passed to multiple handlers. To publish a notification, call `Mediate.publish(notification)`. No response is returned from `publish`.
|
|
180
|
+
|
|
181
|
+
Define a notification by inheriting from `Mediate::Notification`.
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class PostCreated < Mediate::Notification
|
|
185
|
+
attr_reader :post
|
|
186
|
+
|
|
187
|
+
def initialize(post)
|
|
188
|
+
@post = post
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Declare and register a handler by inheriting from `Mediate::NotificationHandler`, calling `handles` with the notification class to handle, and implementing the `handle` method.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class PostCreatedHandler < Mediate::NotificationHandler
|
|
197
|
+
handles PostCreated
|
|
198
|
+
|
|
199
|
+
def handle(notification)
|
|
200
|
+
# do something with PostCreated notification...
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Like [request behaviors](#pre--and-post-request-behaviors), all notification handlers that are registered for a notification class or any of its superclasses will be called when a given notification is published. For example, a handler that `handles Mediate::Notification` will be called when any notification is published. Handlers will be called in order of inheritance of their registered notifications from subclass to superclass (and in order of registration if the registered notification class is the same).
|
|
206
|
+
|
|
207
|
+
### Error handlers
|
|
208
|
+
|
|
209
|
+
When a request or notification handler raises a `StandardError`, the mediator will find all `ErrorHandler`s that have been registered for that request/notification class (or superclasses) and the exception class (or superclasses).
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# This will be called on any StandardError from any request or notification handler
|
|
213
|
+
class GlobalErrorHandler < Mediate::ErrorHandler
|
|
214
|
+
handles StandardError, Mediate::Request
|
|
215
|
+
handles StandardError, Mediate::Notification
|
|
216
|
+
|
|
217
|
+
# dispatched is the Request or Notification
|
|
218
|
+
def handle(dispatched, exception, state)
|
|
219
|
+
# do something...
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# This would get called when ActiveRecord::RecordNotFound is raised while handling a QueryRequest
|
|
224
|
+
class NotFoundHandler < Mediate::ErrorHandler
|
|
225
|
+
handles ActiveRecord::RecordNotFound, QueryRequest
|
|
226
|
+
|
|
227
|
+
def handle(dispatched, exception, state)
|
|
228
|
+
# ...
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Note that the exception class passed to handles must be `StandardError` or a subclass of it.
|
|
234
|
+
|
|
235
|
+
The `state` parameter of `handle` is a `Mediate::ErrorHandlerState` instance that represents whether the exception has been "handled" or not. By calling `set_as_handled` and optionally passing in a result, all subsequent error handlers will be skipped and the given result will be returned to the caller of `dispatch` (obviously, if the error was raised from a notification handler, nothing will be returned).
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class ValidationErrorHandler < Mediate::ErrorHandler
|
|
239
|
+
handles ActiveRecord::RecordInvalid, Mediate::Request
|
|
240
|
+
|
|
241
|
+
def handle(dispatched, exception, state)
|
|
242
|
+
state.set_as_handled(exception.record.errors)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Testing
|
|
248
|
+
|
|
249
|
+
All of the handler and behavior classes described above are just normal Ruby classes. You can instantiate them and call their `handle` methods to test as you normally would.
|
|
250
|
+
|
|
251
|
+
Special consideration is only required when testing paths that invoke methods on the mediator itself (e.g., `Mediate.dispatch` or `Mediate.publish`), since it is designed to be a singleton. The mediator's registration methods are idempotent (and thread-safe), so re-registering handlers should not cause issues. However, if you want to ensure that you are not sharing state between tests, you can call the `Mediate.mediator.reset` method in your test setup or clean-up to remove all handler and behavior registrations.
|
|
252
|
+
|
|
253
|
+
#### Testing implicit request handlers
|
|
254
|
+
|
|
255
|
+
How can you test a request handler defined using `handle_with` and a block like the following?
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
class ExampleRequest < Mediate::Request
|
|
259
|
+
handle_with do |request|
|
|
260
|
+
# ....
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The `handle_with` method defines a handler class and registers it with the mediator to handle the containing request class. `Mediate::Request` provides a convenience method, `create_implicit_handler`, that creates an instance of this handler class. You can then call `handle` on that method like normal to test it.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
RSpec.describe "ExampleRequestHandler" do
|
|
269
|
+
let(:handler) { ExampleRequest.create_implicit_handler }
|
|
270
|
+
|
|
271
|
+
it "returns something" do
|
|
272
|
+
expect(handler.handle(ExampleRequest.new)).to be_truthy
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Using with Rails
|
|
278
|
+
|
|
279
|
+
TODO
|
|
280
|
+
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
284
|
+
|
|
285
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
286
|
+
|
|
287
|
+
## Contributing
|
|
288
|
+
|
|
289
|
+
Bug reports and pull requests are welcome in this repo.
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
require "yard"
|
|
13
|
+
|
|
14
|
+
YARD::Rake::YardocTask.new
|
|
15
|
+
|
|
16
|
+
task default: %i[spec rubocop]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# @abstract override {#handle} to implement.
|
|
6
|
+
#
|
|
7
|
+
# An abstract base class that handles exceptions raised by request handlers, behaviors, or notification handlers.
|
|
8
|
+
#
|
|
9
|
+
class ErrorHandler
|
|
10
|
+
#
|
|
11
|
+
# Registers this to handle exceptions of type exception_class when raised while handling requests or notifications
|
|
12
|
+
# of type dispatched_class.
|
|
13
|
+
#
|
|
14
|
+
# @param [StandardError] exception_class the type of exceptions that this should handle
|
|
15
|
+
# @param [Class] dispatched_class the request or notification type
|
|
16
|
+
# (should inherit from Mediate::Request or Mediate::Notification)
|
|
17
|
+
# @param [Mediate::Mediator] mediator the Mediator instance to register on
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
#
|
|
21
|
+
# @raise [ArgumentError] if exception_class is not a StandardError
|
|
22
|
+
# or dispatched_class is not a Request or Notification
|
|
23
|
+
def self.handles(exception_class = StandardError, dispatched_class = Mediate::Request, mediator = Mediate.mediator)
|
|
24
|
+
mediator.register_error_handler(self, exception_class, dispatched_class)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# The method to implement to handle exceptions.
|
|
29
|
+
#
|
|
30
|
+
# @abstract
|
|
31
|
+
#
|
|
32
|
+
# @param [Mediate::Request, Mediate::Notification] _dispatched the request or notification that was been handled
|
|
33
|
+
# @param [StandardError] _exception the exception that was raised
|
|
34
|
+
# @param [Mediate::ErrorHandlerState] _state the result of handling the current exception--
|
|
35
|
+
# call state.set_as_handled(result) to skip subsequent error handlers and return result
|
|
36
|
+
# (if the exception was thrown while handling a request; notification handlers will not return anything)
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
def handle(_dispatched, _exception, _state)
|
|
41
|
+
raise NoMethodError, "handle must be implemented"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# Represents the result of handling an exception
|
|
6
|
+
#
|
|
7
|
+
class ErrorHandlerState
|
|
8
|
+
attr_reader :result
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Sets the state as handled with the given result. Subsequent error handlers will be skipped and, if
|
|
12
|
+
# the exception was thrown while handling a Mediate::Request, this result will be returned.
|
|
13
|
+
#
|
|
14
|
+
# @param result if the exception was thrown as part of handling a Mediate::Request, this will be returned
|
|
15
|
+
#
|
|
16
|
+
# @return [Boolean] true
|
|
17
|
+
#
|
|
18
|
+
def set_as_handled(result = nil)
|
|
19
|
+
@result = result
|
|
20
|
+
@handled = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Indicates whether the current exception has been handled and the result should be returned (if applicable).
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] whether the current exception has been handled
|
|
27
|
+
#
|
|
28
|
+
def handled?
|
|
29
|
+
!!@handled
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# Namespace that contains custom Mediate errors.
|
|
6
|
+
#
|
|
7
|
+
module Errors
|
|
8
|
+
#
|
|
9
|
+
# Indicates that a Mediate::Request was sent to the Mediator, but no Mediate::RequestHandler
|
|
10
|
+
# was registered to handle it.
|
|
11
|
+
#
|
|
12
|
+
class NoHandlerError < StandardError
|
|
13
|
+
def initialize(request_class)
|
|
14
|
+
super("No handler for #{request_class}. Call handles(#{request_class}) on a RequestHandler to register.")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# Namespace that contains custom Mediate errors.
|
|
6
|
+
#
|
|
7
|
+
module Errors
|
|
8
|
+
#
|
|
9
|
+
# Indicates that a Mediate::RequestHandler was already registered for the given request_class.
|
|
10
|
+
#
|
|
11
|
+
class RequestHandlerAlreadyExistsError < StandardError
|
|
12
|
+
def initialize(request_class, registered_handler_class, attempted_handler_class)
|
|
13
|
+
super("Attempted to register #{attempted_handler_class} to handle #{request_class},\
|
|
14
|
+
but #{registered_handler_class} is already registered to handle #{request_class}.\
|
|
15
|
+
This is probably a mistake, as only one handler should be registered per request type.")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
require "singleton"
|
|
5
|
+
|
|
6
|
+
require_relative "errors/no_handler_error"
|
|
7
|
+
require_relative "errors/request_handler_already_exists_error"
|
|
8
|
+
require_relative "error_handler"
|
|
9
|
+
require_relative "error_handler_state"
|
|
10
|
+
require_relative "notification"
|
|
11
|
+
require_relative "notification_handler"
|
|
12
|
+
require_relative "postrequest_behavior"
|
|
13
|
+
require_relative "prerequest_behavior"
|
|
14
|
+
require_relative "request"
|
|
15
|
+
require_relative "request_handler"
|
|
16
|
+
|
|
17
|
+
module Mediate
|
|
18
|
+
#
|
|
19
|
+
# Implements the mediator pattern. Call {#dispatch} to send requests and
|
|
20
|
+
# {#publish} to publish notifications.
|
|
21
|
+
#
|
|
22
|
+
class Mediator
|
|
23
|
+
include Singleton
|
|
24
|
+
REQUEST_BASE = Mediate::Request
|
|
25
|
+
NOTIF_BASE = Mediate::Notification
|
|
26
|
+
private_constant :REQUEST_BASE, :NOTIF_BASE
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
reset
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Sends a request to the registered handler, passing to any applicable pipeline behaviors.
|
|
34
|
+
#
|
|
35
|
+
# @param [Mediate::Request] request
|
|
36
|
+
#
|
|
37
|
+
# @return the response returned from the Mediate::RequestHandler
|
|
38
|
+
#
|
|
39
|
+
# @raise [ArgumentError] if request is nil
|
|
40
|
+
# @raise [Mediate::Errors::NoHandlerError] if no handlers have been registered for the given request's type
|
|
41
|
+
#
|
|
42
|
+
def dispatch(request)
|
|
43
|
+
raise ArgumentError, "request cannot be nil" if request.nil?
|
|
44
|
+
|
|
45
|
+
request_handler = resolve_handler(@request_handlers, request.class, REQUEST_BASE)
|
|
46
|
+
raise Errors::NoHandlerError, request.class if request_handler == NullHandler
|
|
47
|
+
|
|
48
|
+
prerequest_handlers = collect_by_inheritance(@prerequest_behaviors, request.class, REQUEST_BASE)
|
|
49
|
+
postrequest_handlers = collect_by_inheritance(@postrequest_behaviors, request.class, REQUEST_BASE)
|
|
50
|
+
run_request_pipeline(request, prerequest_handlers, request_handler, postrequest_handlers)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# Sends a notification to all register handlers for the given notification's type.
|
|
55
|
+
#
|
|
56
|
+
# @param [Mediate::Notification] notification
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
#
|
|
60
|
+
def publish(notification)
|
|
61
|
+
raise ArgumentError, "notification cannot be nil" if notification.nil?
|
|
62
|
+
|
|
63
|
+
handler_classes = collect_by_inheritance(@notification_handlers, notification.class, NOTIF_BASE)
|
|
64
|
+
handler_classes.each do |handler_class|
|
|
65
|
+
handler_class.new.handle(notification)
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
handle_exception(notification, e, NOTIF_BASE)
|
|
68
|
+
# Don't break from loop, since we don't want a single notification handler to prevent others from
|
|
69
|
+
# receiving notification.
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register_request_handler(handler_class, request_class)
|
|
74
|
+
validate_base_class(handler_class, Mediate::RequestHandler)
|
|
75
|
+
validate_base_class(request_class, REQUEST_BASE)
|
|
76
|
+
raise_if_request_handler_exists(request_class, handler_class)
|
|
77
|
+
@request_handlers[request_class] = handler_class
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def register_notification_handler(handler_class, notif_class)
|
|
81
|
+
validate_base_class(handler_class, Mediate::NotificationHandler)
|
|
82
|
+
validate_base_class(notif_class, NOTIF_BASE, allow_base: true)
|
|
83
|
+
append_to_hash_value(@notification_handlers, notif_class, handler_class)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def register_prerequest_behavior(behavior_class, request_class)
|
|
87
|
+
validate_base_class(behavior_class, Mediate::PrerequestBehavior)
|
|
88
|
+
validate_base_class(request_class, REQUEST_BASE, allow_base: true)
|
|
89
|
+
append_to_hash_value(@prerequest_behaviors, request_class, behavior_class)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def register_postrequest_behavior(behavior_class, request_class)
|
|
93
|
+
validate_base_class(behavior_class, Mediate::PostrequestBehavior)
|
|
94
|
+
validate_base_class(request_class, REQUEST_BASE, allow_base: true)
|
|
95
|
+
append_to_hash_value(@postrequest_behaviors, request_class, behavior_class)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def register_error_handler(handler_class, exception_class, dispatch_class)
|
|
99
|
+
if dispatch_class <= NOTIF_BASE
|
|
100
|
+
register_error_handler_for_dispatch(handler_class, exception_class, dispatch_class, NOTIF_BASE)
|
|
101
|
+
else
|
|
102
|
+
register_error_handler_for_dispatch(handler_class, exception_class, dispatch_class, REQUEST_BASE)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
#
|
|
107
|
+
# Clears all registered handlers and behaviors for this Mediator instance. This is useful
|
|
108
|
+
# for cleaning up after integration tests.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
#
|
|
112
|
+
def reset
|
|
113
|
+
@request_handlers = Concurrent::Map.new
|
|
114
|
+
@notification_handlers = Concurrent::Map.new
|
|
115
|
+
@prerequest_behaviors = Concurrent::Map.new
|
|
116
|
+
@postrequest_behaviors = Concurrent::Map.new
|
|
117
|
+
@exception_handlers = Concurrent::Map.new
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def raise_if_request_handler_exists(request_class, new_handler)
|
|
123
|
+
registered = @request_handlers.fetch(request_class, nil)
|
|
124
|
+
return if registered.nil? || registered == new_handler
|
|
125
|
+
|
|
126
|
+
raise Errors::RequestHandlerAlreadyExistsError.new(request_class, registered, new_handler)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def register_error_handler_for_dispatch(handler_class, exception_class, dispatch_class, dispatch_base_class)
|
|
130
|
+
validate_base_class(handler_class, Mediate::ErrorHandler)
|
|
131
|
+
validate_base_class(exception_class, StandardError, allow_base: true)
|
|
132
|
+
validate_base_class(dispatch_class, dispatch_base_class, allow_base: true)
|
|
133
|
+
map = @exception_handlers.fetch_or_store(exception_class, Concurrent::Map.new)
|
|
134
|
+
append_to_hash_value(map, dispatch_class, handler_class)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def run_request_pipeline(request, pre_handlers, request_handler, post_handlers)
|
|
138
|
+
result = nil
|
|
139
|
+
pre_handlers.each { |handler_class| handler_class.new.handle(request) }
|
|
140
|
+
result = request_handler.new.handle(request)
|
|
141
|
+
post_handlers.each { |handler_class| handler_class.new.handle(request, result) }
|
|
142
|
+
result
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
handle_exception(request, e, REQUEST_BASE)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def append_to_hash_value(hash, key, value)
|
|
148
|
+
hash[key] = hash.fetch(key, Concurrent::Set.new) << value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def validate_base_class(given, expected_base, allow_base: false)
|
|
152
|
+
raise ArgumentError, "class cannot be nil" if given.nil? || expected_base.nil?
|
|
153
|
+
|
|
154
|
+
return if allow_base && given == expected_base
|
|
155
|
+
|
|
156
|
+
raise ArgumentError, "#{given} does not inherit from #{expected_base}" unless given < expected_base
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def handle_exception(dispatched, exception, dispatch_base_class)
|
|
160
|
+
exception_to_dispatched_maps = collect_by_inheritance(@exception_handlers, exception.class, StandardError)
|
|
161
|
+
handler_classes = exception_to_dispatched_maps.reduce(Concurrent::Set.new) do |memo, curr|
|
|
162
|
+
collect_by_inheritance(curr, dispatched.class, dispatch_base_class, memo)
|
|
163
|
+
end
|
|
164
|
+
state = ErrorHandlerState.new
|
|
165
|
+
handler_classes.each do |handler_class|
|
|
166
|
+
handler_class.new.handle(dispatched, exception, state)
|
|
167
|
+
break if state.handled?
|
|
168
|
+
end
|
|
169
|
+
state.result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def resolve_handler(handlers_hash, request_class, base_class)
|
|
173
|
+
value = handlers_hash[request_class]
|
|
174
|
+
return value unless value.nil?
|
|
175
|
+
return NullHandler if request_class >= base_class
|
|
176
|
+
|
|
177
|
+
resolve_handler(handlers_hash, request_class.superclass, base_class)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def collect_by_inheritance(hash, key_class, base_class, collected = Concurrent::Set.new)
|
|
181
|
+
values = hash.fetch(key_class, Concurrent::Set.new)
|
|
182
|
+
# Wrap values in Set and flatten to account for case when values is not a Set itself.
|
|
183
|
+
# This may break if values is nested Sets, although we don't have that case yet.
|
|
184
|
+
new_collected = (collected || Concurrent::Set.new) | Concurrent::Set[values].flatten
|
|
185
|
+
return new_collected if key_class > base_class
|
|
186
|
+
|
|
187
|
+
collect_by_inheritance(hash, key_class.superclass, base_class, new_collected)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
#
|
|
191
|
+
# A null object for a Mediate::RequestHandler
|
|
192
|
+
#
|
|
193
|
+
class NullHandler; end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# @abstract override {#handle} to implement.
|
|
6
|
+
#
|
|
7
|
+
# Abstract base class of a handler of notifications.
|
|
8
|
+
#
|
|
9
|
+
class NotificationHandler
|
|
10
|
+
#
|
|
11
|
+
# Registers this handler for the given notification Class.
|
|
12
|
+
# The notification Class must have Mediate::Notification as a superclass.
|
|
13
|
+
#
|
|
14
|
+
# @param [Class] notif_class the Class of the notifications that get passed to this handler
|
|
15
|
+
# @param [Mediate::Mediator] mediator the mediator instance to register the handler with
|
|
16
|
+
#
|
|
17
|
+
# @raise [ArgumentError] if notif_class does not inherit from Mediate::Notification
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
#
|
|
21
|
+
def self.handles(notif_class = Mediate::Notification, mediator = Mediate.mediator)
|
|
22
|
+
mediator.register_notification_handler(self, notif_class)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# The method to implement that handles the notifications registered for this handler.
|
|
27
|
+
#
|
|
28
|
+
# @abstract
|
|
29
|
+
#
|
|
30
|
+
# @param [Mediate::Notification] _notification
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
#
|
|
34
|
+
def handle(_notification)
|
|
35
|
+
raise NoMethodError, "handle must be implemented"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# @abstract override {#handle} to implement
|
|
6
|
+
#
|
|
7
|
+
# Abstract base class of a pipeline behavior that processes a request after the RequestHandler finishes.
|
|
8
|
+
#
|
|
9
|
+
class PostrequestBehavior
|
|
10
|
+
#
|
|
11
|
+
# Registers this behavior to handle requests of the given type.
|
|
12
|
+
#
|
|
13
|
+
# @param [Class] request_class the type of requests to handle (should inherit from Mediate::Request)
|
|
14
|
+
# @param [Mediate::Mediator] mediator the Mediator instance to register to
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
#
|
|
18
|
+
# @raise [ArgumentError] if request_class does not inherit from Mediate::Request
|
|
19
|
+
#
|
|
20
|
+
def self.handles(request_class = Mediate::Request, mediator = Mediate.mediator)
|
|
21
|
+
mediator.register_postrequest_behavior(self, request_class)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# @abstract
|
|
26
|
+
#
|
|
27
|
+
# The method that handles the request and the result that the handler returned.
|
|
28
|
+
#
|
|
29
|
+
# @param [Mediate::Request] _request
|
|
30
|
+
# @param _result what was returned by the RequestHandler
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
#
|
|
34
|
+
def handle(_request, _result)
|
|
35
|
+
raise NoMethodError, "handle must be implemented"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# @abstract override {#handle} to implement
|
|
6
|
+
#
|
|
7
|
+
# Abstract base class of a pipeline behavior that processes a request before it's given to the handler.
|
|
8
|
+
#
|
|
9
|
+
class PrerequestBehavior
|
|
10
|
+
#
|
|
11
|
+
# Registers this behavior to handle requests of the given type.
|
|
12
|
+
#
|
|
13
|
+
# @param [Class] request_class the type of requests to handle (should inherit from Mediate::Request)
|
|
14
|
+
# @param [Mediate::Mediator] mediator the Mediator instance to register to
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
#
|
|
18
|
+
# @raise [ArgumentError] if request_class does not inherit from Mediate::Request
|
|
19
|
+
#
|
|
20
|
+
def self.handles(request_class = Mediate::Request, mediator = Mediate.mediator)
|
|
21
|
+
mediator.register_prerequest_behavior(self, request_class)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# @abstract
|
|
26
|
+
#
|
|
27
|
+
# The method that handles the request.
|
|
28
|
+
#
|
|
29
|
+
# @param [Mediate::Request] _request
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
#
|
|
33
|
+
def handle(_request)
|
|
34
|
+
raise NoMethodError, "handle must be implemented"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# The base class of a request that can be dispatched to the mediator, which
|
|
6
|
+
# returns a response from its handler.
|
|
7
|
+
#
|
|
8
|
+
class Request
|
|
9
|
+
IMPLICIT_HANDLER_CLASS_NAME = "Handler"
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Registers a handler for this Request type using the given block as the handle method.
|
|
13
|
+
#
|
|
14
|
+
# @param [Mediate::Mediator] mediator the instance to register the handler on
|
|
15
|
+
# @param [Proc] &proc the block that will handle the request
|
|
16
|
+
#
|
|
17
|
+
# @raises [ArgumentError] if no block is given
|
|
18
|
+
#
|
|
19
|
+
# @example When a request of this type is dispatched, the handle_with block will run
|
|
20
|
+
# class MyRequest < Mediate::Request
|
|
21
|
+
# handle_with do |request|
|
|
22
|
+
# ## do something with request...
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
def self.handle_with(mediator = Mediate.mediator, &proc)
|
|
26
|
+
raise ArgumentError, "expected block to be passed to #handle_with." unless proc
|
|
27
|
+
|
|
28
|
+
if implicit_handler_defined?
|
|
29
|
+
raise "#{name}::#{IMPLICIT_HANDLER_CLASS_NAME} is already defined. Cannot create implicit handler."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
handler_class = define_handler(proc)
|
|
33
|
+
const_set(IMPLICIT_HANDLER_CLASS_NAME, handler_class)
|
|
34
|
+
mediator.register_request_handler(handler_class, self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# If an implicit handler is defined for this Request using the #handle_with method,
|
|
39
|
+
# this will return an instance of it. Use this for testing the handler.
|
|
40
|
+
#
|
|
41
|
+
# @return [Mediate::RequestHandler] the implicit handler class for this Request
|
|
42
|
+
#
|
|
43
|
+
# @raise [RuntimeError] if no implicit handler is defined
|
|
44
|
+
#
|
|
45
|
+
# @example Create an instance and call #handle on it to test it
|
|
46
|
+
# handler = MyRequest.create_implicit_handler
|
|
47
|
+
# result = handler.handle(MyRequest.new)
|
|
48
|
+
def self.create_implicit_handler
|
|
49
|
+
raise "Implicit handler is not defined." unless implicit_handler_defined?
|
|
50
|
+
|
|
51
|
+
const_get(IMPLICIT_HANDLER_CLASS_NAME).new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.undefine_implicit_handler
|
|
55
|
+
return unless implicit_handler_defined?
|
|
56
|
+
|
|
57
|
+
remove_const(IMPLICIT_HANDLER_CLASS_NAME)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.define_handler(proc)
|
|
61
|
+
Class.new(RequestHandler) do
|
|
62
|
+
@@handle_proc = proc # rubocop:disable Style/ClassVars
|
|
63
|
+
def handle(request)
|
|
64
|
+
@@handle_proc.call(request)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.implicit_handler_defined?
|
|
70
|
+
const_defined?(IMPLICIT_HANDLER_CLASS_NAME)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private_constant :IMPLICIT_HANDLER_CLASS_NAME
|
|
74
|
+
private_class_method :define_handler, :implicit_handler_defined?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mediate
|
|
4
|
+
#
|
|
5
|
+
# @abstract override {#handle} to implement.
|
|
6
|
+
#
|
|
7
|
+
# Abstract base class of a handler of requests. Each request type should have only one
|
|
8
|
+
# handler.
|
|
9
|
+
#
|
|
10
|
+
class RequestHandler
|
|
11
|
+
#
|
|
12
|
+
# Registers this handler for the given request Class.
|
|
13
|
+
# The request Class must have Mediate::Request as a superclass.
|
|
14
|
+
#
|
|
15
|
+
# @param [Class] request_class the Class of the requests that get passed to this handler
|
|
16
|
+
# @param [Mediate::Mediator] mediator the mediator instance to register the handler with
|
|
17
|
+
#
|
|
18
|
+
# @raise [ArgumentError] if request_class does not inherit from Mediate::Request
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
#
|
|
22
|
+
def self.handles(request_class, mediator = Mediate.mediator)
|
|
23
|
+
mediator.register_request_handler(self, request_class)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# The method to implement that handles the request of the type registered
|
|
28
|
+
# for this handler. Whatever this method returns will be returned to the sender of the request.
|
|
29
|
+
#
|
|
30
|
+
# @abstract
|
|
31
|
+
#
|
|
32
|
+
# @param [Mediate::Request] _request
|
|
33
|
+
#
|
|
34
|
+
# @return the result of handling the request
|
|
35
|
+
#
|
|
36
|
+
def handle(_request)
|
|
37
|
+
raise NoMethodError, "handle must be implemented"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/mediate.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mediate/version"
|
|
4
|
+
require_relative "mediate/mediator"
|
|
5
|
+
require "singleton"
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# Namespace containing a simple implementation of the mediator pattern.
|
|
9
|
+
#
|
|
10
|
+
module Mediate
|
|
11
|
+
#
|
|
12
|
+
# Get the current mediator instance. This will be a singleton
|
|
13
|
+
# throughout the lifetime of the program.
|
|
14
|
+
#
|
|
15
|
+
# @return [Mediate::Mediator] the current mediator instance
|
|
16
|
+
#
|
|
17
|
+
def self.mediator
|
|
18
|
+
Mediator.instance
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Sends a request to the registered handler, passing to any applicable pipeline behaviors.
|
|
23
|
+
#
|
|
24
|
+
# @param [Mediate::Request] request
|
|
25
|
+
#
|
|
26
|
+
# @return the response returned from the Mediate::RequestHandler
|
|
27
|
+
#
|
|
28
|
+
# @raise [ArgumentError] if request is nil
|
|
29
|
+
# @raise [Mediate::Errors::NoHandlerError] if no handlers have been registered for the given request's type
|
|
30
|
+
#
|
|
31
|
+
def self.dispatch(request)
|
|
32
|
+
mediator.dispatch(request)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# Sends a notification to all register handlers for the given notification's type.
|
|
37
|
+
#
|
|
38
|
+
# @param [Mediate::Notification] notification
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
#
|
|
42
|
+
def self.publish(notification)
|
|
43
|
+
mediator.publish(notification)
|
|
44
|
+
end
|
|
45
|
+
end
|
data/mediate.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/mediate/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "mediate"
|
|
7
|
+
spec.version = Mediate::VERSION
|
|
8
|
+
spec.authors = ["Ryan Ferguson"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "Simple mediator implementation"
|
|
11
|
+
spec.homepage = "https://github.com/rferg/mediate"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/rferg/mediate"
|
|
17
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
|
+
|
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
spec.bindir = "exe"
|
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "concurrent-ruby", "~> 1.1"
|
|
31
|
+
|
|
32
|
+
# For more information and examples about making a new gem, check out our
|
|
33
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
34
|
+
end
|
data/sig/mediate.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mediate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ryan Ferguson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2022-08-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: concurrent-ruby
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.1'
|
|
27
|
+
description:
|
|
28
|
+
email:
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- ".rspec"
|
|
34
|
+
- ".rubocop.yml"
|
|
35
|
+
- ".vscode/extensions.json"
|
|
36
|
+
- Gemfile
|
|
37
|
+
- Gemfile.lock
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- Rakefile
|
|
41
|
+
- lib/mediate.rb
|
|
42
|
+
- lib/mediate/error_handler.rb
|
|
43
|
+
- lib/mediate/error_handler_state.rb
|
|
44
|
+
- lib/mediate/errors/no_handler_error.rb
|
|
45
|
+
- lib/mediate/errors/request_handler_already_exists_error.rb
|
|
46
|
+
- lib/mediate/mediator.rb
|
|
47
|
+
- lib/mediate/notification.rb
|
|
48
|
+
- lib/mediate/notification_handler.rb
|
|
49
|
+
- lib/mediate/postrequest_behavior.rb
|
|
50
|
+
- lib/mediate/prerequest_behavior.rb
|
|
51
|
+
- lib/mediate/request.rb
|
|
52
|
+
- lib/mediate/request_handler.rb
|
|
53
|
+
- lib/mediate/version.rb
|
|
54
|
+
- mediate.gemspec
|
|
55
|
+
- sig/mediate.rbs
|
|
56
|
+
homepage: https://github.com/rferg/mediate
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://github.com/rferg/mediate
|
|
61
|
+
source_code_uri: https://github.com/rferg/mediate
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 2.6.0
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.3.7
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Simple mediator implementation
|
|
82
|
+
test_files: []
|