cuprum 0.8.0 → 0.10.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.
@@ -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