substation 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.travis.yml +4 -0
  2. data/Changelog.md +92 -2
  3. data/Gemfile.devtools +19 -17
  4. data/README.md +116 -46
  5. data/config/flay.yml +2 -2
  6. data/config/mutant.yml +1 -0
  7. data/config/reek.yml +11 -5
  8. data/config/rubocop.yml +35 -0
  9. data/lib/substation.rb +10 -1
  10. data/lib/substation/chain.rb +108 -64
  11. data/lib/substation/chain/dsl.rb +62 -37
  12. data/lib/substation/dispatcher.rb +3 -1
  13. data/lib/substation/environment.rb +9 -6
  14. data/lib/substation/environment/dsl.rb +4 -3
  15. data/lib/substation/observer.rb +2 -0
  16. data/lib/substation/processor.rb +106 -7
  17. data/lib/substation/processor/evaluator.rb +98 -7
  18. data/lib/substation/processor/transformer.rb +26 -0
  19. data/lib/substation/processor/wrapper.rb +5 -3
  20. data/lib/substation/request.rb +12 -1
  21. data/lib/substation/response.rb +13 -0
  22. data/lib/substation/utils.rb +3 -1
  23. data/lib/substation/version.rb +3 -1
  24. data/spec/integration/substation/dispatcher/call_spec.rb +12 -12
  25. data/spec/spec_helper.rb +39 -32
  26. data/spec/unit/substation/chain/call_spec.rb +205 -29
  27. data/spec/unit/substation/chain/class_methods/failure_response_spec.rb +16 -0
  28. data/spec/unit/substation/chain/dsl/builder/dsl_spec.rb +7 -4
  29. data/spec/unit/substation/chain/dsl/class_methods/build_spec.rb +24 -0
  30. data/spec/unit/substation/chain/dsl/failure_chain_spec.rb +35 -0
  31. data/spec/unit/substation/chain/dsl/processors_spec.rb +8 -6
  32. data/spec/unit/substation/chain/dsl/use_spec.rb +1 -1
  33. data/spec/unit/substation/chain/each_spec.rb +5 -9
  34. data/spec/unit/substation/chain/failure_data/equalizer_spec.rb +46 -0
  35. data/spec/unit/substation/chain/failure_data/hash_spec.rb +13 -0
  36. data/spec/unit/substation/dispatcher/action/call_spec.rb +2 -1
  37. data/spec/unit/substation/dispatcher/action/class_methods/coerce_spec.rb +7 -5
  38. data/spec/unit/substation/dispatcher/call_spec.rb +2 -2
  39. data/spec/unit/substation/dispatcher/class_methods/coerce_spec.rb +6 -6
  40. data/spec/unit/substation/environment/chain_spec.rb +22 -27
  41. data/spec/unit/substation/environment/class_methods/build_spec.rb +11 -4
  42. data/spec/unit/substation/environment/dsl/class_methods/registry_spec.rb +5 -3
  43. data/spec/unit/substation/environment/dsl/register_spec.rb +8 -3
  44. data/spec/unit/substation/environment/dsl/registry_spec.rb +5 -3
  45. data/spec/unit/substation/environment/equalizer_spec.rb +25 -0
  46. data/spec/unit/substation/observer/chain/call_spec.rb +2 -0
  47. data/spec/unit/substation/observer/class_methods/coerce_spec.rb +2 -0
  48. data/spec/unit/substation/observer/null/call_spec.rb +2 -0
  49. data/spec/unit/substation/processor/evaluator/call_spec.rb +20 -10
  50. data/spec/unit/substation/processor/evaluator/class_methods/new_spec.rb +9 -0
  51. data/spec/unit/substation/processor/evaluator/data/call_spec.rb +34 -0
  52. data/spec/unit/substation/processor/evaluator/pivot/call_spec.rb +34 -0
  53. data/spec/unit/substation/processor/evaluator/request/call_spec.rb +34 -0
  54. data/spec/unit/substation/processor/fallible/name_spec.rb +15 -0
  55. data/spec/unit/substation/processor/fallible/with_failure_chain_spec.rb +18 -0
  56. data/spec/unit/substation/processor/incoming/result_spec.rb +25 -0
  57. data/spec/unit/substation/processor/outgoing/call_spec.rb +28 -0
  58. data/spec/unit/substation/processor/outgoing/name_spec.rb +14 -0
  59. data/spec/unit/substation/processor/outgoing/success_predicate_spec.rb +15 -0
  60. data/spec/unit/substation/{chain/outgoing → processor}/result_spec.rb +4 -3
  61. data/spec/unit/substation/processor/success_predicate_spec.rb +22 -0
  62. data/spec/unit/substation/processor/transformer/call_spec.rb +21 -0
  63. data/spec/unit/substation/processor/wrapper/call_spec.rb +9 -7
  64. data/spec/unit/substation/request/env_spec.rb +3 -2
  65. data/spec/unit/substation/request/error_spec.rb +2 -1
  66. data/spec/unit/substation/request/input_spec.rb +3 -2
  67. data/spec/unit/substation/request/name_spec.rb +15 -0
  68. data/spec/unit/substation/request/success_spec.rb +2 -1
  69. data/spec/unit/substation/response/env_spec.rb +3 -2
  70. data/spec/unit/substation/response/failure/success_predicate_spec.rb +2 -1
  71. data/spec/unit/substation/response/input_spec.rb +3 -2
  72. data/spec/unit/substation/response/output_spec.rb +2 -1
  73. data/spec/unit/substation/response/request_spec.rb +3 -2
  74. data/spec/unit/substation/response/success/success_predicate_spec.rb +2 -1
  75. data/spec/unit/substation/response/to_request_spec.rb +19 -0
  76. data/spec/unit/substation/utils/class_methods/coerce_callable_spec.rb +14 -12
  77. data/spec/unit/substation/utils/class_methods/const_get_spec.rb +6 -6
  78. data/spec/unit/substation/utils/class_methods/symbolize_keys_spec.rb +4 -4
  79. metadata +25 -9
  80. data/lib/substation/processor/pivot.rb +0 -25
  81. data/spec/unit/substation/chain/class_methods/build_spec.rb +0 -31
  82. data/spec/unit/substation/chain/dsl/class_methods/processors_spec.rb +0 -23
  83. data/spec/unit/substation/chain/incoming/result_spec.rb +0 -21
  84. data/spec/unit/substation/chain/outgoing/call_spec.rb +0 -25
  85. data/spec/unit/substation/processor/pivot/call_spec.rb +0 -16
@@ -0,0 +1,35 @@
1
+ AllCops:
2
+ Includes:
3
+ - '../**/*.rake'
4
+ Excludes:
5
+ - '../spec/spec_helper.rb'
6
+
7
+ # Avoid parameter lists longer than five parameters.
8
+ ParameterLists:
9
+ Max: 3
10
+ CountKeywordArgs: true
11
+
12
+ # Avoid more than `Max` levels of nesting.
13
+ BlockNesting:
14
+ Max: 3
15
+
16
+ HashSyntax:
17
+ Enabled: false
18
+
19
+ Blocks:
20
+ Enabled: false
21
+
22
+ SpaceInsideBrackets:
23
+ Enabled: false
24
+
25
+ Documentation:
26
+ Enabled: false # reek already checks this and rubocop requires duplicate docs
27
+
28
+ SingleLineMethods:
29
+ Enabled: false
30
+
31
+ LineLength:
32
+ Max: 106 # the offending lines are in specs, sadly this means global disabling for now
33
+
34
+ MethodLength:
35
+ Max: 12 # reek performs these checks anyway
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'set'
2
4
  require 'forwardable'
3
5
 
@@ -31,6 +33,13 @@ require 'concord'
31
33
  # action.
32
34
 
33
35
  module Substation
36
+
37
+ # An empty frozen array useful for (default) parameters
38
+ EMPTY_ARRAY = [].freeze
39
+
40
+ # Error raised when trying to access an unknown processor
41
+ UnknownProcessor = Class.new(StandardError)
42
+
34
43
  end
35
44
 
36
45
  require 'substation/utils'
@@ -41,8 +50,8 @@ require 'substation/chain'
41
50
  require 'substation/chain/dsl'
42
51
  require 'substation/processor'
43
52
  require 'substation/processor/evaluator'
44
- require 'substation/processor/pivot'
45
53
  require 'substation/processor/wrapper'
54
+ require 'substation/processor/transformer'
46
55
  require 'substation/environment'
47
56
  require 'substation/environment/dsl'
48
57
  require 'substation/dispatcher'
@@ -1,20 +1,19 @@
1
+ # encoding: utf-8
2
+
1
3
  module Substation
2
4
 
3
5
  # Implements a chain of responsibility for an action
4
6
  #
5
7
  # An instance of this class will typically contain (in that order)
6
- # a few handlers that process the incoming {Request} object, one
7
- # handler that calls an action ({Chain::Pivot}), and some handlers
8
+ # a few processors that process the incoming {Request} object, one
9
+ # processor that calls an action ({Processor::Pivot}), and some processors
8
10
  # that process the outgoing {Response} object.
9
11
  #
10
- # Both {Chain::Incoming} and {Chain::Outgoing} handlers must
11
- # respond to `#call(response)` and `#result(response)`.
12
- #
13
- # @example chain handlers (used in instance method examples)
12
+ # @example chain processors (used in instance method examples)
14
13
  #
15
14
  # module App
16
15
  #
17
- # class Handler
16
+ # class Processor
18
17
  #
19
18
  # def initialize(handler = nil)
20
19
  # @handler = handler
@@ -25,11 +24,11 @@ module Substation
25
24
  # attr_reader :handler
26
25
  #
27
26
  # class Incoming < self
28
- # include Substation::Chain::Incoming
27
+ # include Substation::Processor::Incoming
29
28
  # end
30
29
  #
31
30
  # class Outgoing < self
32
- # include Substation::Chain::Outgoing
31
+ # include Substation::Processor::Outgoing
33
32
  #
34
33
  # private
35
34
  #
@@ -39,13 +38,13 @@ module Substation
39
38
  # end
40
39
  # end
41
40
  #
42
- # class Validator < Handler::Incoming
41
+ # class Validator < Processor::Incoming
43
42
  # def call(request)
44
43
  # result = handler.call(request.input)
45
- # if result.valid?
44
+ # if result.success?
46
45
  # request.success(request.input)
47
46
  # else
48
- # request.error(result.violations)
47
+ # request.error(result.output)
49
48
  # end
50
49
  # end
51
50
  # end
@@ -58,7 +57,7 @@ module Substation
58
57
  # end
59
58
  # end
60
59
  #
61
- # class Presenter < Handler::Outgoing
60
+ # class Presenter < Processor::Outgoing
62
61
  # def call(response)
63
62
  # respond_with(response, handler.new(response.output))
64
63
  # end
@@ -67,74 +66,97 @@ module Substation
67
66
  #
68
67
  class Chain
69
68
 
70
- # Supports chaining handlers processed before the {Pivot}
71
- module Incoming
69
+ include Enumerable
70
+ include Concord.new(:processors, :failure_chain)
71
+ include Adamantium::Flat
72
72
 
73
- # The request passed on to the next handler in a {Chain}
74
- #
75
- # @param [Response] response
76
- # the response returned from the previous handler in a {Chain}
73
+ # Empty chain
74
+ EMPTY = Class.new(self).new(EMPTY_ARRAY, EMPTY_ARRAY)
75
+
76
+ # Wraps response data and an exception not caught from a handler
77
+ class FailureData
78
+ include Equalizer.new(:data)
79
+
80
+ # Return the data available when +exception+ was raised
77
81
  #
78
- # @return [Request]
79
- # the request passed on to the next handler in a {Chain}
82
+ # @return [Object]
80
83
  #
81
84
  # @api private
82
- def result(response)
83
- Request.new(response.env, response.output)
84
- end
85
- end
85
+ attr_reader :data
86
86
 
87
- # Supports chaining the {Pivot} or handlers processed after the {Pivot}
88
- module Outgoing
87
+ # Return the exception instance
88
+ #
89
+ # @return [Class<StandardError>]
90
+ #
91
+ # @api private
92
+ attr_reader :exception
89
93
 
90
- # The response passed on to the next handler in a {Chain}
94
+ # Initialize a new instance
95
+ #
96
+ # @param [Object] data
97
+ # the data available when +exception+ was raised
91
98
  #
92
- # @param [Response] response
93
- # the response returned from the previous handler in a {Chain}
99
+ # @param [Class<StandardError>] exception
100
+ # the exception instance raised from a handler
94
101
  #
95
- # @return [Response]
96
- # the response passed on to the next handler in a {Chain}
102
+ # @return [undefined]
97
103
  #
98
104
  # @api private
99
- def result(response)
100
- response
105
+ def initialize(data, exception)
106
+ @data, @exception = data, exception
107
+ end
108
+
109
+ # Return the hash value
110
+ #
111
+ # @return [Fixnum]
112
+ #
113
+ # @api private
114
+ def hash
115
+ super ^ exception.class.hash
101
116
  end
102
117
 
103
118
  private
104
119
 
105
- # Build a new {Response} based on +response+ and +output+
120
+ # Tests wether +other+ is comparable using +comparator+
106
121
  #
107
- # @param [Response] response
108
- # the original response
122
+ # @param [Symbol] comparator
123
+ # the operation used for comparison
109
124
  #
110
- # @param [Object] output
111
- # the data to be wrapped within the new {Response}
125
+ # @param [Object] other
126
+ # the object to test
112
127
  #
113
- # @return [Response]
128
+ # @return [Boolean]
114
129
  #
115
130
  # @api private
116
- def respond_with(response, output)
117
- response.class.new(response.request, output)
131
+ def cmp?(comparator, other)
132
+ super && exception.class.send(comparator, other.exception.class)
118
133
  end
119
134
  end
120
135
 
121
- # Supports chaining the {Pivot} handler
122
- Pivot = Outgoing
123
-
124
- include Enumerable
125
- include Concord.new(:handlers)
126
- include Adamantium::Flat
127
- include Pivot # allow nesting of chains
128
-
129
- # Empty chain
130
- EMPTY = Class.new(self).new([])
136
+ # Return a failure response
137
+ #
138
+ # @param [Request] request
139
+ # the initial request passed into the chain
140
+ #
141
+ # @param [Object] data
142
+ # the processed data available when the exception was raised
143
+ #
144
+ # @param [Class<StandardError>] exception
145
+ # the exception instance that was raised
146
+ #
147
+ # @return [Response::Failure]
148
+ #
149
+ # @api private
150
+ def self.failure_response(request, data, exception)
151
+ Response::Failure.new(request, FailureData.new(data, exception))
152
+ end
131
153
 
132
154
  # Call the chain
133
155
  #
134
- # Invokes all handlers and returns either the first
156
+ # Invokes all processors and returns either the first
135
157
  # {Response::Failure} that it encounters, or if all
136
158
  # goes well, the {Response::Success} returned from
137
- # the last handler.
159
+ # the last processor.
138
160
  #
139
161
  # @example
140
162
  #
@@ -162,20 +184,21 @@ module Substation
162
184
  # the request to handle
163
185
  #
164
186
  # @return [Response::Success]
165
- # the response returned from the last handler
187
+ # the response returned from the last processor
166
188
  #
167
189
  # @return [Response::Failure]
168
- # the response returned from the failing handler
169
- #
170
- # @raise [Exception]
171
- # any exception that isn't explicitly rescued in client code
190
+ # the response returned from the failing processor's failure chain
172
191
  #
173
192
  # @api public
174
193
  def call(request)
175
- handlers.inject(request) { |result, handler|
176
- response = handler.call(result)
177
- return response unless response.success?
178
- handler.result(response)
194
+ processors.reduce(request) { |result, processor|
195
+ begin
196
+ response = processor.call(result)
197
+ return response unless processor.success?(response)
198
+ processor.result(response)
199
+ rescue => exception
200
+ return on_failure(request, result, exception)
201
+ end
179
202
  }
180
203
  end
181
204
 
@@ -194,8 +217,29 @@ module Substation
194
217
  # @api private
195
218
  def each(&block)
196
219
  return to_enum unless block
197
- handlers.each(&block)
220
+ processors.each(&block)
198
221
  self
199
222
  end
223
+
224
+ private
225
+
226
+ # Call the failure chain in case of an uncaught exception
227
+ #
228
+ # @param [Request] request
229
+ # the initial request passed into the chain
230
+ #
231
+ # @param [Object] data
232
+ # the processed data available when the exception was raised
233
+ #
234
+ # @param [Class<StandardError>] exception
235
+ # the exception instance that was raised
236
+ #
237
+ # @return [Response::Failure]
238
+ #
239
+ # @api private
240
+ def on_failure(request, data, exception)
241
+ failure_chain.call(self.class.failure_response(request, data, exception))
242
+ end
243
+
200
244
  end # class Chain
201
245
  end # module Substation
@@ -1,25 +1,8 @@
1
- module Substation
1
+ # encoding: utf-8
2
2
 
3
+ module Substation
3
4
  class Chain
4
5
 
5
- # Build a new chain instance
6
- #
7
- # @param [DSL] dsl
8
- # the dsl klass to use for defining the chain
9
- #
10
- # @param [Chain] other
11
- # another chain to build on top of
12
- #
13
- # @param [Proc] block
14
- # a block to instance_eval in the context of +dsl+
15
- #
16
- # @return [Chain]
17
- #
18
- # @api private
19
- def self.build(dsl, other, &block)
20
- new(dsl.processors(other, &block))
21
- end
22
-
23
6
  # The DSL class used to define chains in an {Environment}
24
7
  class DSL
25
8
 
@@ -72,7 +55,6 @@ module Substation
72
55
  }
73
56
  end
74
57
 
75
-
76
58
  # Define a new instance method on the +dsl+ class
77
59
  #
78
60
  # @param [Symbol] name
@@ -89,38 +71,36 @@ module Substation
89
71
  # @api private
90
72
  def define_dsl_method(name, processor, dsl)
91
73
  dsl.class_eval do
92
- define_method(name) { |*args| use(processor.new(*args)) }
74
+ define_method(name) { |*args| use(processor.new(name, *args)) }
93
75
  end
94
76
  end
95
77
 
96
78
  end # class Builder
97
79
 
98
- # The processors to be used within a {Chain}
80
+ # Build a new {Chain} based on +other+, a +failure_chain+ and a block
99
81
  #
100
- # @return [Array<#call>]
101
- #
102
- # @api private
103
- attr_reader :processors
104
-
105
- # The processors to be used within a {Chain}
82
+ # @param [#each<#call>] other
83
+ # the processors to build on top of
106
84
  #
107
- # @param [Chain] chain
108
- # the chain to build on top of
85
+ # @param [Chain] failure_chain
86
+ # the failure chain to invoke in case of an uncaught exception
109
87
  #
110
88
  # @param [Proc] block
111
89
  # a block to be instance_eval'ed
112
90
  #
113
- # @return [Array<#call>]
91
+ # @return [Chain]
114
92
  #
115
93
  # @api private
116
- def self.processors(chain, &block)
117
- new(chain, &block).processors
94
+ def self.build(other, failure_chain, &block)
95
+ Chain.new(new(other, &block).processors, failure_chain)
118
96
  end
119
97
 
98
+ include Equalizer.new(:processors)
99
+
120
100
  # Initialize a new instance
121
101
  #
122
102
  # @param [#each<#call>] processors
123
- # an enumerable of processors to build on top of
103
+ # the processors to build on top of
124
104
  #
125
105
  # @param [Proc] block
126
106
  # a block to be instance_eval'ed
@@ -129,11 +109,20 @@ module Substation
129
109
  #
130
110
  # @api private
131
111
  def initialize(processors, &block)
132
- @processors = []
112
+ @processors = {}
133
113
  chain(processors)
134
114
  instance_eval(&block) if block
135
115
  end
136
116
 
117
+ # The processors to be used within a {Chain}
118
+ #
119
+ # @return [Array<#call>]
120
+ #
121
+ # @api private
122
+ def processors
123
+ @processors.values
124
+ end
125
+
137
126
  # Use the given +processor+ within a chain
138
127
  #
139
128
  # @param [#call] processor
@@ -143,7 +132,7 @@ module Substation
143
132
  #
144
133
  # @api private
145
134
  def use(processor)
146
- @processors << processor
135
+ @processors[processor.name] = processor
147
136
  self
148
137
  end
149
138
 
@@ -156,10 +145,46 @@ module Substation
156
145
  #
157
146
  # @api private
158
147
  def chain(other)
159
- other.each { |handler| use(handler) }
148
+ other.each { |processor| use(processor) }
149
+ self
150
+ end
151
+
152
+ # Use +chain+ as the failure chain for the processor identified by +name+
153
+ #
154
+ # @param [Symbol] name
155
+ # the processor's name
156
+ #
157
+ # @param [#call] chain
158
+ # the failure chain to use for the processor identified by +name+
159
+ #
160
+ # @return [self]
161
+ #
162
+ # @api private
163
+ def failure_chain(name, chain)
164
+ @processors[name] = processor(name).with_failure_chain(chain)
160
165
  self
161
166
  end
162
167
 
168
+ private
169
+
170
+ # Return the processor identified by +name+
171
+ #
172
+ # @param [Symbol] name
173
+ # the processor's name
174
+ #
175
+ # @return [#call]
176
+ # the processor identified by +name+
177
+ #
178
+ # @raise [UnknownProcessor]
179
+ # if no processor identified by +name+ is registered
180
+ #
181
+ # @api private
182
+ def processor(name)
183
+ @processors.fetch(name) {
184
+ raise UnknownProcessor, "No processor named #{name.inspect} is registered"
185
+ }
186
+ end
187
+
163
188
  end # class DSL
164
189
  end # class Chain
165
190
  end # module Substation