cuprum 0.9.1 → 0.10.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@ module Cuprum
4
4
  autoload :Command, 'cuprum/command'
5
5
  autoload :Operation, 'cuprum/operation'
6
6
  autoload :Result, 'cuprum/result'
7
+ autoload :Steps, 'cuprum/steps'
7
8
 
8
9
  class << self
9
10
  # @return [String] The current version of the gem.
@@ -197,7 +197,7 @@ module Cuprum
197
197
  # @see Cuprum::Command
198
198
  module Chaining
199
199
  # (see Cuprum::Processing#call)
200
- def call(*args, &block)
200
+ def call(*args, **kwargs, &block)
201
201
  yield_chain(super)
202
202
  end
203
203
 
@@ -282,6 +282,8 @@ module Cuprum
282
282
 
283
283
  protected
284
284
 
285
+ # rubocop:disable Metrics/MethodLength
286
+
285
287
  # @!visibility public
286
288
  #
287
289
  # As #chain, but modifies the current command instead of creating a clone.
@@ -319,6 +321,11 @@ module Cuprum
319
321
  #
320
322
  # @yieldparam value [Object] The value of the previous result.
321
323
  def chain!(command = nil, on: nil, &block)
324
+ SleepingKingStudios::Tools::CoreTools.deprecate(
325
+ "#{self.class}#chain",
326
+ message: 'Use the #step method to compose commands.'
327
+ )
328
+
322
329
  command ||= Cuprum::Command.new(&block)
323
330
 
324
331
  chained_procs <<
@@ -329,11 +336,14 @@ module Cuprum
329
336
 
330
337
  self
331
338
  end
339
+ # rubocop:enable Metrics/MethodLength
332
340
 
333
341
  def chained_procs
334
342
  @chained_procs ||= []
335
343
  end
336
344
 
345
+ # rubocop:disable Metrics/MethodLength
346
+
337
347
  # @!visibility public
338
348
  #
339
349
  # As #tap_result, but modifies the current command instead of creating a
@@ -348,6 +358,11 @@ module Cuprum
348
358
  #
349
359
  # @see #tap_result
350
360
  def tap_result!(on: nil, &block)
361
+ SleepingKingStudios::Tools::CoreTools.deprecate(
362
+ "#{self.class}#tap_result",
363
+ message: 'Use the #step method to compose commands.'
364
+ )
365
+
351
366
  tapped = ->(result) { result.tap { block.call(result) } }
352
367
 
353
368
  chained_procs <<
@@ -358,6 +373,7 @@ module Cuprum
358
373
 
359
374
  self
360
375
  end
376
+ # rubocop:enable Metrics/MethodLength
361
377
 
362
378
  # @!visibility public
363
379
  #
@@ -373,6 +389,11 @@ module Cuprum
373
389
  #
374
390
  # @see #yield_result
375
391
  def yield_result!(on: nil, &block)
392
+ SleepingKingStudios::Tools::CoreTools.deprecate(
393
+ "#{self.class}#yield_result",
394
+ message: 'Use the #step method to compose commands.'
395
+ )
396
+
376
397
  chained_procs <<
377
398
  {
378
399
  proc: block,
@@ -1,5 +1,7 @@
1
1
  require 'cuprum/chaining'
2
+ require 'cuprum/currying'
2
3
  require 'cuprum/processing'
4
+ require 'cuprum/steps'
3
5
 
4
6
  module Cuprum
5
7
  # Functional object that encapsulates a business logic operation with a
@@ -111,7 +113,8 @@ module Cuprum
111
113
  # @see Cuprum::Processing
112
114
  class Command
113
115
  include Cuprum::Processing
114
- include Cuprum::Chaining
116
+ include Cuprum::Currying
117
+ include Cuprum::Steps
115
118
 
116
119
  # Returns a new instance of Cuprum::Command.
117
120
  #
@@ -126,5 +129,9 @@ module Cuprum
126
129
 
127
130
  singleton_class.send(:private, :process)
128
131
  end # method initialize
132
+
133
+ def call(*args, **kwargs, &block)
134
+ steps { super }
135
+ end
129
136
  end # class
130
137
  end # module
@@ -28,7 +28,7 @@ module Cuprum
28
28
  #
29
29
  # factory::Dream #=> DreamCommand
30
30
  # factory.dream #=> an instance of DreamCommand
31
- class CommandFactory < Module
31
+ class CommandFactory < Module # rubocop:disable Metrics/ClassLength
32
32
  # Defines the Domain-Specific Language and helper methods for dynamically
33
33
  # defined commands.
34
34
  class << self
@@ -149,18 +149,12 @@ module Cuprum
149
149
 
150
150
  raise ArgumentError, 'must provide a block'.freeze unless block_given?
151
151
 
152
- name = normalize_command_name(name)
152
+ method_name = normalize_command_name(name)
153
153
 
154
- (@command_definitions ||= {})[name] =
154
+ (@command_definitions ||= {})[method_name] =
155
155
  metadata.merge(__const_defn__: defn)
156
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
157
+ define_lazy_command_method(method_name)
164
158
  end
165
159
 
166
160
  protected
@@ -184,21 +178,47 @@ module Cuprum
184
178
 
185
179
  (@command_definitions ||= {})[command_name] = metadata
186
180
 
187
- define_method(command_name) do |*args|
188
- instance_exec(*args, &builder)
181
+ define_method(command_name) do |*args, **kwargs|
182
+ if kwargs.empty?
183
+ instance_exec(*args, &builder)
184
+ else
185
+ instance_exec(*args, **kwargs, &builder)
186
+ end
189
187
  end
190
188
  end
191
189
 
192
190
  def define_command_from_class(command_class, name:, metadata: {})
193
191
  guard_invalid_definition!(command_class)
194
192
 
195
- command_name = normalize_command_name(name)
193
+ method_name = normalize_command_name(name)
196
194
 
197
- (@command_definitions ||= {})[command_name] =
195
+ (@command_definitions ||= {})[method_name] =
198
196
  metadata.merge(__const_defn__: command_class)
199
197
 
200
- define_method(command_name) do |*args, &block|
201
- build_command(command_class, *args, &block)
198
+ define_command_method(method_name, command_class)
199
+ end
200
+
201
+ def define_command_method(method_name, command_class)
202
+ define_method(method_name) do |*args, **kwargs, &block|
203
+ if kwargs.empty?
204
+ build_command(command_class, *args, &block)
205
+ else
206
+ build_command(command_class, *args, **kwargs, &block)
207
+ end
208
+ end
209
+ end
210
+
211
+ def define_lazy_command_method(method_name)
212
+ const_name = tools.string_tools.camelize(method_name)
213
+
214
+ define_method(method_name) do |*args, **kwargs, &block|
215
+ command_class = const_get(const_name)
216
+
217
+ if kwargs.empty?
218
+ build_command(command_class, *args, &block)
219
+ else
220
+ build_command(command_class, *args, **kwargs, &block)
221
+ end
202
222
  end
203
223
  end
204
224
 
@@ -217,7 +237,7 @@ module Cuprum
217
237
  end
218
238
 
219
239
  def normalize_command_name(command_name)
220
- tools.string.underscore(command_name).intern
240
+ tools.string_tools.underscore(command_name).intern
221
241
  end
222
242
 
223
243
  def require_definition!
@@ -265,8 +285,12 @@ module Cuprum
265
285
 
266
286
  private
267
287
 
268
- def build_command(command_class, *args, &block)
269
- command_class.new(*args, &block)
288
+ def build_command(command_class, *args, **kwargs, &block)
289
+ if kwargs.empty?
290
+ command_class.new(*args, &block)
291
+ else
292
+ command_class.new(*args, **kwargs, &block)
293
+ end
270
294
  end
271
295
 
272
296
  def normalize_command_name(command_name)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ module Cuprum
6
+ # Implements partial application for command objects.
7
+ #
8
+ # Partial application (more commonly referred to, if imprecisely, as currying)
9
+ # refers to fixing some number of arguments to a function, resulting in a
10
+ # function with a smaller number of arguments.
11
+ #
12
+ # In Cuprum's case, a curried (partially applied) command takes an original
13
+ # command and pre-defines some of its arguments. When the curried command is
14
+ # called, the predefined arguments and/or keywords will be combined with the
15
+ # arguments passed to #call.
16
+ #
17
+ # @example Currying Arguments
18
+ # # Our base command takes two arguments.
19
+ # say_command = Cuprum::Command.new do |greeting, person|
20
+ # "#{greeting}, #{person}!"
21
+ # end
22
+ # say_command.call('Hello', 'world')
23
+ # #=> returns a result with value 'Hello, world!'
24
+ #
25
+ # # Next, we create a curried command. This sets the first argument to
26
+ # # always be 'Greetings', so our curried command only takes one argument,
27
+ # # namely the name of the person being greeted.
28
+ # greet_command = say_command.curry('Greetings')
29
+ # greet_command.call('programs')
30
+ # #=> returns a result with value 'Greetings, programs!'
31
+ #
32
+ # # Here, we are creating a curried command that passes both arguments.
33
+ # # Therefore, our curried command does not take any arguments.
34
+ # recruit_command = say_command.curry('Greetings', 'starfighter')
35
+ # recruit_command.call
36
+ # #=> returns a result with value 'Greetings, starfighter!'
37
+ #
38
+ # @example Currying Keywords
39
+ # # Our base command takes two keywords: a math operation and an array of
40
+ # # integers.
41
+ # math_command = Cuprum::Command.new do |operands:, operation:|
42
+ # operations.reduce(&operation)
43
+ # end
44
+ # math_command.call(operands: [2, 2], operation: :+)
45
+ # #=> returns a result with value 4
46
+ #
47
+ # # Our curried command still takes two keywords, but now the operation
48
+ # # keyword is optional. It now defaults to :*, for multiplication.
49
+ # multiply_command = math_command.curry(operation: :*)
50
+ # multiply_command.call(operands: [3, 3])
51
+ # #=> returns a result with value 9
52
+ module Currying
53
+ autoload :CurriedCommand, 'cuprum/currying/curried_command'
54
+
55
+ # Returns a CurriedCommand that wraps this command with pre-set arguments.
56
+ #
57
+ # When the curried command is called, the predefined arguments and/or
58
+ # keywords will be combined with the arguments passed to #call.
59
+ #
60
+ # The original command is unchanged.
61
+ #
62
+ # @param arguments [Array] The arguments to pass to the curried command.
63
+ # @param keywords [Hash] The keywords to pass to the curried command.
64
+ #
65
+ # @return [Cuprum::Currying::CurriedCommand] the curried command.
66
+ #
67
+ # @see Cuprum::Currying::CurriedCommand#call
68
+ def curry(*arguments, **keywords)
69
+ return self if arguments.empty? && keywords.empty?
70
+
71
+ Cuprum::Currying::CurriedCommand.new(
72
+ arguments: arguments,
73
+ command: self,
74
+ keywords: keywords
75
+ )
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/currying'
4
+
5
+ module Cuprum::Currying
6
+ # A CurriedCommand wraps another command and passes preset args to #call.
7
+ #
8
+ # @example Currying Arguments
9
+ # # Our base command takes two arguments.
10
+ # say_command = Cuprum::Command.new do |greeting, person|
11
+ # "#{greeting}, #{person}!"
12
+ # end
13
+ # say_command.call('Hello', 'world')
14
+ # #=> returns a result with value 'Hello, world!'
15
+ #
16
+ # # Next, we create a curried command. This sets the first argument to
17
+ # # always be 'Greetings', so our curried command only takes one argument,
18
+ # # namely the name of the person being greeted.
19
+ # greet_command =
20
+ # Cuprum::CurriedCommand.new(
21
+ # arguments: ['Greetings'],
22
+ # command: say_command
23
+ # )
24
+ # greet_command.call('programs')
25
+ # #=> returns a result with value 'Greetings, programs!'
26
+ #
27
+ # # Here, we are creating a curried command that passes both arguments.
28
+ # # Therefore, our curried command does not take any arguments.
29
+ # recruit_command =
30
+ # Cuprum::CurriedCommand.new(
31
+ # arguments: ['Greetings', 'starfighter'],
32
+ # command: say_command
33
+ # )
34
+ # recruit_command.call
35
+ # #=> returns a result with value 'Greetings, starfighter!'
36
+ #
37
+ # @example Currying Keywords
38
+ # # Our base command takes two keywords: a math operation and an array of
39
+ # # integers.
40
+ # math_command = Cuprum::Command.new do |operands:, operation:|
41
+ # operations.reduce(&operation)
42
+ # end
43
+ # math_command.call(operands: [2, 2], operation: :+)
44
+ # #=> returns a result with value 4
45
+ #
46
+ # # Our curried command still takes two keywords, but now the operation
47
+ # # keyword is optional. It now defaults to :*, for multiplication.
48
+ # multiply_command =
49
+ # Cuprum::CurriedCommand.new(
50
+ # command: math_command,
51
+ # keywords: { operation: :* }
52
+ # )
53
+ # multiply_command.call(operands: [3, 3])
54
+ # #=> returns a result with value 9
55
+ class CurriedCommand < Cuprum::Command
56
+ # @param arguments [Array] The arguments to pass to the curried command.
57
+ # @param command [Cuprum::Command] The original command to curry.
58
+ # @param keywords [Hash] The keywords to pass to the curried command.
59
+ def initialize(arguments: [], command:, keywords: {})
60
+ @arguments = arguments
61
+ @command = command
62
+ @keywords = keywords
63
+ end
64
+
65
+ # @!method call(*args, **kwargs)
66
+ # Merges the arguments and keywords and calls the wrapped command.
67
+ #
68
+ # First, the arguments array is created starting with the :arguments
69
+ # passed to #initialize. Any positional arguments passed directly to #call
70
+ # are then appended.
71
+ #
72
+ # Second, the keyword arguments are created by merging the keywords passed
73
+ # directly into #call into the keywods passed to #initialize. This means
74
+ # that if a key is passed in both places, the value passed into #call will
75
+ # take precedence.
76
+ #
77
+ # Finally, the merged arguments and keywords are passed into the original
78
+ # command's #call method.
79
+ #
80
+ # @param args [Array] Additional arguments to pass to the curried command.
81
+ # @param kwargs [Hash] Additional keywords to pass to the curried command.
82
+ #
83
+ # @return [Cuprum::Result]
84
+ #
85
+ # @see Cuprum::Processing#call
86
+
87
+ # @return [Array] the arguments to pass to the curried command.
88
+ attr_reader :arguments
89
+
90
+ # @return [Cuprum::Command] the original command to curry.
91
+ attr_reader :command
92
+
93
+ # @return [Hash] the keywords to pass to the curried command.
94
+ attr_reader :keywords
95
+
96
+ private
97
+
98
+ def process(*args, **kwargs, &block)
99
+ args = [*arguments, *args]
100
+ kwargs = keywords.merge(kwargs)
101
+
102
+ if kwargs.empty?
103
+ command.call(*args, &block)
104
+ else
105
+ command.call(*args, **kwargs, &block)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -62,7 +62,7 @@ module Cuprum
62
62
  # implementation.
63
63
  #
64
64
  # @see Cuprum::Command#call
65
- def call *args, &block
65
+ def call *args, **kwargs, &block
66
66
  reset! if called? # Clear reference to most recent result.
67
67
 
68
68
  @result = super
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cuprum/errors/command_not_implemented'
4
+ require 'cuprum/result_helpers'
4
5
 
5
6
  module Cuprum
6
7
  # Functional implementation for creating a command object. Cuprum::Processing
@@ -57,6 +58,8 @@ module Cuprum
57
58
  #
58
59
  # @see Cuprum::Command
59
60
  module Processing
61
+ include Cuprum::ResultHelpers
62
+
60
63
  # Returns a nonnegative integer for commands that take a fixed number of
61
64
  # arguments. For commands that take a variable number of arguments, returns
62
65
  # -n-1, where n is the number of required arguments.
@@ -87,8 +90,13 @@ module Cuprum
87
90
  #
88
91
  # @yield If a block argument is given, it will be passed to the
89
92
  # implementation.
90
- def call(*args, &block)
91
- value = process(*args, &block)
93
+ def call(*args, **kwargs, &block)
94
+ value =
95
+ if kwargs.empty?
96
+ process(*args, &block)
97
+ else
98
+ process(*args, **kwargs, &block)
99
+ end
92
100
 
93
101
  return value.to_cuprum_result if value_is_result?(value)
94
102
 
@@ -97,14 +105,6 @@ module Cuprum
97
105
 
98
106
  private
99
107
 
100
- def build_result(error: nil, status: nil, value: nil)
101
- Cuprum::Result.new(error: error, status: status, value: value)
102
- end
103
-
104
- def failure(error)
105
- build_result(error: error)
106
- end
107
-
108
108
  # @!visibility public
109
109
  # @overload process(*arguments, **keywords, &block)
110
110
  # The implementation of the command, to be executed when the #call method
@@ -128,10 +128,6 @@ module Cuprum
128
128
  build_result(error: error)
129
129
  end
130
130
 
131
- def success(value)
132
- build_result(value: value)
133
- end
134
-
135
131
  def value_is_result?(value)
136
132
  value.respond_to?(:to_cuprum_result)
137
133
  end