substation 0.0.8 → 0.0.9

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