substation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +37 -0
- data/.rspec +4 -0
- data/.rvmrc +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +8 -0
- data/Gemfile.devtools +60 -0
- data/Guardfile +18 -0
- data/LICENSE +20 -0
- data/README.md +322 -0
- data/Rakefile +6 -0
- data/TODO +0 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +103 -0
- data/config/yardstick.yml +2 -0
- data/lib/substation/dispatcher.rb +262 -0
- data/lib/substation/observer.rb +66 -0
- data/lib/substation/request.rb +69 -0
- data/lib/substation/response.rb +178 -0
- data/lib/substation/support/utils.rb +68 -0
- data/lib/substation/version.rb +4 -0
- data/lib/substation.rb +40 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/unit/substation/dispatcher/action/call_spec.rb +23 -0
- data/spec/unit/substation/dispatcher/action/class_methods/coerce_spec.rb +46 -0
- data/spec/unit/substation/dispatcher/action_names_spec.rb +13 -0
- data/spec/unit/substation/dispatcher/call_spec.rb +47 -0
- data/spec/unit/substation/dispatcher/class_methods/coerce_spec.rb +18 -0
- data/spec/unit/substation/observer/chain/call_spec.rb +26 -0
- data/spec/unit/substation/observer/class_methods/coerce_spec.rb +33 -0
- data/spec/unit/substation/observer/null/call_spec.rb +12 -0
- data/spec/unit/substation/request/env_spec.rb +14 -0
- data/spec/unit/substation/request/error_spec.rb +15 -0
- data/spec/unit/substation/request/input_spec.rb +14 -0
- data/spec/unit/substation/request/success_spec.rb +15 -0
- data/spec/unit/substation/response/env_spec.rb +16 -0
- data/spec/unit/substation/response/failure/success_predicate_spec.rb +15 -0
- data/spec/unit/substation/response/input_spec.rb +16 -0
- data/spec/unit/substation/response/output_spec.rb +16 -0
- data/spec/unit/substation/response/success/success_predicate_spec.rb +15 -0
- data/spec/unit/substation/utils/class_methods/coerce_callable_spec.rb +34 -0
- data/spec/unit/substation/utils/class_methods/const_get_spec.rb +46 -0
- data/spec/unit/substation/utils/class_methods/symbolize_keys_spec.rb +20 -0
- data/substation.gemspec +25 -0
- metadata +177 -0
@@ -0,0 +1,262 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# Encapsulates all registered actions and their observers
|
4
|
+
#
|
5
|
+
# The only protocol actions must support is +#call(request)+.
|
6
|
+
# Actions are intended to be classes that handle one specific
|
7
|
+
# application use case.
|
8
|
+
class Dispatcher
|
9
|
+
|
10
|
+
# Encapsulates access to one registered action
|
11
|
+
class Action
|
12
|
+
|
13
|
+
# Raised when no action class name is configured
|
14
|
+
MissingHandlerError = Class.new(StandardError)
|
15
|
+
|
16
|
+
# Coerce the given +name+ and +config+ to an {Action} instance
|
17
|
+
#
|
18
|
+
# @param [Hash<Symbol, Object>] config
|
19
|
+
# the configuration hash
|
20
|
+
#
|
21
|
+
# @return [Action]
|
22
|
+
# the coerced instance
|
23
|
+
#
|
24
|
+
# @raise [MissingHandlerError]
|
25
|
+
# if no action handler is configured
|
26
|
+
#
|
27
|
+
# @raise [ArgumentError]
|
28
|
+
# if action or observer handlers are not coercible
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
def self.coerce(config)
|
32
|
+
handler = config.fetch(:action) { raise(MissingHandlerError) }
|
33
|
+
observer = Observer.coerce(config[:observer])
|
34
|
+
|
35
|
+
new(Utils.coerce_callable(handler), observer)
|
36
|
+
end
|
37
|
+
|
38
|
+
include Concord.new(:handler, :observer)
|
39
|
+
include Adamantium
|
40
|
+
|
41
|
+
# Call the action
|
42
|
+
#
|
43
|
+
# @param [Substation::Request] request
|
44
|
+
# the request passed to the registered action
|
45
|
+
#
|
46
|
+
# @return [Substation::Response]
|
47
|
+
# the response returned when calling the action
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
def call(request)
|
51
|
+
response = handler.call(request)
|
52
|
+
observer.call(response)
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
end # class Action
|
57
|
+
|
58
|
+
# Raised when trying to dispatch to an unregistered action
|
59
|
+
UnknownActionError = Class.new(StandardError)
|
60
|
+
|
61
|
+
# Coerce the given +config+ to a {Dispatcher} instance
|
62
|
+
#
|
63
|
+
# @example without observers
|
64
|
+
#
|
65
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
66
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
67
|
+
# })
|
68
|
+
#
|
69
|
+
# @example with a single observer
|
70
|
+
#
|
71
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
72
|
+
# 'some_use_case' => {
|
73
|
+
# 'action' => 'SomeUseCase',
|
74
|
+
# 'observer' => 'SomeObserver'
|
75
|
+
# }
|
76
|
+
# })
|
77
|
+
#
|
78
|
+
# @example with multiple observers
|
79
|
+
#
|
80
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
81
|
+
# 'some_use_case' => {
|
82
|
+
# 'action' => 'SomeUseCase',
|
83
|
+
# 'observer' => [
|
84
|
+
# 'SomeObserver',
|
85
|
+
# 'AnotherObserver'
|
86
|
+
# ]
|
87
|
+
# }
|
88
|
+
# })
|
89
|
+
#
|
90
|
+
# @example with Symbol keys and const handlers
|
91
|
+
#
|
92
|
+
# module App
|
93
|
+
# class SomeUseCase
|
94
|
+
# def self.call(request)
|
95
|
+
# data = perform_work
|
96
|
+
# request.success(data)
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# class SomeObserver
|
101
|
+
# def self.call(response)
|
102
|
+
# # do something
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
108
|
+
# :some_use_case => {
|
109
|
+
# :action => App::SomeUseCase,
|
110
|
+
# :observer => App::SomeObserver
|
111
|
+
# }
|
112
|
+
# })
|
113
|
+
#
|
114
|
+
# @example with Symbol keys and proc handlers
|
115
|
+
#
|
116
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
117
|
+
# :some_use_case => {
|
118
|
+
# :action => Proc.new { |request| request.success(:foo) },
|
119
|
+
# :observer => Proc.new { |response| do_something }
|
120
|
+
# }
|
121
|
+
# })
|
122
|
+
#
|
123
|
+
# @param [Hash<#to_sym, Object>] config
|
124
|
+
# the action configuration
|
125
|
+
#
|
126
|
+
# @return [Dispatcher]
|
127
|
+
# the coerced instance
|
128
|
+
#
|
129
|
+
# @raise [Action::MissingHandlerError]
|
130
|
+
# if no action handler is configured
|
131
|
+
#
|
132
|
+
# @raise [ArgumentError]
|
133
|
+
# if action or observer handlers are not coercible
|
134
|
+
#
|
135
|
+
# @api public
|
136
|
+
def self.coerce(config)
|
137
|
+
new(normalize_config(config))
|
138
|
+
end
|
139
|
+
|
140
|
+
# Normalize the given +config+
|
141
|
+
#
|
142
|
+
# @param [Hash<#to_sym, Object>] config
|
143
|
+
# the action configuration
|
144
|
+
#
|
145
|
+
# @return [Hash<Symbol, Action>]
|
146
|
+
# the normalized config hash
|
147
|
+
#
|
148
|
+
# @raise [Action::MissingHandlerError]
|
149
|
+
# if no action handler is configured
|
150
|
+
#
|
151
|
+
# @raise [ArgumentError]
|
152
|
+
# if action or observer handlers are not coercible
|
153
|
+
#
|
154
|
+
# @api private
|
155
|
+
def self.normalize_config(config)
|
156
|
+
Utils.symbolize_keys(config).each_with_object({}) { |(name, hash), actions|
|
157
|
+
actions[name] = Action.coerce(hash)
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
private_class_method :normalize_config
|
162
|
+
|
163
|
+
include Concord.new(:actions)
|
164
|
+
include Adamantium
|
165
|
+
|
166
|
+
# Invoke the action identified by +name+
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
#
|
170
|
+
# module App
|
171
|
+
# class Environment
|
172
|
+
# def initialize(dispatcher, logger)
|
173
|
+
# @dispatcher, @logger = dispatcher, logger
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# class SomeUseCase
|
178
|
+
# def self.call(request)
|
179
|
+
# data = perform_work
|
180
|
+
# request.success(data)
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
186
|
+
# :some_use_case => { :action => App::SomeUseCase }
|
187
|
+
# })
|
188
|
+
#
|
189
|
+
# env = App::Environment.new(dispatcher, Logger.new($stdout))
|
190
|
+
#
|
191
|
+
# response = dispatcher.call(:some_use_case, :some_input, env)
|
192
|
+
# response.success? # => true
|
193
|
+
#
|
194
|
+
# @param [Symbol] name
|
195
|
+
# a registered action name
|
196
|
+
#
|
197
|
+
# @param [Object] input
|
198
|
+
# the input model instance to pass to the action
|
199
|
+
#
|
200
|
+
# @param [Object] env
|
201
|
+
# the application environment
|
202
|
+
#
|
203
|
+
# @return [Response]
|
204
|
+
# the response returned when calling the action
|
205
|
+
#
|
206
|
+
# @raise [UnknownActionError]
|
207
|
+
# if no action is registered for +name+
|
208
|
+
#
|
209
|
+
# @api public
|
210
|
+
def call(name, input, env)
|
211
|
+
fetch(name).call(Request.new(env, input))
|
212
|
+
end
|
213
|
+
|
214
|
+
# The names of all registered actions
|
215
|
+
#
|
216
|
+
# @example
|
217
|
+
#
|
218
|
+
# module App
|
219
|
+
# class SomeUseCase
|
220
|
+
# def self.call(request)
|
221
|
+
# data = perform_work
|
222
|
+
# request.success(data)
|
223
|
+
# end
|
224
|
+
# end
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# dispatcher = Substation::Dispatcher.coerce({
|
228
|
+
# :some_use_case => { :action => App::SomeUseCase }
|
229
|
+
# })
|
230
|
+
#
|
231
|
+
# dispatcher.action_names # => #<Set: {:some_use_case}>
|
232
|
+
#
|
233
|
+
# @return [Set<Symbol>]
|
234
|
+
# the set of registered action names
|
235
|
+
#
|
236
|
+
# @api public
|
237
|
+
def action_names
|
238
|
+
Set.new(actions.keys)
|
239
|
+
end
|
240
|
+
|
241
|
+
memoize :action_names
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
# The action registered with +name+
|
246
|
+
#
|
247
|
+
# @param [Symbol] name
|
248
|
+
# a name for which an action is registered
|
249
|
+
#
|
250
|
+
# @return [Action]
|
251
|
+
# the action configuration registered for +name+
|
252
|
+
#
|
253
|
+
# @raise [KeyError]
|
254
|
+
# if no action is registered with +name+
|
255
|
+
#
|
256
|
+
# @api private
|
257
|
+
def fetch(name)
|
258
|
+
actions.fetch(name) { raise(UnknownActionError) }
|
259
|
+
end
|
260
|
+
|
261
|
+
end # class Dispatcher
|
262
|
+
end # module Substation
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# Abstract observer base class
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class Observer
|
7
|
+
|
8
|
+
include AbstractType
|
9
|
+
include Adamantium::Flat
|
10
|
+
|
11
|
+
# Notify the observer
|
12
|
+
#
|
13
|
+
# @param [Response] response
|
14
|
+
# the response returned when calling the observed action
|
15
|
+
#
|
16
|
+
# @return [self]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
abstract_method :call
|
20
|
+
|
21
|
+
# Coerce +input+ to an instance of {Observer}
|
22
|
+
#
|
23
|
+
# @param [NilClass, String, Array<String>] input
|
24
|
+
# 0..n observer class names
|
25
|
+
#
|
26
|
+
# @return [Observer::NULL, Object, Observer::Chain]
|
27
|
+
# a null observer, an observer object, or a chain of observers
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def self.coerce(input)
|
31
|
+
case input
|
32
|
+
when NilClass
|
33
|
+
NULL
|
34
|
+
when Array
|
35
|
+
Chain.new(input.map { |item| coerce(item) })
|
36
|
+
else
|
37
|
+
Utils.coerce_callable(input)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Null observer
|
42
|
+
NULL = Class.new(self) { def call(_response); self; end; }.new.freeze
|
43
|
+
|
44
|
+
# Chain of observers
|
45
|
+
class Chain < self
|
46
|
+
|
47
|
+
include Concord.new(:observers)
|
48
|
+
|
49
|
+
# Notify the observer
|
50
|
+
#
|
51
|
+
# @param [Response] response
|
52
|
+
# the response returned when calling the observed action
|
53
|
+
#
|
54
|
+
# @return [self]
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def call(response)
|
58
|
+
observers.each do |observer|
|
59
|
+
observer.call(response)
|
60
|
+
end
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
end # Chain
|
65
|
+
end # Observer
|
66
|
+
end # Substation
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# Encapsulates the application environment and an input model instance
|
4
|
+
class Request
|
5
|
+
|
6
|
+
include Concord.new(:env, :input)
|
7
|
+
include Adamantium
|
8
|
+
|
9
|
+
# Create a new successful response
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# class SomeUseCase
|
14
|
+
# def self.call(request)
|
15
|
+
# data = perform_use_case
|
16
|
+
# request.success(data)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @param [Object] output
|
21
|
+
# the data associated with the response
|
22
|
+
#
|
23
|
+
# @return [Response::Success]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def success(output)
|
27
|
+
respond_with(Response::Success, output)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a new failure response
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
#
|
34
|
+
# class SomeUseCase
|
35
|
+
# def self.call(request)
|
36
|
+
# error = perform_use_case
|
37
|
+
# request.error(error)
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @param [Object] output
|
42
|
+
# the data associated with the response
|
43
|
+
#
|
44
|
+
# @return [Response::Failure]
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def error(output)
|
48
|
+
respond_with(Response::Failure, output)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Instantiate an instance of +klass+ and pass +output+
|
54
|
+
#
|
55
|
+
# @param [Response::Success, Response::Failure] klass
|
56
|
+
# the response class
|
57
|
+
#
|
58
|
+
# @param [Object] output
|
59
|
+
# the data associated with the response
|
60
|
+
#
|
61
|
+
# @return [Response::Success, Response::Failure]
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
def respond_with(klass, output)
|
65
|
+
klass.new(self, output)
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class Request
|
69
|
+
end # module Substation
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# Base class for action responses
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class Response
|
7
|
+
|
8
|
+
include AbstractType
|
9
|
+
include Equalizer.new(:request, :output)
|
10
|
+
include Adamantium
|
11
|
+
|
12
|
+
# The environment used to return this response
|
13
|
+
#
|
14
|
+
# @return [Environment]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :env
|
18
|
+
|
19
|
+
# The request model instance passed into an action
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
#
|
23
|
+
# class SomeUseCase
|
24
|
+
# def self.call(request)
|
25
|
+
# data = perform_work
|
26
|
+
# request.success(data)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# env = Substation::Environment.coerce({
|
31
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
32
|
+
# })
|
33
|
+
#
|
34
|
+
# response = env.dispatch(:some_use_case, :input)
|
35
|
+
# response.input # => :input
|
36
|
+
#
|
37
|
+
# @see Request#input
|
38
|
+
#
|
39
|
+
# @return [Object]
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
attr_reader :input
|
43
|
+
|
44
|
+
# The data wrapped inside an action {Response}
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
#
|
48
|
+
# class SomeUseCase
|
49
|
+
# def self.call(request)
|
50
|
+
# request.success(:output)
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# env = Substation::Environment.coerce({
|
55
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
56
|
+
# })
|
57
|
+
#
|
58
|
+
# response = env.dispatch(:some_use_case, :input)
|
59
|
+
# response.output # => :output
|
60
|
+
#
|
61
|
+
# @return [Object]
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
attr_reader :output
|
65
|
+
|
66
|
+
# Initialize a new instance
|
67
|
+
#
|
68
|
+
# @param [Request] request
|
69
|
+
# the request passed to the action that returned this response
|
70
|
+
#
|
71
|
+
# @param [Object] output
|
72
|
+
# the data returned from the action that returned this response
|
73
|
+
#
|
74
|
+
# @return [undefined]
|
75
|
+
#
|
76
|
+
# @api private
|
77
|
+
def initialize(request, output)
|
78
|
+
@request = request
|
79
|
+
@env = @request.env
|
80
|
+
@input = @request.input
|
81
|
+
@output = output
|
82
|
+
end
|
83
|
+
|
84
|
+
# Indicates wether this is a successful response or not
|
85
|
+
#
|
86
|
+
# @abstract
|
87
|
+
#
|
88
|
+
# @see Success#success?
|
89
|
+
# @see Failure#success?
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
#
|
93
|
+
# class SomeUseCase
|
94
|
+
# def self.call(request)
|
95
|
+
# request.success(:data)
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# env = Substation::Environment.coerce({
|
100
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
101
|
+
# })
|
102
|
+
#
|
103
|
+
# response = env.dispatch(:some_use_case, :input)
|
104
|
+
# response.class # Substation::Response::Success
|
105
|
+
# response.success? # => true
|
106
|
+
#
|
107
|
+
# @return [Boolean]
|
108
|
+
# true if successful, false otherwise
|
109
|
+
#
|
110
|
+
# @api public
|
111
|
+
abstract_method :success?
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# The request that lead to this response
|
116
|
+
#
|
117
|
+
# @return [Request]
|
118
|
+
#
|
119
|
+
# @api private
|
120
|
+
attr_reader :request
|
121
|
+
|
122
|
+
# An errorneous {Response}
|
123
|
+
class Failure < self
|
124
|
+
|
125
|
+
# Tests wether this response was successful
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
#
|
129
|
+
# class SomeUseCase
|
130
|
+
# def self.call(request)
|
131
|
+
# request.error(:output)
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# env = Substation::Environment.coerce({
|
136
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
137
|
+
# })
|
138
|
+
#
|
139
|
+
# response = env.dispatch(:some_use_case, :input)
|
140
|
+
# response.success? # => false
|
141
|
+
#
|
142
|
+
# @return [false]
|
143
|
+
#
|
144
|
+
# @api public
|
145
|
+
def success?
|
146
|
+
false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# A successful {Response}
|
151
|
+
class Success < self
|
152
|
+
|
153
|
+
# Tests wether this response was successful
|
154
|
+
#
|
155
|
+
# @example
|
156
|
+
#
|
157
|
+
# class SomeUseCase
|
158
|
+
# def self.call(request)
|
159
|
+
# request.success(:data)
|
160
|
+
# end
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# env = Substation::Environment.coerce({
|
164
|
+
# 'some_use_case' => { 'action' => 'SomeUseCase' }
|
165
|
+
# })
|
166
|
+
#
|
167
|
+
# response = env.dispatch(:some_use_case, :input)
|
168
|
+
# response.success? # => true
|
169
|
+
#
|
170
|
+
# @return [true]
|
171
|
+
#
|
172
|
+
# @api public
|
173
|
+
def success?
|
174
|
+
true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Substation
|
2
|
+
|
3
|
+
# A collection of utility methods
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
# Get the constant for the given FQN
|
7
|
+
#
|
8
|
+
# @param [#to_s] name
|
9
|
+
# the FQN denoting a constant
|
10
|
+
#
|
11
|
+
# @return [Class, nil]
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
def self.const_get(name)
|
15
|
+
list = name.to_s.split("::")
|
16
|
+
list.shift if list.first.empty?
|
17
|
+
obj = Object
|
18
|
+
list.each do |const|
|
19
|
+
# This is required because const_get tries to look for constants in the
|
20
|
+
# ancestor chain, but we only want constants that are HERE
|
21
|
+
obj =
|
22
|
+
if obj.const_defined?(const)
|
23
|
+
obj.const_get(const)
|
24
|
+
else
|
25
|
+
obj.const_missing(const)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
obj
|
29
|
+
end
|
30
|
+
|
31
|
+
# Converts string keys into symbol keys
|
32
|
+
#
|
33
|
+
# @param [Hash<#to_sym, Object>] hash
|
34
|
+
# a hash with keys that respond to `#to_sym`
|
35
|
+
#
|
36
|
+
# @return [Hash<Symbol, Object>]
|
37
|
+
# a hash with symbol keys
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
def self.symbolize_keys(hash)
|
41
|
+
hash.each_with_object({}) { |(key, value), normalized_hash|
|
42
|
+
normalized_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
43
|
+
normalized_hash[key.to_sym] = normalized_value
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Coerce the given +handler+ object
|
48
|
+
#
|
49
|
+
# @param [Symbol, String, Proc] handler
|
50
|
+
# a name denoting a const that responds to `#call(object)`, or a proc
|
51
|
+
#
|
52
|
+
# @return [Class, Proc]
|
53
|
+
# the callable action handler
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
def self.coerce_callable(handler)
|
57
|
+
case handler
|
58
|
+
when Symbol, String
|
59
|
+
Utils.const_get(handler)
|
60
|
+
when Proc
|
61
|
+
handler
|
62
|
+
else
|
63
|
+
raise(ArgumentError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end # module Utils
|
68
|
+
end # module Substation
|
data/lib/substation.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'adamantium'
|
5
|
+
require 'equalizer'
|
6
|
+
require 'abstract_type'
|
7
|
+
require 'concord'
|
8
|
+
|
9
|
+
# Substation can be thought of as a domain level request router. It assumes
|
10
|
+
# that every usecase in your application has a name and is implemented in a
|
11
|
+
# dedicated class that will be referred to as an *action* in the context of
|
12
|
+
# substation. The only protocol such actions must support is `#call(request)`.
|
13
|
+
#
|
14
|
+
# The contract for actions specifies that when invoked, actions can
|
15
|
+
# receive arbitrary input data which will be available in `request.input`.
|
16
|
+
# Additionally, `request.env` contains an arbitrary object that
|
17
|
+
# represents your application environment and will typically provide access
|
18
|
+
# to useful things like a logger or a storage engine abstraction.
|
19
|
+
#
|
20
|
+
# The contract further specifies that every action must return an instance
|
21
|
+
# of either `Substation::Response::Success` or `Substation::Response::Failure`.
|
22
|
+
# Again, arbitrary data can be associated with any kind of response, and will
|
23
|
+
# be available in `response.data`. In addition to that, `response.success?` is
|
24
|
+
# available and will indicate wether invoking the action was successful or not.
|
25
|
+
#
|
26
|
+
# `Substation::Dispatcher` stores a mapping of action names to the actual
|
27
|
+
# objects implementing the action. Clients can use
|
28
|
+
# `Substation::Dispatcher#call(name, input, env)` to dispatch to any
|
29
|
+
# registered action. For example, a web application could map an http
|
30
|
+
# route to a specific action name and pass relevant http params on to the
|
31
|
+
# action.
|
32
|
+
|
33
|
+
module Substation
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'substation/request'
|
37
|
+
require 'substation/response'
|
38
|
+
require 'substation/observer'
|
39
|
+
require 'substation/dispatcher'
|
40
|
+
require 'substation/support/utils'
|