cuprum 0.9.1 → 0.10.0.rc.0

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