cuprum 0.10.0 → 1.0.0.rc.1

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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum/built_in'
2
4
  require 'cuprum/command'
3
5
 
@@ -24,8 +26,8 @@ module Cuprum::BuiltIn
24
26
  class IdentityCommand < Cuprum::Command
25
27
  private
26
28
 
27
- def process value = nil
29
+ def process(value = nil)
28
30
  value
29
- end # method process
30
- end # class
31
- end # module
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum/built_in/identity_command'
2
4
  require 'cuprum/operation'
3
5
 
@@ -23,5 +25,5 @@ module Cuprum::BuiltIn
23
25
  # #=> ['errors.messages.unknown']
24
26
  class IdentityOperation < Cuprum::BuiltIn::IdentityCommand
25
27
  include Cuprum::Operation::Mixin
26
- end # class
27
- end # module
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum/built_in'
2
4
  require 'cuprum/command'
3
5
 
@@ -13,6 +15,6 @@ module Cuprum::BuiltIn
13
15
  class NullCommand < Cuprum::Command
14
16
  private
15
17
 
16
- def process *_args; end
17
- end # class
18
- end # module
18
+ def process(*_args); end
19
+ end
20
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum/built_in/null_command'
2
4
  require 'cuprum/operation'
3
5
 
@@ -12,5 +14,5 @@ module Cuprum::BuiltIn
12
14
  # #=> true
13
15
  class NullOperation < Cuprum::BuiltIn::NullCommand
14
16
  include Cuprum::Operation::Mixin
15
- end # class
16
- end # module
17
+ end
18
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum'
2
4
 
3
5
  module Cuprum
4
6
  # Namespace for predefined command and operation classes.
5
7
  module BuiltIn; end
6
- end # module
8
+ end
@@ -1,4 +1,5 @@
1
- require 'cuprum/chaining'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'cuprum/currying'
3
4
  require 'cuprum/processing'
4
5
  require 'cuprum/steps'
@@ -20,14 +21,14 @@ module Cuprum
20
21
  # class MultiplyCommand < Cuprum::Command
21
22
  # def initialize multiplier
22
23
  # @multiplier = multiplier
23
- # end # constructor
24
+ # end
24
25
  #
25
26
  # private
26
27
  #
27
28
  # def process int
28
29
  # int * @multiplier
29
- # end # method process
30
- # end # class
30
+ # end
31
+ # end
31
32
  #
32
33
  # triple_command = MultiplyCommand.new(3)
33
34
  # result = command_command.call(5)
@@ -38,7 +39,7 @@ module Cuprum
38
39
  # class DivideCommand < Cuprum::Command
39
40
  # def initialize divisor
40
41
  # @divisor = divisor
41
- # end # constructor
42
+ # end
42
43
  #
43
44
  # private
44
45
  #
@@ -48,8 +49,8 @@ module Cuprum
48
49
  # end
49
50
  #
50
51
  # int / @divisor
51
- # end # method process
52
- # end # class
52
+ # end
53
+ # end
53
54
  #
54
55
  # halve_command = DivideCommand.new(2)
55
56
  # result = halve_command.call(10)
@@ -63,53 +64,6 @@ module Cuprum
63
64
  # result.error #=> 'errors.messages.divide_by_zero'
64
65
  # result.value #=> nil
65
66
  #
66
- # @example Command Chaining
67
- # class AddCommand < Cuprum::Command
68
- # def initialize addend
69
- # @addend = addend
70
- # end # constructor
71
- #
72
- # private
73
- #
74
- # def process int
75
- # int + @addend
76
- # end # method process
77
- # end # class
78
- #
79
- # double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
80
- # result = double_and_add_one(5)
81
- #
82
- # result.value #=> 5
83
- #
84
- # @example Conditional Chaining
85
- # class EvenCommand < Cuprum::Command
86
- # private
87
- #
88
- # def process int
89
- # return int if int.even?
90
- #
91
- # Cuprum::Errors.new(error: 'errors.messages.not_even')
92
- # end # method process
93
- # end # class
94
- #
95
- # # The next step in a Collatz sequence is determined as follows:
96
- # # - If the number is even, divide it by 2.
97
- # # - If the number is odd, multiply it by 3 and add 1.
98
- # collatz_command =
99
- # EvenCommand.new.
100
- # chain(DivideCommand.new(2), :on => :success).
101
- # chain(
102
- # MultiplyCommand.new(3).chain(AddCommand.new(1),
103
- # :on => :failure
104
- # )
105
- #
106
- # result = collatz_command.new(5)
107
- # result.value #=> 16
108
- #
109
- # result = collatz_command.new(16)
110
- # result.value #=> 8
111
- #
112
- # @see Cuprum::Chaining
113
67
  # @see Cuprum::Processing
114
68
  class Command
115
69
  include Cuprum::Processing
@@ -122,16 +76,33 @@ module Cuprum
122
76
  # #call method will wrap the block and set the result #value to the return
123
77
  # value of the block. This overrides the implementation in #process, if
124
78
  # any.
125
- def initialize &implementation
79
+ def initialize(&implementation)
126
80
  return unless implementation
127
81
 
128
82
  define_singleton_method :process, &implementation
129
83
 
130
84
  singleton_class.send(:private, :process)
131
- end # method initialize
85
+ end
132
86
 
87
+ # (see Cuprum::Processing#call)
133
88
  def call(*args, **kwargs, &block)
134
89
  steps { super }
135
90
  end
136
- end # class
137
- end # module
91
+
92
+ # Wraps the command in a proc.
93
+ #
94
+ # Calling the proc will call the command with the given arguments, keywords,
95
+ # and block.
96
+ #
97
+ # @return [Proc] the wrapping proc.
98
+ def to_proc
99
+ @to_proc ||= lambda do |*args, **kwargs, &block|
100
+ if kwargs.empty?
101
+ call(*args, &block)
102
+ else
103
+ call(*args, **kwargs, &block)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum'
2
4
 
3
5
  require 'sleeping_king_studios/tools/toolbelt'
@@ -147,7 +149,7 @@ module Cuprum
147
149
  def command_class(name, **metadata, &defn)
148
150
  guard_abstract_factory!
149
151
 
150
- raise ArgumentError, 'must provide a block'.freeze unless block_given?
152
+ raise ArgumentError, 'must provide a block' unless block_given?
151
153
 
152
154
  method_name = normalize_command_name(name)
153
155
 
@@ -227,13 +229,13 @@ module Cuprum
227
229
 
228
230
  raise NotImplementedError,
229
231
  'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
230
- 'define commands for a factory.'.freeze
232
+ 'define commands for a factory.'
231
233
  end
232
234
 
233
235
  def guard_invalid_definition!(command_class)
234
236
  return if command_class.is_a?(Class) && command_class < Cuprum::Command
235
237
 
236
- raise ArgumentError, 'definition must be a command class'.freeze
238
+ raise ArgumentError, 'definition must be a command class'
237
239
  end
238
240
 
239
241
  def normalize_command_name(command_name)
@@ -241,7 +243,7 @@ module Cuprum
241
243
  end
242
244
 
243
245
  def require_definition!
244
- raise ArgumentError, 'must provide a command class or a block'.freeze
246
+ raise ArgumentError, 'must provide a command class or a block'
245
247
  end
246
248
 
247
249
  def tools
@@ -263,7 +265,7 @@ module Cuprum
263
265
  end
264
266
 
265
267
  # @private
266
- def const_defined?(const_name, inherit = true)
268
+ def const_defined?(const_name, inherit = true) # rubocop:disable Style/OptionalBooleanParameter
267
269
  command?(const_name) || super
268
270
  end
269
271
 
@@ -56,8 +56,12 @@ module Cuprum::Currying
56
56
  # @param arguments [Array] The arguments to pass to the curried command.
57
57
  # @param command [Cuprum::Command] The original command to curry.
58
58
  # @param keywords [Hash] The keywords to pass to the curried command.
59
- def initialize(arguments: [], command:, keywords: {})
59
+ # @yield A block to pass to the curried command.
60
+ def initialize(command:, arguments: [], block: nil, keywords: {})
61
+ super()
62
+
60
63
  @arguments = arguments
64
+ @block = block
61
65
  @command = command
62
66
  @keywords = keywords
63
67
  end
@@ -87,6 +91,9 @@ module Cuprum::Currying
87
91
  # @return [Array] the arguments to pass to the curried command.
88
92
  attr_reader :arguments
89
93
 
94
+ # @return [Proc, nil] a block to pass to the curried command.
95
+ attr_reader :block
96
+
90
97
  # @return [Cuprum::Command] the original command to curry.
91
98
  attr_reader :command
92
99
 
@@ -95,14 +102,14 @@ module Cuprum::Currying
95
102
 
96
103
  private
97
104
 
98
- def process(*args, **kwargs, &block)
105
+ def process(*args, **kwargs, &override)
99
106
  args = [*arguments, *args]
100
107
  kwargs = keywords.merge(kwargs)
101
108
 
102
109
  if kwargs.empty?
103
- command.call(*args, &block)
110
+ command.call(*args, &(override || block))
104
111
  else
105
- command.call(*args, **kwargs, &block)
112
+ command.call(*args, **kwargs, &(override || block))
106
113
  end
107
114
  end
108
115
  end
@@ -65,11 +65,12 @@ module Cuprum
65
65
  # @return [Cuprum::Currying::CurriedCommand] the curried command.
66
66
  #
67
67
  # @see Cuprum::Currying::CurriedCommand#call
68
- def curry(*arguments, **keywords)
69
- return self if arguments.empty? && keywords.empty?
68
+ def curry(*arguments, **keywords, &block)
69
+ return self if arguments.empty? && keywords.empty? && block.nil?
70
70
 
71
71
  Cuprum::Currying::CurriedCommand.new(
72
72
  arguments: arguments,
73
+ block: block,
73
74
  command: self,
74
75
  keywords: keywords
75
76
  )
data/lib/cuprum/error.rb CHANGED
@@ -4,34 +4,68 @@ require 'cuprum'
4
4
 
5
5
  module Cuprum
6
6
  # Wrapper class for encapsulating an error state for a failed Cuprum result.
7
+ #
7
8
  # Additional details can be passed by setting the #message or by using a
8
9
  # subclass of Cuprum::Error.
9
10
  class Error
10
- COMPARABLE_PROPERTIES = %i[message].freeze
11
- private_constant :COMPARABLE_PROPERTIES
11
+ # Short string used to identify the type of error.
12
+ #
13
+ # Primarily used for serialization. This value can be overriden by passing
14
+ # in the :type parameter to the constructor.
15
+ #
16
+ # Subclasses of Cuprum::Error should define their own default TYPE constant.
17
+ TYPE = 'cuprum.error'
12
18
 
13
19
  # @param message [String] Optional message describing the nature of the
14
20
  # error.
15
- def initialize(message: nil)
16
- @message = message
21
+ # @param properties [Hash] Additional properties used to compare errors.
22
+ # @param type [String] Short string used to identify the type of error.
23
+ def initialize(message: nil, type: nil, **properties)
24
+ @message = message
25
+ @type = type || self.class::TYPE
26
+ @comparable_properties = properties.merge(message: message, type: type)
17
27
  end
18
28
 
19
- # @return [String] Optional message describing the nature of the error.
29
+ # @return [String] optional message describing the nature of the error.
20
30
  attr_reader :message
21
31
 
32
+ # @return [String] short string used to identify the type of error.
33
+ attr_reader :type
34
+
22
35
  # @param other [Cuprum::Error] The other object to compare.
23
36
  #
24
- # @return [Boolean] true if the other object has the same class and message;
25
- # otherwise false.
37
+ # @return [Boolean] true if the other object has the same class and
38
+ # properties; otherwise false.
26
39
  def ==(other)
27
40
  other.instance_of?(self.class) &&
28
- comparable_properties.all? { |prop| send(prop) == other.send(prop) }
41
+ other.comparable_properties == comparable_properties
42
+ end
43
+
44
+ # Generates a serializable representation of the error object.
45
+ #
46
+ # By default, contains the #type and #message properties and an empty :data
47
+ # Hash. This can be overriden in subclasses by overriding the private method
48
+ # #as_json_data; this should always return a Hash with String keys and whose
49
+ # values are basic objects or data structures of the same.
50
+ #
51
+ # @return [Hash<String, Object>] a serializable hash representation of the
52
+ # error.
53
+ def as_json
54
+ {
55
+ 'data' => as_json_data,
56
+ 'message' => message,
57
+ 'type' => type
58
+ }
29
59
  end
30
60
 
61
+ protected
62
+
63
+ attr_reader :comparable_properties
64
+
31
65
  private
32
66
 
33
- def comparable_properties
34
- self.class.const_get(:COMPARABLE_PROPERTIES)
67
+ def as_json_data
68
+ {}
35
69
  end
36
70
  end
37
71
  end
@@ -13,6 +13,9 @@ module Cuprum::Errors
13
13
  # Format for generating error message.
14
14
  MESSAGE_FORMAT = 'no implementation defined for %s'
15
15
 
16
+ # Short string used to identify the type of error.
17
+ TYPE = 'cuprum.errors.command_not_implemented'
18
+
16
19
  # @param command [Cuprum::Command] The command called without a definition.
17
20
  def initialize(command:)
18
21
  @command = command
@@ -20,7 +23,7 @@ module Cuprum::Errors
20
23
  class_name = command&.class&.name || 'command'
21
24
  message = MESSAGE_FORMAT % class_name
22
25
 
23
- super(message: message)
26
+ super(command: command, message: message)
24
27
  end
25
28
 
26
29
  # @return [Cuprum::Command] The command called without a definition.
@@ -28,8 +31,8 @@ module Cuprum::Errors
28
31
 
29
32
  private
30
33
 
31
- def comparable_properties
32
- COMPARABLE_PROPERTIES
34
+ def as_json_data
35
+ command ? { 'class_name' => command.class.name } : {}
33
36
  end
34
37
  end
35
38
  end
@@ -7,12 +7,12 @@ module Cuprum::Errors
7
7
  # Error class to be used when trying to access the result of an uncalled
8
8
  # Operation.
9
9
  class OperationNotCalled < Cuprum::Error
10
- COMPARABLE_PROPERTIES = %i[operation].freeze
11
- private_constant :COMPARABLE_PROPERTIES
12
-
13
10
  # Format for generating error message.
14
11
  MESSAGE_FORMAT = '%s was not called and does not have a result'
15
12
 
13
+ # Short string used to identify the type of error.
14
+ TYPE = 'cuprum.errors.operation_not_called'
15
+
16
16
  # @param operation [Cuprum::Operation] The uncalled operation.
17
17
  def initialize(operation:)
18
18
  @operation = operation
@@ -20,7 +20,7 @@ module Cuprum::Errors
20
20
  class_name = operation&.class&.name || 'operation'
21
21
  message = MESSAGE_FORMAT % class_name
22
22
 
23
- super(message: message)
23
+ super(message: message, operation: operation)
24
24
  end
25
25
 
26
26
  # @return [Cuprum::Operation] The uncalled operation.
@@ -28,8 +28,8 @@ module Cuprum::Errors
28
28
 
29
29
  private
30
30
 
31
- def comparable_properties
32
- COMPARABLE_PROPERTIES
31
+ def as_json_data
32
+ operation ? { 'class_name' => operation.class.name } : {}
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/error'
4
+ require 'cuprum/errors'
5
+
6
+ module Cuprum::Errors
7
+ # An error returned when a command encounters an unhandled exception.
8
+ class UncaughtException < Cuprum::Error
9
+ # Short string used to identify the type of error.
10
+ TYPE = 'cuprum.collections.errors.uncaught_exception'
11
+
12
+ # @param exception [StandardError] The exception that was raised.
13
+ # @param message [String] A message to display. Will be annotated with
14
+ # details on the exception and the exception's cause (if any).
15
+ def initialize(exception:, message: 'uncaught exception')
16
+ @exception = exception
17
+ @cause = exception.cause
18
+
19
+ super(message: generate_message(message))
20
+ end
21
+
22
+ # @return [StandardError] the exception that was raised.
23
+ attr_reader :exception
24
+
25
+ private
26
+
27
+ attr_reader :cause
28
+
29
+ def as_json_data # rubocop:disable Metrics/MethodLength
30
+ data = {
31
+ 'exception_backtrace' => exception.backtrace,
32
+ 'exception_class' => exception.class,
33
+ 'exception_message' => exception.message
34
+ }
35
+
36
+ return data unless cause
37
+
38
+ data.update(
39
+ {
40
+ 'cause_backtrace' => cause.backtrace,
41
+ 'cause_class' => cause.class,
42
+ 'cause_message' => cause.message
43
+ }
44
+ )
45
+ end
46
+
47
+ def generate_message(message)
48
+ message = "#{message} #{exception.class}: #{exception.message}"
49
+
50
+ return message unless cause
51
+
52
+ message + " caused by #{cause.class}: #{cause.message}"
53
+ end
54
+ end
55
+ end
data/lib/cuprum/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum'
2
4
 
3
5
  module Cuprum
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/errors/uncaught_exception'
4
+
5
+ module Cuprum
6
+ # Utility class for handling uncaught exceptions in commands.
7
+ #
8
+ # @example
9
+ # class UnsafeCommand < Cuprum::Command
10
+ # private
11
+ #
12
+ # def process
13
+ # raise 'Something went wrong.'
14
+ # end
15
+ # end
16
+ #
17
+ # class SafeCommand < UnsafeCommand
18
+ # include Cuprum::ExceptionHandling
19
+ # end
20
+ #
21
+ # UnsafeCommand.new.call
22
+ # #=> raises a StandardError
23
+ #
24
+ # result = SafeCommand.new.call
25
+ # #=> a Cuprum::Result
26
+ # result.error
27
+ # #=> a Cuprum::Errors::UncaughtException error.
28
+ # result.error.message
29
+ # #=> 'uncaught exception in SafeCommand -' \
30
+ # ' StandardError: Something went wrong.'
31
+ module ExceptionHandling
32
+ # Wraps the #call method with a rescue clause matching any StandardError.
33
+ #
34
+ # If a StandardError or subclass thereof is raised and not caught by #call,
35
+ # then ExceptionHandling will rescue the exception and return a failing
36
+ # Cuprum::Result with a Cuprum::Errors::UncaughtException error.
37
+ #
38
+ # @return [Cuprum::Result] the result of calling the superclass method, or
39
+ # a failing result if a StandardError is raised.
40
+ def call(*args, **kwargs, &block)
41
+ super
42
+ rescue StandardError => exception
43
+ error = Cuprum::Errors::UncaughtException.new(
44
+ exception: exception,
45
+ message: "uncaught exception in #{self.class.name} - "
46
+ )
47
+ failure(error)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+ require 'cuprum/matching'
5
+
6
+ module Cuprum
7
+ # Provides result matching based on result status, error, and value.
8
+ #
9
+ # First, define match clauses using the .match DSL. Each match clause has a
10
+ # status and optionally a value class and/or error class. A result will only
11
+ # match the clause if the result status is the same as the clause's status.
12
+ # If the clause sets a value class, then the result value must be an instance
13
+ # of that class (or an instance of a subclass). If the clause sets an error
14
+ # class, then the result error must be an instance of that class (or an
15
+ # instance of a subclass).
16
+ #
17
+ # Once the matcher defines one or more match clauses, call #call with a result
18
+ # to match the result. The matcher will determine the best match with the same
19
+ # status (value and error match the result, only value or error match, or just
20
+ # status matches) and then call the match clause with the result. If no match
21
+ # clauses match the result, the matcher will instead raise a
22
+ # Cuprum::Matching::NoMatchError.
23
+ #
24
+ # @example Matching A Status
25
+ # matcher = Cuprum::Matcher.new do
26
+ # match(:failure) { 'Something went wrong' }
27
+ #
28
+ # match(:success) { 'Ok' }
29
+ # end
30
+ #
31
+ # matcher.call(Cuprum::Result.new(status: :failure))
32
+ # #=> 'Something went wrong'
33
+ #
34
+ # matcher.call(Cuprum::Result.new(status: :success))
35
+ # #=> 'Ok'
36
+ #
37
+ # @example Matching An Error
38
+ # matcher = Cuprum::Matcher.new do
39
+ # match(:failure) { 'Something went wrong' }
40
+ #
41
+ # match(:failure, error: CustomError) { |result| result.error.message }
42
+ #
43
+ # match(:success) { 'Ok' }
44
+ # end
45
+ #
46
+ # matcher.call(Cuprum::Result.new(status: :failure))
47
+ # #=> 'Something went wrong'
48
+ #
49
+ # error = CustomError.new(message: 'The magic smoke is escaping.')
50
+ # matcher.call(Cuprum::Result.new(error: error))
51
+ # #=> 'The magic smoke is escaping.'
52
+ #
53
+ # @example Using A Match Context
54
+ # context = Struct.new(:name).new('programs')
55
+ # matcher = Cuprum::Matcher.new(context) do
56
+ # match(:failure) { 'Something went wrong' }
57
+ #
58
+ # match(:success) { "Greetings, #{name}!" }
59
+ # end
60
+ #
61
+ # matcher.call(Cuprum::Result.new(status: :success)
62
+ # #=> 'Greetings, programs!'
63
+ class Matcher
64
+ include Cuprum::Matching
65
+
66
+ # @param match_context [Object] the execution context for a matching clause.
67
+ #
68
+ # @yield Executes the block in the context of the singleton class. This is
69
+ # used to define match clauses when instantiating a Matcher instance.
70
+ def initialize(match_context = nil, &block)
71
+ @match_context = match_context
72
+
73
+ singleton_class.instance_exec(&block) if block_given?
74
+ end
75
+
76
+ # Returns a copy of the matcher with the given execution context.
77
+ #
78
+ # @param match_context [Object] the execution context for a matching clause.
79
+ #
80
+ # @return [Cuprum::Matcher] the copied matcher.
81
+ def with_context(match_context)
82
+ clone.tap { |copy| copy.match_context = match_context }
83
+ end
84
+ alias_method :using_context, :with_context
85
+
86
+ protected
87
+
88
+ attr_writer :match_context
89
+ end
90
+ end