substation 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +37 -0
  2. data/.rspec +4 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +16 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.devtools +60 -0
  7. data/Guardfile +18 -0
  8. data/LICENSE +20 -0
  9. data/README.md +322 -0
  10. data/Rakefile +6 -0
  11. data/TODO +0 -0
  12. data/config/devtools.yml +2 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +3 -0
  16. data/config/reek.yml +103 -0
  17. data/config/yardstick.yml +2 -0
  18. data/lib/substation/dispatcher.rb +262 -0
  19. data/lib/substation/observer.rb +66 -0
  20. data/lib/substation/request.rb +69 -0
  21. data/lib/substation/response.rb +178 -0
  22. data/lib/substation/support/utils.rb +68 -0
  23. data/lib/substation/version.rb +4 -0
  24. data/lib/substation.rb +40 -0
  25. data/spec/spec_helper.rb +34 -0
  26. data/spec/unit/substation/dispatcher/action/call_spec.rb +23 -0
  27. data/spec/unit/substation/dispatcher/action/class_methods/coerce_spec.rb +46 -0
  28. data/spec/unit/substation/dispatcher/action_names_spec.rb +13 -0
  29. data/spec/unit/substation/dispatcher/call_spec.rb +47 -0
  30. data/spec/unit/substation/dispatcher/class_methods/coerce_spec.rb +18 -0
  31. data/spec/unit/substation/observer/chain/call_spec.rb +26 -0
  32. data/spec/unit/substation/observer/class_methods/coerce_spec.rb +33 -0
  33. data/spec/unit/substation/observer/null/call_spec.rb +12 -0
  34. data/spec/unit/substation/request/env_spec.rb +14 -0
  35. data/spec/unit/substation/request/error_spec.rb +15 -0
  36. data/spec/unit/substation/request/input_spec.rb +14 -0
  37. data/spec/unit/substation/request/success_spec.rb +15 -0
  38. data/spec/unit/substation/response/env_spec.rb +16 -0
  39. data/spec/unit/substation/response/failure/success_predicate_spec.rb +15 -0
  40. data/spec/unit/substation/response/input_spec.rb +16 -0
  41. data/spec/unit/substation/response/output_spec.rb +16 -0
  42. data/spec/unit/substation/response/success/success_predicate_spec.rb +15 -0
  43. data/spec/unit/substation/utils/class_methods/coerce_callable_spec.rb +34 -0
  44. data/spec/unit/substation/utils/class_methods/const_get_spec.rb +46 -0
  45. data/spec/unit/substation/utils/class_methods/symbolize_keys_spec.rb +20 -0
  46. data/substation.gemspec +25 -0
  47. metadata +177 -0
data/.gitignore ADDED
@@ -0,0 +1,37 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## Rubinius
17
+ *.rbc
18
+ .rbx
19
+
20
+ ## PROJECT::GENERAL
21
+ *.gem
22
+ coverage
23
+ profiling
24
+ turbulence
25
+ rdoc
26
+ pkg
27
+ tmp
28
+ doc
29
+ log
30
+ .yardoc
31
+ measurements
32
+
33
+ ## BUNDLER
34
+ .bundle
35
+ Gemfile.lock
36
+
37
+ ## PROJECT::SPECIFIC
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --profile
3
+ --order random
4
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use @$(basename `pwd`) --create
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ before_install: gem install bundler
3
+ bundler_args: --without yard guard benchmarks
4
+ script: "bundle exec rake ci"
5
+ rvm:
6
+ - 1.9.3
7
+ - 1.9.2
8
+ - 1.8.7
9
+ - ree
10
+ - ruby-head
11
+ - 2.0.0
12
+ - jruby-19mode
13
+ - jruby-18mode
14
+ - jruby-head
15
+ - rbx-19mode
16
+ - rbx-18mode
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'devtools', :git => 'https://github.com/datamapper/devtools.git'
7
+ eval File.read('Gemfile.devtools')
8
+ end
data/Gemfile.devtools ADDED
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ group :development do
4
+ gem 'rake', '~> 10.0.4'
5
+ gem 'rspec', '~> 2.13.0'
6
+ gem 'yard', '~> 0.8.6.1'
7
+ end
8
+
9
+ group :yard do
10
+ gem 'kramdown', '~> 1.0.1'
11
+ end
12
+
13
+ group :guard do
14
+ gem 'guard', '~> 1.8.0'
15
+ gem 'guard-bundler', '~> 1.0.0'
16
+ gem 'guard-rspec', '~> 2.5.4'
17
+
18
+ # file system change event handling
19
+ gem 'listen', '~> 1.0.2'
20
+ gem 'rb-fchange', '~> 0.0.6', :require => false
21
+ gem 'rb-fsevent', '~> 0.9.3', :require => false
22
+ gem 'rb-inotify', '~> 0.9.0', :require => false
23
+
24
+ # notification handling
25
+ gem 'libnotify', '~> 0.8.0', :require => false
26
+ gem 'rb-notifu', '~> 0.0.4', :require => false
27
+ gem 'terminal-notifier-guard', '~> 1.5.3', :require => false
28
+ end
29
+
30
+ group :metrics do
31
+ gem 'backports', '~> 3.3', '>= 3.3.0'
32
+ gem 'coveralls', '~> 0.6.6'
33
+ gem 'flay', '~> 2.2.0'
34
+ gem 'flog', '~> 4.0.0'
35
+ gem 'reek', '~> 1.3.1', :git => 'https://github.com/troessner/reek.git'
36
+ gem 'simplecov', '~> 0.7.1'
37
+ gem 'yardstick', '~> 0.9.6'
38
+
39
+ platforms :ruby_19 do
40
+ gem 'yard-spellcheck', '~> 0.1.5'
41
+ end
42
+
43
+ platforms :mri_19, :rbx do
44
+ gem 'mutant', '~> 0.2.20'
45
+ end
46
+
47
+ platforms :rbx do
48
+ gem 'pelusa', '~> 0.2.2'
49
+ end
50
+ end
51
+
52
+ group :benchmarks do
53
+ gem 'rbench', '~> 0.2.3'
54
+ end
55
+
56
+ platform :jruby do
57
+ group :jruby do
58
+ gem 'jruby-openssl', '~> 0.8.5'
59
+ end
60
+ end
data/Guardfile ADDED
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ guard :bundler do
4
+ watch('Gemfile')
5
+ end
6
+
7
+ guard :rspec, :all_on_start => false, :all_after_pass => false do
8
+ # run all specs if the spec_helper or supporting files files are modified
9
+ watch('spec/spec_helper.rb') { 'spec/unit' }
10
+ watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec/unit' }
11
+
12
+ # run unit specs if associated lib code is modified
13
+ watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}"] }
14
+ watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec/unit' }
15
+
16
+ # run a spec if it is modified
17
+ watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z})
18
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Martin Gamsjaeger (snusnu)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # substation
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/substation.png)][gem]
4
+ [![Build Status](https://secure.travis-ci.org/snusnu/substation.png?branch=master)][travis]
5
+ [![Dependency Status](https://gemnasium.com/snusnu/substation.png)][gemnasium]
6
+ [![Code Climate](https://codeclimate.com/github/snusnu/substation.png)][codeclimate]
7
+
8
+ [gem]: https://rubygems.org/gems/substation
9
+ [travis]: https://travis-ci.org/snusnu/substation
10
+ [gemnasium]: https://gemnasium.com/snusnu/substation
11
+ [codeclimate]: https://codeclimate.com/github/snusnu/substation
12
+
13
+ `substation` can be thought of as a domain level request router. It assumes
14
+ that every usecase in your application has a name and is implemented in a dedicated
15
+ class that will be referred to as an *action* for the purposes of this
16
+ document. The only protocol such actions must support is `#call(request)`.
17
+
18
+ The contract for actions specifies that when invoked, actions can
19
+ receive arbitrary input data which will be available in `request.input`.
20
+ Additionally, `request.env` contains an arbitrary object that
21
+ represents your application environment and will typically provide access
22
+ to useful things like a logger or a storage engine abstraction.
23
+
24
+ The contract further specifies that every action must return an instance
25
+ of either `Substation::Response::Success` or
26
+ `Substation::Response::Failure`. Again, arbitrary data can be associated
27
+ with any kind of response, and will be available in `response.data`. In
28
+ addition to that, `response.success?` is available and will indicate
29
+ wether invoking the action was successful or not.
30
+
31
+ `Substation::Dispatcher` stores a mapping of action names to the actual
32
+ objects implementing the action. Clients can use
33
+ `Substation::Dispatcher#call(name, input, env)` to dispatch to any
34
+ registered action. For example, a web application could map an http
35
+ route to a specific action name and pass relevant http params on to the
36
+ action.
37
+
38
+ ## Actions
39
+
40
+ Here's an example of a valid action.
41
+
42
+ ```ruby
43
+ module App
44
+ class SomeUseCase
45
+
46
+ # Perform the usecase
47
+ #
48
+ # @param [Substation::Request] request
49
+ # the request passed to the registered action
50
+ #
51
+ # @return [Substation::Response]
52
+ # the response returned when calling the action
53
+ #
54
+ # @api private
55
+ def self.call(request)
56
+ data = perform_work
57
+ if data
58
+ request.success(data)
59
+ else
60
+ request.error("Something went wrong")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ ```
66
+
67
+ It is up to you how to implement the action. Another way of writing an
68
+ action could involve providing an application specific baseclass for all
69
+ your actions, which provides access to methods you frequently use within
70
+ any specific action.
71
+
72
+ ```ruby
73
+ module App
74
+
75
+ # Base class for all actions
76
+ #
77
+ # @abstract
78
+ class Action
79
+
80
+ # Perform the usecase
81
+ #
82
+ # @param [Substation::Request] request
83
+ # the request passed to the registered action
84
+ #
85
+ # @return [Substation::Response]
86
+ # the response returned when calling the action
87
+ #
88
+ # @api private
89
+ def self.call(request)
90
+ new(request).call
91
+ end
92
+
93
+ def initialize(request)
94
+ @request = request
95
+ @env = @request.env
96
+ end
97
+
98
+ def call
99
+ raise NotImplementedError, "#{self.class}##{__method__} must be implemented"
100
+ end
101
+
102
+ private
103
+
104
+ def success(data)
105
+ @request.success(data)
106
+ end
107
+
108
+ def error(data)
109
+ @request.error(data)
110
+ end
111
+ end
112
+
113
+ class SomeUseCase < Action
114
+
115
+ def call
116
+ data = perform_work
117
+ if data
118
+ success(data)
119
+ else
120
+ error("Something went wrong")
121
+ end
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Observers
128
+
129
+ Sometimes, additional code needs to run wether your action was
130
+ successful or not. Observers provide you with a place for that code.
131
+ Again, the contract for observers is very simple: all they need to
132
+ implement is `call(response)` and `substation` will make sure that the
133
+ `response` param will be the response returned from invoking your
134
+ action.
135
+
136
+ It is therefore possible to dispatch to different observers based on
137
+ wether the action was successful or not by utilizing
138
+ `response.success?`. By accepting a `response` object, observers also
139
+ have access to the original `input` and `env` the action was invoked
140
+ with, as well as the `output` that the action produced. These objects
141
+ are made available via `response.input`, `response.env` and
142
+ `response.output`.
143
+
144
+ Here's an example of a simple observer:
145
+
146
+ ```ruby
147
+ module App
148
+ class SomeUseCaseObserver
149
+ def self.call(response)
150
+ # your code here
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ A more involved observer could dispatch based on the success of the
157
+ invoked action:
158
+
159
+ ```ruby
160
+ module App
161
+ class SomeUseCaseObserver
162
+ def self.call(response)
163
+ klass = response.success? ? Success : Failure
164
+ klass.new(response).call
165
+ end
166
+
167
+ def initialize(response)
168
+ @response = response
169
+ end
170
+
171
+ class Success < self
172
+ def call
173
+ # your code here
174
+ end
175
+ end
176
+
177
+ class Failure < self
178
+ def call
179
+ # your code here
180
+ end
181
+ end
182
+ end
183
+ end
184
+ ```
185
+
186
+ ## Configuration
187
+
188
+ Since an application will most likely involve more than one usecase, we
189
+ need a way to inform `substation` about all the usecases it should handle.
190
+ For this purpose, we can instantiate a `Substation::Dispatcher` and hand
191
+ it a configuration hash that describes the various actions by giving
192
+ them a name, a class that's responsible for implementing the actual
193
+ usecase, and a list of `0..n` observers that should be invoked depending
194
+ on the action response.
195
+
196
+ An example configuration for an action without any observers:
197
+
198
+ ```ruby
199
+ dispatcher = Substation::Dispatcher.coerce({
200
+ 'some_use_case' => { 'action' => 'App::SomeUseCase' }
201
+ })
202
+ ```
203
+
204
+ An example configuration for an action with one observer:
205
+
206
+ ```ruby
207
+ dispatcher = Substation::Dispatcher.coerce({
208
+ 'some_use_case' => {
209
+ 'action' => 'App::SomeUseCase',
210
+ 'observer' => 'App::SomeUseCaseObserver'
211
+ }
212
+ })
213
+ ```
214
+
215
+ An example configuration for an action with multiple observers:
216
+
217
+ ```ruby
218
+ dispatcher = Substation::Dispatcher.coerce({
219
+ 'some_use_case' => {
220
+ 'action' => 'App::SomeUseCase',
221
+ 'observer' => [
222
+ 'App::SomeUseCaseObserver',
223
+ 'App::AnotherObserver'
224
+ ]
225
+ }
226
+ })
227
+ ```
228
+
229
+ The above configuration examples are tailored towards being read from a
230
+ (yaml) config file and therefore accept strings as keys and values. It's
231
+ also possible to use symbols as keys and values. Values correspond to
232
+ action or observer "handlers" and can also be given as either constants
233
+ or procs. In any case, handlers must respond to `call(object)`.
234
+
235
+ An example configuration using symbol keys and constants for handlers:
236
+
237
+ ```ruby
238
+ dispatcher = Substation::Dispatcher.coerce({
239
+ :some_use_case => {
240
+ :action => App::SomeUseCase,
241
+ :observer => App::SomeUseCaseObserver
242
+ }
243
+ })
244
+ ```
245
+
246
+ An example configuration using symbol keys and procs for handlers:
247
+
248
+ ```ruby
249
+ dispatcher = Substation::Dispatcher.coerce({
250
+ :some_use_case => {
251
+ :action => Proc.new { |request| request.success(:foo) },
252
+ :observer => Proc.new { |response| do_something }
253
+ }
254
+ })
255
+ ```
256
+
257
+
258
+ ## Application environments
259
+
260
+ In order to provide your actions with objects typically needed during
261
+ the course of performing a usecase (like a logger or a storage engine
262
+ abstraction), you can encapsulate these objects within an application
263
+ specific environment object, and send that along to every action.
264
+
265
+ Here's a simple example with an environment that encapsulates a logger,
266
+ an artificial storage abstraction object and the dispatcher itself.
267
+
268
+ The example builds on top of the application specific action baseclass
269
+ shown above:
270
+
271
+ ```ruby
272
+ module App
273
+ class Environment
274
+ attr_reader :storage
275
+ attr_reader :dispatcher
276
+ attr_reader :logger
277
+
278
+ def initialize(storage, dispatcher, logger)
279
+ @storage = storage
280
+ @dispatcher = dispatcher
281
+ @logger = logger
282
+ end
283
+ end
284
+
285
+ class Action
286
+ # ...
287
+ # code from above example
288
+ # ...
289
+
290
+ def db
291
+ @env.storage
292
+ end
293
+ end
294
+
295
+ class SomeUseCase < Action
296
+
297
+ def initialize(request)
298
+ super
299
+ @person = request.input
300
+ end
301
+
302
+ def call
303
+ if person = db.save_person(@person)
304
+ success(person)
305
+ else
306
+ error("Something went wrong")
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ dispatcher = Substation::Dispatcher.coerce({
313
+ 'some_use_case' => { 'action' => 'App::SomeUseCase' }
314
+ })
315
+
316
+ storage = App::Storage.new # some storage abstraction
317
+ env = App::Environment.new(storage, dispatcher, Logger.new($stdout))
318
+
319
+ # :some_input is no person, db.save_person will fail
320
+ response = dispatcher.call(:some_use_case, :some_input, env)
321
+ response.success? # => false
322
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rake'
4
+ require 'devtools'
5
+
6
+ Devtools.init_rake_tasks
data/TODO ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ ---
2
+ unit_test_timeout: 0.3
data/config/flay.yml ADDED
@@ -0,0 +1,3 @@
1
+ ---
2
+ threshold: 6
3
+ total_score: 51
data/config/flog.yml ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 10.9
data/config/mutant.yml ADDED
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: substation
3
+ namespace: Substation
data/config/reek.yml ADDED
@@ -0,0 +1,103 @@
1
+ ---
2
+ Attribute:
3
+ enabled: false
4
+ exclude: []
5
+ BooleanParameter:
6
+ enabled: true
7
+ exclude: []
8
+ ClassVariable:
9
+ enabled: true
10
+ exclude: []
11
+ ControlParameter:
12
+ enabled: true
13
+ exclude: []
14
+ DataClump:
15
+ enabled: true
16
+ exclude: []
17
+ max_copies: 2
18
+ min_clump_size: 2
19
+ DuplicateMethodCall:
20
+ enabled: true
21
+ exclude: []
22
+ max_calls: 1
23
+ allow_calls: []
24
+ FeatureEnvy:
25
+ enabled: true
26
+ exclude: []
27
+ IrresponsibleModule:
28
+ enabled: true
29
+ exclude: []
30
+ LongParameterList:
31
+ enabled: true
32
+ exclude:
33
+ - Substation::Dispatcher#call
34
+ max_params: 2
35
+ LongYieldList:
36
+ enabled: true
37
+ exclude: []
38
+ max_params: 2
39
+ NestedIterators:
40
+ enabled: true
41
+ exclude: []
42
+ max_allowed_nesting: 1
43
+ ignore_iterators: []
44
+ NilCheck:
45
+ enabled: true
46
+ exclude: []
47
+ RepeatedConditional:
48
+ enabled: true
49
+ exclude: []
50
+ max_ifs: 1
51
+ TooManyInstanceVariables:
52
+ enabled: true
53
+ exclude:
54
+ - Substation::Response
55
+ max_instance_variables: 3
56
+ TooManyMethods:
57
+ enabled: true
58
+ exclude: []
59
+ max_methods: 4
60
+ TooManyStatements:
61
+ enabled: true
62
+ exclude:
63
+ - Substation::Utils#self.const_get
64
+ - Substation::Utils#self.symbolize_keys
65
+ max_statements: 3
66
+ UncommunicativeMethodName:
67
+ enabled: true
68
+ exclude: []
69
+ reject:
70
+ - !ruby/regexp /^[a-z]$/
71
+ - !ruby/regexp /[0-9]$/
72
+ - !ruby/regexp /[A-Z]/
73
+ accept: []
74
+ UncommunicativeModuleName:
75
+ enabled: true
76
+ exclude: []
77
+ reject:
78
+ - !ruby/regexp /^.$/
79
+ - !ruby/regexp /[0-9]$/
80
+ accept: []
81
+ UncommunicativeParameterName:
82
+ enabled: true
83
+ exclude: []
84
+ reject:
85
+ - !ruby/regexp /^.$/
86
+ - !ruby/regexp /[0-9]$/
87
+ - !ruby/regexp /[A-Z]/
88
+ accept: []
89
+ UncommunicativeVariableName:
90
+ enabled: true
91
+ exclude: []
92
+ reject:
93
+ - !ruby/regexp /^.$/
94
+ - !ruby/regexp /[0-9]$/
95
+ - !ruby/regexp /[A-Z]/
96
+ accept: []
97
+ UnusedParameters:
98
+ enabled: true
99
+ exclude: []
100
+ UtilityFunction:
101
+ enabled: true
102
+ exclude: []
103
+ max_helper_calls: 0
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 100.0