cuprum 0.7.0 → 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.
@@ -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,6 @@
1
+ require 'cuprum'
2
+
3
+ module Cuprum
4
+ # Namespace for custom Cuprum error classes.
5
+ module Errors; end
6
+ 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
@@ -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 trying to access the result of an uncalled
8
+ # Operation.
9
+ class OperationNotCalled < Cuprum::Error
10
+ COMPARABLE_PROPERTIES = %i[operation].freeze
11
+ private_constant :COMPARABLE_PROPERTIES
12
+
13
+ # Format for generating error message.
14
+ MESSAGE_FORMAT = '%s was not called and does not have a result'
15
+
16
+ # @param operation [Cuprum::Operation] The uncalled operation.
17
+ def initialize(operation:)
18
+ @operation = operation
19
+
20
+ class_name = operation&.class&.name || 'operation'
21
+ message = MESSAGE_FORMAT % class_name
22
+
23
+ super(message: message)
24
+ end
25
+
26
+ # @return [Cuprum::Operation] The uncalled operation.
27
+ attr_reader :operation
28
+
29
+ private
30
+
31
+ def comparable_properties
32
+ COMPARABLE_PROPERTIES
33
+ end
34
+ end
35
+ end
@@ -1,4 +1,5 @@
1
1
  require 'cuprum/command'
2
+ require 'cuprum/errors/operation_not_called'
2
3
 
3
4
  module Cuprum
4
5
  # Functional object with syntactic sugar for tracking the last result.
@@ -45,7 +46,6 @@ module Cuprum
45
46
  # @return [Cuprum::Result] The result from the most recent call of the
46
47
  # operation.
47
48
  attr_reader :result
48
- alias_method :to_result, :result
49
49
 
50
50
  # @overload call(*arguments, **keywords, &block)
51
51
  # Executes the logic encoded in the constructor block, or the #process
@@ -61,12 +61,8 @@ module Cuprum
61
61
  # @yield If a block argument is given, it will be passed to the
62
62
  # implementation.
63
63
  #
64
- # @raise [Cuprum::NotImplementedError] Unless a block was passed to the
65
- # constructor or the #process method was overriden by a Command
66
- # subclass.
67
- #
68
64
  # @see Cuprum::Command#call
69
- def call *args, &block
65
+ def call *args, **kwargs, &block
70
66
  reset! if called? # Clear reference to most recent result.
71
67
 
72
68
  @result = super
@@ -80,23 +76,18 @@ module Cuprum
80
76
  !result.nil?
81
77
  end # method called?
82
78
 
83
- # @return [Array] the errors from the most recent result, or nil if the
84
- # operation has not been called.
85
- def errors
86
- called? ? result.errors : nil
87
- end # method errors
79
+ # @return [Object] the error (if any) from the most recent result, or nil
80
+ # if the operation has not been called.
81
+ def error
82
+ called? ? result.error : nil
83
+ end # method error
88
84
 
89
- # @return [Boolean] true if the most recent result had errors, or false if
90
- # the most recent result had no errors or if the operation has not been
91
- # called.
85
+ # @return [Boolean] true if the most recent result had an error, or false
86
+ # if the most recent result had no error or if the operation has not
87
+ # been called.
92
88
  def failure?
93
89
  called? ? result.failure? : false
94
- end # method success?
95
-
96
- # @return [Boolean] true if the most recent was halted, otherwise false.
97
- def halted?
98
- called? ? result.halted? : false
99
- end # method halted?
90
+ end # method failure?
100
91
 
101
92
  # Clears the reference to the most recent call of the operation, if any.
102
93
  # This allows the result and any referenced data to be garbage collected.
@@ -110,13 +101,31 @@ module Cuprum
110
101
  @result = nil
111
102
  end # method reset
112
103
 
113
- # @return [Boolean] true if the most recent result had no errors, or false
114
- # if the most recent result had errors or if the operation has not been
115
- # called.
104
+ # @return [Symbol, nil] the status of the most recent result, or nil if
105
+ # the operation has not been called.
106
+ def status
107
+ called? ? result.status : nil
108
+ end
109
+
110
+ # @return [Boolean] true if the most recent result had no error, or false
111
+ # if the most recent result had an error or if the operation has not
112
+ # been called.
116
113
  def success?
117
114
  called? ? result.success? : false
118
115
  end # method success?
119
116
 
117
+ # Returns the most result if the operation was previously called.
118
+ # Otherwise, returns a failing result.
119
+ #
120
+ # @return [Cuprum::Result] the most recent result or failing result.
121
+ def to_cuprum_result
122
+ return result if result
123
+
124
+ error = Cuprum::Errors::OperationNotCalled.new(operation: self)
125
+
126
+ Cuprum::Result.new(error: error)
127
+ end
128
+
120
129
  # @return [Object] the value of the most recent result, or nil if the
121
130
  # operation has not been called.
122
131
  def value
@@ -131,15 +140,12 @@ module Cuprum
131
140
  # @!method called?
132
141
  # (see Cuprum::Operation::Mixin#called?)
133
142
 
134
- # @!method errors
135
- # (see Cuprum::Operation::Mixin#errors)
143
+ # @!method error
144
+ # (see Cuprum::Operation::Mixin#error)
136
145
 
137
146
  # @!method failure?
138
147
  # (see Cuprum::Operation::Mixin#failure?)
139
148
 
140
- # @!method halted?
141
- # (see Cuprum::Operation::Mixin#halted?)
142
-
143
149
  # @!method reset!
144
150
  # (see Cuprum::Operation::Mixin#reset!)
145
151
 
@@ -149,6 +155,9 @@ module Cuprum
149
155
  # @!method success?
150
156
  # (see Cuprum::Operation::Mixin#success?)
151
157
 
158
+ # @!method to_cuprum_result
159
+ # (see Cuprum::Operation::Mixin#to_cuprum_result?)
160
+
152
161
  # @!method value
153
162
  # (see Cuprum::Operation::Mixin#value)
154
163
  end # class
@@ -1,5 +1,7 @@
1
- require 'cuprum/not_implemented_error'
2
- require 'cuprum/utils/result_not_empty_warning'
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/errors/command_not_implemented'
4
+ require 'cuprum/result_helpers'
3
5
 
4
6
  module Cuprum
5
7
  # Functional implementation for creating a command object. Cuprum::Processing
@@ -12,14 +14,14 @@ module Cuprum
12
14
  #
13
15
  # def initialize addend
14
16
  # @addend = addend
15
- # end # constructor
17
+ # end
16
18
  #
17
19
  # private
18
20
  #
19
21
  # def process int
20
22
  # int + addend
21
- # end # method process
22
- # end # class AdderCommand
23
+ # end
24
+ # end
23
25
  #
24
26
  # adder = AdderCommand.new(2)
25
27
  # result = adder.call(3)
@@ -35,31 +37,28 @@ module Cuprum
35
37
  #
36
38
  # def process value
37
39
  # if value.negative?
38
- # result.errors << 'value cannot be negative'
39
- #
40
- # return nil
41
- # end # if
40
+ # return Cuprum::Result.new(error: 'value cannot be negative')
41
+ # end
42
42
  #
43
43
  # Math.sqrt(value)
44
- # end # method process
45
- # end # class
44
+ # end
45
+ # end
46
46
  #
47
47
  # result = SquareRootCommand.new.call(2)
48
48
  # result.value #=> 1.414
49
49
  # result.success? #=> true
50
50
  # result.failure? #=> false
51
- # result.errors #=> []
51
+ # result.error #=> nil
52
52
  #
53
53
  # result = SquareRootCommand.new.call(-1)
54
54
  # result.value #=> nil
55
55
  # result.success? #=> false
56
56
  # result.failure? #=> true
57
- # result.errors #=> ['value cannot be negative']
57
+ # result.error #=> 'value cannot be negative'
58
58
  #
59
59
  # @see Cuprum::Command
60
60
  module Processing
61
- VALUE_METHODS = %i[to_result value success?].freeze
62
- private_constant :VALUE_METHODS
61
+ include Cuprum::ResultHelpers
63
62
 
64
63
  # Returns a nonnegative integer for commands that take a fixed number of
65
64
  # arguments. For commands that take a variable number of arguments, returns
@@ -68,7 +67,7 @@ module Cuprum
68
67
  # @return [Integer] The number of arguments.
69
68
  def arity
70
69
  method(:process).arity
71
- end # method arity
70
+ end
72
71
 
73
72
  # @overload call(*arguments, **keywords, &block)
74
73
  # Executes the command implementation and returns a Cuprum::Result or
@@ -76,14 +75,12 @@ module Cuprum
76
75
  #
77
76
  # Each time #call is invoked, the object performs the following steps:
78
77
  #
79
- # 1. Creates a result object, typically an instance of Cuprum::Result.
80
- # The result is assigned to the command as the private #result reader.
81
- # 2. The #process method is called, passing the arguments, keywords, and
82
- # block that were passed to #call. The #process method can set errors,
83
- # set the status, or halt the result via the #result reader method.
84
- # 3. If #process returns a result, that result is returned by #call.
85
- # Otherwise, the return value of #process is assigned to the #value
86
- # property of the result, and the result is returned by #call.
78
+ # 1. The #process method is called, passing the arguments, keywords, and
79
+ # block that were passed to #call.
80
+ # 2. If the value returned by #process is a Cuprum::Result or compatible
81
+ # object, that result is directly returned by #call.
82
+ # 3. Otherwise, the value returned by #process will be wrapped in a
83
+ # successful result, which will be returned by #call.
87
84
  #
88
85
  # @param arguments [Array] Arguments to be passed to the implementation.
89
86
  #
@@ -93,57 +90,28 @@ module Cuprum
93
90
  #
94
91
  # @yield If a block argument is given, it will be passed to the
95
92
  # implementation.
96
- #
97
- # @raise [Cuprum::NotImplementedError] Unless the #process method was
98
- # overriden.
99
- def call *args, &block
100
- result = build_result(nil, :errors => build_errors)
101
-
102
- process_with_result(result, *args, &block)
103
- end # method call
104
-
105
- private
106
-
107
- # @return [Cuprum::Result] The current result. Only available while #process
108
- # is being called.
109
- attr_reader :result
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
110
100
 
111
- # @!visibility public
112
- #
113
- # Generates an empty errors object. When the command is called, the result
114
- # will have its #errors property initialized to the value returned by
115
- # #build_errors. By default, this is an array. If you want to use a custom
116
- # errors object type, override this method in a subclass.
117
- #
118
- # @return [Array] An empty errors object.
119
- def build_errors
120
- []
121
- end # method build_errors
101
+ return value.to_cuprum_result if value_is_result?(value)
122
102
 
123
- def build_result value, errors:
124
- Cuprum::Result.new(value, :errors => errors)
125
- end # method build_result
103
+ build_result(value: value)
104
+ end
126
105
 
127
- def merge_results result, other
128
- if value_is_result?(other)
129
- return result if result == other
130
-
131
- warn_unless_empty!(result)
132
-
133
- other.to_result
134
- else
135
- result.value = other
136
-
137
- result
138
- end # if-else
139
- end # method merge_results
106
+ private
140
107
 
141
108
  # @!visibility public
142
109
  # @overload process(*arguments, **keywords, &block)
143
110
  # The implementation of the command, to be executed when the #call method
144
- # is called. Can add errors to or set the status of the result, and the
145
- # value of the result will be set to the value returned by #process. Do
146
- # not call this method directly.
111
+ # is called. If #process returns a result, that result will be returned by
112
+ # #call; otherwise, the value returned by #process will be wrapped in a
113
+ # successful Cuprum::Result object. This method should not be called
114
+ # directly.
147
115
  #
148
116
  # @param arguments [Array] The arguments, if any, passed from #call.
149
117
  #
@@ -153,34 +121,15 @@ module Cuprum
153
121
  #
154
122
  # @return [Object] the value of the result object to be returned by #call.
155
123
  #
156
- # @raise [Cuprum::NotImplementedError] Unless a block was passed to the
157
- # constructor or the #process method was overriden by a Command
158
- # subclass.
159
- #
160
124
  # @note This is a private method.
161
- def process *_args
162
- raise Cuprum::NotImplementedError, nil, caller(1..-1)
163
- end # method process
164
-
165
- def process_with_result result, *args, &block
166
- @result = result
167
- value = process(*args, &block)
168
-
169
- merge_results(result, value)
170
- ensure
171
- @result = nil
172
- end # method process_with_result
173
-
174
- def value_is_result? value
175
- VALUE_METHODS.all? { |method_name| value.respond_to?(method_name) }
176
- end # method value
177
-
178
- def warn_unless_empty! result
179
- return unless result.respond_to?(:empty?) && !result.empty?
125
+ def process(*_args)
126
+ error = Cuprum::Errors::CommandNotImplemented.new(command: self)
180
127
 
181
- not_empty = Cuprum::Utils::ResultNotEmptyWarning.new(result)
128
+ build_result(error: error)
129
+ end
182
130
 
183
- Cuprum.warn(not_empty.message) if not_empty.warning?
184
- end # method warn_unless_empty!
185
- end # module
186
- end # module
131
+ def value_is_result?(value)
132
+ value.respond_to?(:to_cuprum_result)
133
+ end
134
+ end
135
+ end