substation 0.0.1
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.
- 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'
|