cuprum 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,6 +11,7 @@ module Cuprum
11
11
  # # the value of the previous command.
12
12
  #
13
13
  # class GenerateUrlCommand
14
+ # include Cuprum::Chaining
14
15
  # include Cuprum::Processing
15
16
  #
16
17
  # private
@@ -47,6 +48,7 @@ module Cuprum
47
48
  # # the command is failing. This can be used to perform error handling.
48
49
  #
49
50
  # class CreateTaggingCommand
51
+ # include Cuprum::Chaining
50
52
  # include Cuprum::Processing
51
53
  #
52
54
  # private
@@ -111,6 +113,7 @@ module Cuprum
111
113
  # # ignored.
112
114
  #
113
115
  # class UpdatePostCommand
116
+ # include Cuprum::Chaining
114
117
  # include Cuprum::Processing
115
118
  #
116
119
  # private
@@ -148,6 +151,25 @@ module Cuprum
148
151
  # end
149
152
  # end
150
153
  #
154
+ # @example Protected Chaining Methods
155
+ # # Using the protected chaining methods #chain!, #tap_result!, and
156
+ # # #yield_result!, you can create a command class that composes other
157
+ # # commands.
158
+ #
159
+ # # We subclass the build command, which will be executed first.
160
+ # class CreateCommentCommand < BuildCommentCommand
161
+ # include Cuprum::Chaining
162
+ # include Cuprum::Processing
163
+ #
164
+ # def initialize
165
+ # # After the build step is run, we validate the comment.
166
+ # chain!(ValidateCommentCommand.new)
167
+ #
168
+ # # If the validation passes, we then save the comment.
169
+ # chain!(SaveCommentCommand.new, on: :success)
170
+ # end
171
+ # end
172
+ #
151
173
  # @see Cuprum::Command
152
174
  module Chaining
153
175
  # Creates a copy of the first command, and then chains the given command or
@@ -193,15 +215,7 @@ module Cuprum
193
215
  #
194
216
  # @yieldparam value [Object] The value of the previous result.
195
217
  def chain command = nil, on: nil, &block
196
- command ||= Cuprum::Command.new(&block)
197
-
198
- clone.tap do |fn|
199
- fn.chained_procs <<
200
- {
201
- :proc => chain_command(command),
202
- :on => on
203
- } # end hash
204
- end # tap
218
+ clone.chain!(command, :on => on, &block)
205
219
  end # method chain
206
220
 
207
221
  # Shorthand for command.chain(:on => :failure). Creates a copy of the first
@@ -223,7 +237,7 @@ module Cuprum
223
237
  #
224
238
  # @yieldparam value [Object] The value of the previous result.
225
239
  def failure command = nil, &block
226
- chain(command, :on => :failure, &block)
240
+ clone.chain!(command, :on => :failure, &block)
227
241
  end # method failure
228
242
 
229
243
  # Shorthand for command.chain(:on => :success). Creates a copy of the first
@@ -245,7 +259,7 @@ module Cuprum
245
259
  #
246
260
  # @yieldparam value [Object] The value of the previous result.
247
261
  def success command = nil, &block
248
- chain(command, :on => :success, &block)
262
+ clone.chain!(command, :on => :success, &block)
249
263
  end # method success
250
264
 
251
265
  # As #yield_result, but always returns the previous result when the block is
@@ -259,15 +273,7 @@ module Cuprum
259
273
  #
260
274
  # @see #yield_result
261
275
  def tap_result on: nil, &block
262
- tapped = ->(result) { result.tap { block.call(result) } }
263
-
264
- clone.tap do |fn|
265
- fn.chained_procs <<
266
- {
267
- :proc => tapped,
268
- :on => on
269
- } # end hash
270
- end # tap
276
+ clone.tap_result!(:on => on, &block)
271
277
  end # method tap_result
272
278
 
273
279
  # Creates a copy of the command, and then chains the block to execute after
@@ -291,17 +297,63 @@ module Cuprum
291
297
  #
292
298
  # @see #tap_result
293
299
  def yield_result on: nil, &block
294
- clone.tap do |fn|
295
- fn.chained_procs <<
296
- {
297
- :proc => block,
298
- :on => on
299
- } # end hash
300
- end # tap
300
+ clone.yield_result!(:on => on, &block)
301
301
  end # method yield_result
302
302
 
303
303
  protected
304
304
 
305
+ # @!visibility public
306
+ #
307
+ # As #chain, but modifies the current command instead of creating a clone.
308
+ # This is a protected method, and is meant to be called by the command to be
309
+ # chained, such as during #initialize.
310
+ #
311
+ # @return [Cuprum::Chaining] The current command.
312
+ #
313
+ # @see #chain
314
+ #
315
+ # @overload chain!(command, on: nil)
316
+ # @param command [Cuprum::Command] The command to chain.
317
+ #
318
+ # @param on [Symbol] Sets a condition on when the chained block can run,
319
+ # based on the previous result. Valid values are :success, :failure, and
320
+ # :always. If the value is :success, the block will be called only if
321
+ # the previous result succeeded and is not halted. If the value is
322
+ # :failure, the block will be called only if the previous result failed
323
+ # and is not halted. If the value is :always, the block will be called
324
+ # regardless of the previous result status, even if the previous result
325
+ # is halted. If no value is given, the command will run whether the
326
+ # previous command was a success or a failure, but not if the command
327
+ # chain has been halted.
328
+ #
329
+ # @overload chain!(on: nil) { |value| }
330
+ # Creates an anonymous command from the given block. The command will be
331
+ # passed the value of the previous result.
332
+ #
333
+ # @param on [Symbol] Sets a condition on when the chained block can run,
334
+ # based on the previous result. Valid values are :success, :failure, and
335
+ # :always. If the value is :success, the block will be called only if
336
+ # the previous result succeeded and is not halted. If the value is
337
+ # :failure, the block will be called only if the previous result failed
338
+ # and is not halted. If the value is :always, the block will be called
339
+ # regardless of the previous result status, even if the previous result
340
+ # is halted. If no value is given, the command will run whether the
341
+ # previous command was a success or a failure, but not if the command
342
+ # chain has been halted.
343
+ #
344
+ # @yieldparam value [Object] The value of the previous result.
345
+ def chain! command = nil, on: nil, &block
346
+ command ||= Cuprum::Command.new(&block)
347
+
348
+ chained_procs <<
349
+ {
350
+ :proc => chain_command(command),
351
+ :on => on
352
+ } # end hash
353
+
354
+ self
355
+ end # method chain!
356
+
305
357
  def chained_procs
306
358
  @chained_procs ||= []
307
359
  end # method chained_procs
@@ -310,6 +362,54 @@ module Cuprum
310
362
  yield_chain(super)
311
363
  end # method call
312
364
 
365
+ # @!visibility public
366
+ #
367
+ # As #tap_result, but modifies the current command instead of creating a
368
+ # clone. This is a protected method, and is meant to be called by the
369
+ # command to be chained, such as during #initialize.
370
+ #
371
+ # @param (see #tap_result)
372
+ #
373
+ # @yieldparam result [Cuprum::Result] The #result of the previous command.
374
+ #
375
+ # @return (see #tap_result)
376
+ #
377
+ # @see #tap_result
378
+ def tap_result! on: nil, &block
379
+ tapped = ->(result) { result.tap { block.call(result) } }
380
+
381
+ chained_procs <<
382
+ {
383
+ :proc => tapped,
384
+ :on => on
385
+ } # end hash
386
+
387
+ self
388
+ end # method tap_result!
389
+
390
+ # @!visibility public
391
+ #
392
+ # As #yield_result, but modifies the current command instead of creating a
393
+ # clone. This is a protected method, and is meant to be called by the
394
+ # command to be chained, such as during #initialize.
395
+ #
396
+ # @param (see #yield_result)
397
+ #
398
+ # @yieldparam result [Cuprum::Result] The #result of the previous command.
399
+ #
400
+ # @return (see #yield_result)
401
+ #
402
+ # @see #yield_result
403
+ def yield_result! on: nil, &block
404
+ chained_procs <<
405
+ {
406
+ :proc => block,
407
+ :on => on
408
+ } # end hash
409
+
410
+ self
411
+ end # method yield_result!
412
+
313
413
  private
314
414
 
315
415
  def chain_command command
@@ -342,7 +442,7 @@ module Cuprum
342
442
  if value_is_result?(value)
343
443
  value.to_result
344
444
  else
345
- build_result(value, :errors => build_errors)
445
+ build_result(value)
346
446
  end # if-else
347
447
  end # reduce
348
448
  end # method yield_chain
@@ -1,6 +1,5 @@
1
1
  require 'cuprum/chaining'
2
2
  require 'cuprum/processing'
3
- require 'cuprum/result_helpers'
4
3
 
5
4
  module Cuprum
6
5
  # Functional object that encapsulates a business logic operation with a
@@ -112,11 +111,9 @@ module Cuprum
112
111
  #
113
112
  # @see Cuprum::Chaining
114
113
  # @see Cuprum::Processing
115
- # @see Cuprum::ResultHelpers
116
114
  class Command
117
115
  include Cuprum::Processing
118
116
  include Cuprum::Chaining
119
- include Cuprum::ResultHelpers
120
117
 
121
118
  # Returns a new instance of Cuprum::Command.
122
119
  #
@@ -0,0 +1,276 @@
1
+ require 'cuprum'
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+
5
+ module Cuprum
6
+ # Builder class for instantiating command objects.
7
+ #
8
+ # @example
9
+ # class SpaceFactory < Cuprum::CommandFactory
10
+ # command :build, BuildCommand
11
+ #
12
+ # command :fly { |launch_site:| FlyCommand.new(launch_site) }
13
+ #
14
+ # command_class :dream { DreamCommand }
15
+ # end
16
+ #
17
+ # factory = SpaceFactory.new
18
+ #
19
+ # factory::Build #=> BuildCommand
20
+ # factory.build #=> an instance of BuildCommand
21
+ #
22
+ # rocket = factory.build.call({ size: 'big' }) #=> an instance of Rocket
23
+ # rocket.size #=> 'big'
24
+ #
25
+ # command = factory.fly(launch_site: 'KSC') #=> an instance of FlyCommand
26
+ # command.call(rocket)
27
+ # #=> launches the rocket from KSC
28
+ #
29
+ # factory::Dream #=> DreamCommand
30
+ # factory.dream #=> an instance of DreamCommand
31
+ class CommandFactory < Module
32
+ # Defines the Domain-Specific Language and helper methods for dynamically
33
+ # defined commands.
34
+ class << self
35
+ # Defines a command for the factory.
36
+ #
37
+ # @overload command(name, command_class)
38
+ # Defines a command using the given factory class. For example, when a
39
+ # command is defined with the name "whirlpool" and the WhirlpoolCommand
40
+ # class:
41
+ #
42
+ # A factory instance will define the constant ::Whirlpool, and accessing
43
+ # factory::Whirlpool will return the WhirlpoolCommand class.
44
+ #
45
+ # A factory instance will define the method #whirlpool, and calling
46
+ # factory#whirlpool will return an instance of WhirlpoolCommand. Any
47
+ # arguments passed to the #whirlpool method will be forwarded to the
48
+ # constructor when building the command.
49
+ #
50
+ # @param name [String, Symbol] The name of the command.
51
+ # @param command_class [Class] The command class. Must be a subclass of
52
+ # Cuprum::Command.
53
+ #
54
+ # @example
55
+ # class MoveFactory < Cuprum::CommandFactory
56
+ # command :cut, CutCommand
57
+ # end
58
+ #
59
+ # factory = MoveFactory.new
60
+ # factory::Cut #=> CutCommand
61
+ # factory.cut #=> an instance of CutCommand
62
+ #
63
+ # @overload command(name) { |*args| }
64
+ # Defines a command using the given block, which must return an instance
65
+ # of a Cuprum::Command subclass. For example, when a command is defined
66
+ # with the name "dive" and a block that returns an instance of the
67
+ # DiveCommand class:
68
+ #
69
+ # A factory instance will define the method #dive, and calling
70
+ # factory#dive will call the block and return the resulting command
71
+ # instance. Any arguments passed to the #dive method will be forwarded
72
+ # to the block when building the command.
73
+ #
74
+ # The block will be evaluated in the context of the factory instance, so
75
+ # it has access to any methods or instance variables defined for the
76
+ # factory instance.
77
+ #
78
+ # @param name [String, Symbol] The name of the command.
79
+ #
80
+ # @yield The block will be executed in the context of the factory
81
+ # instance.
82
+ # @yieldparam *args [Array] Any arguments given to the method
83
+ # factory.name() will be passed on the block.
84
+ # @yieldreturn [Cuprum::Command] The block return an instance of a
85
+ # Cuprum::Command subclass, or else raise an error.
86
+ #
87
+ # @example
88
+ # class MoveFactory < Cuprum::CommandFactory
89
+ # command :fly { |destination| FlyCommand.new(destination) }
90
+ # end
91
+ #
92
+ # factory = MoveFactory.new
93
+ # factory.fly_command('Indigo Plateau')
94
+ # #=> an instance of FlyCommand with a destination of 'Indigo Plateau'
95
+ def command(name, klass = nil, **metadata, &defn)
96
+ guard_abstract_factory!
97
+
98
+ if klass
99
+ define_command_from_class(klass, name: name, metadata: metadata)
100
+ elsif block_given?
101
+ define_command_from_block(defn, name: name, metadata: metadata)
102
+ else
103
+ require_definition!
104
+ end
105
+ end
106
+
107
+ # Defines a command using the given block, which must return a subclass of
108
+ # Cuprum::Command. For example, when a command is defined with the name
109
+ # "rock_climb" and a block returning a subclass of RockClimbCommand:
110
+ #
111
+ # A factory instance will define the constant ::RockClimb, and accessing
112
+ # factory::RockClimb will call the block and return the resulting command
113
+ # class. This value is memoized, so subsequent factory::RockClimb accesses
114
+ # on the same factory instance will return the same command class.
115
+ #
116
+ # A factory instance will define the method #rock_climb, and calling
117
+ # factory#rock_climb will access the constant at ::RockClimb and return an
118
+ # instance of that subclass of RockClimbCommand. Any arguments passed to
119
+ # the #whirlpool method will be forwarded to the constructor when building
120
+ # the command.
121
+ #
122
+ # @param name [String, Symbol] The name of the command.
123
+ # @yield The block will be executed in the context of the factory
124
+ # instance.
125
+ # @yieldparam *args [Array] Any arguments given to the method
126
+ # factory.name() will be passed on the block.
127
+ # @yieldreturn [Cuprum::Command] The block return an instance of a
128
+ # Cuprum::Command subclass, or else raise an error.
129
+ #
130
+ # @example
131
+ # class MoveFactory < Cuprum::CommandFactory
132
+ # command_class :flash do
133
+ # Class.new(FlashCommand) do
134
+ # def brightness
135
+ # :intense
136
+ # end
137
+ # end
138
+ # end
139
+ # end
140
+ #
141
+ # factory = MoveFactory.new
142
+ # factory::Flash #=> a subclass of FlashCommand
143
+ # factory.flash #=> an instance of factory::Flash
144
+ #
145
+ # command = factory.flash
146
+ # command.brightness #=> :intense
147
+ def command_class(name, **metadata, &defn)
148
+ guard_abstract_factory!
149
+
150
+ raise ArgumentError, 'must provide a block'.freeze unless block_given?
151
+
152
+ name = normalize_command_name(name)
153
+
154
+ (@command_definitions ||= {})[name] =
155
+ metadata.merge(__const_defn__: defn)
156
+
157
+ const_name = tools.string.camelize(name)
158
+
159
+ define_method(name) do |*args, &block|
160
+ command_class = const_get(const_name)
161
+
162
+ build_command(command_class, *args, &block)
163
+ end
164
+ end
165
+
166
+ protected
167
+
168
+ def command_definitions
169
+ definitions = (@command_definitions ||= {})
170
+
171
+ return definitions unless superclass < Cuprum::CommandFactory
172
+
173
+ superclass.command_definitions.merge(definitions)
174
+ end
175
+
176
+ private
177
+
178
+ def abstract_factory?
179
+ self == Cuprum::CommandFactory
180
+ end
181
+
182
+ def define_command_from_block(builder, name:, metadata: {})
183
+ command_name = normalize_command_name(name)
184
+
185
+ (@command_definitions ||= {})[command_name] = metadata
186
+
187
+ define_method(command_name) do |*args|
188
+ instance_exec(*args, &builder)
189
+ end
190
+ end
191
+
192
+ def define_command_from_class(command_class, name:, metadata: {})
193
+ guard_invalid_definition!(command_class)
194
+
195
+ command_name = normalize_command_name(name)
196
+
197
+ (@command_definitions ||= {})[command_name] =
198
+ metadata.merge(__const_defn__: command_class)
199
+
200
+ define_method(command_name) do |*args, &block|
201
+ build_command(command_class, *args, &block)
202
+ end
203
+ end
204
+
205
+ def guard_abstract_factory!
206
+ return unless abstract_factory?
207
+
208
+ raise NotImplementedError,
209
+ 'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
210
+ 'define commands for a factory.'.freeze
211
+ end
212
+
213
+ def guard_invalid_definition!(command_class)
214
+ return if command_class.is_a?(Class) && command_class < Cuprum::Command
215
+
216
+ raise ArgumentError, 'definition must be a command class'.freeze
217
+ end
218
+
219
+ def normalize_command_name(command_name)
220
+ tools.string.underscore(command_name).intern
221
+ end
222
+
223
+ def require_definition!
224
+ raise ArgumentError, 'must provide a command class or a block'.freeze
225
+ end
226
+
227
+ def tools
228
+ SleepingKingStudios::Tools::Toolbelt.instance
229
+ end
230
+ end
231
+
232
+ # @return [Boolean] true if the factory defines the given command, otherwise
233
+ # false.
234
+ def command?(command_name)
235
+ command_name = normalize_command_name(command_name)
236
+
237
+ commands.include?(command_name)
238
+ end
239
+
240
+ # @return [Array<Symbol>] a list of the commands defined by the factory.
241
+ def commands
242
+ self.class.send(:command_definitions).keys
243
+ end
244
+
245
+ # @private
246
+ def const_defined?(const_name, inherit = true)
247
+ command?(const_name) || super
248
+ end
249
+
250
+ # @private
251
+ def const_missing(const_name)
252
+ definitions = self.class.send(:command_definitions)
253
+ command_name = normalize_command_name(const_name)
254
+ command_defn = definitions.dig(command_name, :__const_defn__)
255
+
256
+ return super unless command_defn
257
+
258
+ command_class =
259
+ command_defn.is_a?(Proc) ? instance_exec(&command_defn) : command_defn
260
+
261
+ const_set(const_name, command_class)
262
+
263
+ command_class
264
+ end
265
+
266
+ private
267
+
268
+ def build_command(command_class, *args, &block)
269
+ command_class.new(*args, &block)
270
+ end
271
+
272
+ def normalize_command_name(command_name)
273
+ self.class.send(:normalize_command_name, command_name)
274
+ end
275
+ end
276
+ end