cuprum 0.10.0 → 1.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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