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.
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