cuprum 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -32,7 +34,7 @@ module Cuprum
32
34
  #
33
35
  # result.value #=> 15
34
36
  #
35
- # @example A Command with errors
37
+ # @example A Command with an error state
36
38
  # class DivideCommand < Cuprum::Command
37
39
  # def initialize divisor
38
40
  # @divisor = divisor
@@ -42,10 +44,8 @@ module Cuprum
42
44
  #
43
45
  # def process int
44
46
  # if @divisor.zero?
45
- # errors << 'errors.messages.divide_by_zero'
46
- #
47
- # return
48
- # end # if
47
+ # return Cuprum::Result.new(error: 'errors.messages.divide_by_zero')
48
+ # end
49
49
  #
50
50
  # int / @divisor
51
51
  # end # method process
@@ -54,14 +54,14 @@ module Cuprum
54
54
  # halve_command = DivideCommand.new(2)
55
55
  # result = halve_command.call(10)
56
56
  #
57
- # result.errors #=> []
58
- # result.value #=> 5
57
+ # result.error #=> nil
58
+ # result.value #=> 5
59
59
  #
60
- # command_with_errors = DivideCommand.new(0)
61
- # result = command_with_errors.call(10)
60
+ # divide_command = DivideCommand.new(0)
61
+ # result = divide_command.call(10)
62
62
  #
63
- # result.errors #=> ['errors.messages.divide_by_zero']
64
- # result.value #=> nil
63
+ # result.error #=> 'errors.messages.divide_by_zero'
64
+ # result.value #=> nil
65
65
  #
66
66
  # @example Command Chaining
67
67
  # class AddCommand < Cuprum::Command
@@ -86,9 +86,9 @@ module Cuprum
86
86
  # private
87
87
  #
88
88
  # def process int
89
- # errors << 'errors.messages.not_even' unless int.even?
89
+ # return int if int.even?
90
90
  #
91
- # int
91
+ # Cuprum::Errors.new(error: 'errors.messages.not_even')
92
92
  # end # method process
93
93
  # end # class
94
94
  #
@@ -113,7 +113,8 @@ module Cuprum
113
113
  # @see Cuprum::Processing
114
114
  class Command
115
115
  include Cuprum::Processing
116
- include Cuprum::Chaining
116
+ include Cuprum::Currying
117
+ include Cuprum::Steps
117
118
 
118
119
  # Returns a new instance of Cuprum::Command.
119
120
  #
@@ -122,7 +123,15 @@ module Cuprum
122
123
  # value of the block. This overrides the implementation in #process, if
123
124
  # any.
124
125
  def initialize &implementation
125
- define_singleton_method :process, &implementation if implementation
126
+ return unless implementation
127
+
128
+ define_singleton_method :process, &implementation
129
+
130
+ singleton_class.send(:private, :process)
126
131
  end # method initialize
132
+
133
+ def call(*args, **kwargs, &block)
134
+ steps { super }
135
+ end
127
136
  end # class
128
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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ module Cuprum
6
+ # Wrapper class for encapsulating an error state for a failed Cuprum result.
7
+ # Additional details can be passed by setting the #message or by using a
8
+ # subclass of Cuprum::Error.
9
+ class Error
10
+ COMPARABLE_PROPERTIES = %i[message].freeze
11
+ private_constant :COMPARABLE_PROPERTIES
12
+
13
+ # @param message [String] Optional message describing the nature of the
14
+ # error.
15
+ def initialize(message: nil)
16
+ @message = message
17
+ end
18
+
19
+ # @return [String] Optional message describing the nature of the error.
20
+ attr_reader :message
21
+
22
+ # @param other [Cuprum::Error] The other object to compare.
23
+ #
24
+ # @return [Boolean] true if the other object has the same class and message;
25
+ # otherwise false.
26
+ def ==(other)
27
+ other.instance_of?(self.class) &&
28
+ comparable_properties.all? { |prop| send(prop) == other.send(prop) }
29
+ end
30
+
31
+ private
32
+
33
+ def comparable_properties
34
+ self.class.const_get(:COMPARABLE_PROPERTIES)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/error'
4
+ require 'cuprum/errors'
5
+
6
+ module Cuprum::Errors
7
+ # Error class to be used when a Command is called without defining a #process
8
+ # method.
9
+ class CommandNotImplemented < Cuprum::Error
10
+ COMPARABLE_PROPERTIES = %i[command].freeze
11
+ private_constant :COMPARABLE_PROPERTIES
12
+
13
+ # Format for generating error message.
14
+ MESSAGE_FORMAT = 'no implementation defined for %s'
15
+
16
+ # @param command [Cuprum::Command] The command called without a definition.
17
+ def initialize(command:)
18
+ @command = command
19
+
20
+ class_name = command&.class&.name || 'command'
21
+ message = MESSAGE_FORMAT % class_name
22
+
23
+ super(message: message)
24
+ end
25
+
26
+ # @return [Cuprum::Command] The command called without a definition.
27
+ attr_reader :command
28
+
29
+ private
30
+
31
+ def comparable_properties
32
+ COMPARABLE_PROPERTIES
33
+ end
34
+ end
35
+ end