cuprum 0.9.1 → 0.11.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/DEVELOPMENT.md +42 -53
  4. data/README.md +728 -536
  5. data/lib/cuprum.rb +12 -6
  6. data/lib/cuprum/built_in.rb +3 -1
  7. data/lib/cuprum/built_in/identity_command.rb +6 -4
  8. data/lib/cuprum/built_in/identity_operation.rb +4 -2
  9. data/lib/cuprum/built_in/null_command.rb +5 -3
  10. data/lib/cuprum/built_in/null_operation.rb +4 -2
  11. data/lib/cuprum/command.rb +37 -59
  12. data/lib/cuprum/command_factory.rb +50 -24
  13. data/lib/cuprum/currying.rb +79 -0
  14. data/lib/cuprum/currying/curried_command.rb +116 -0
  15. data/lib/cuprum/error.rb +44 -10
  16. data/lib/cuprum/errors.rb +2 -0
  17. data/lib/cuprum/errors/command_not_implemented.rb +6 -3
  18. data/lib/cuprum/errors/operation_not_called.rb +6 -6
  19. data/lib/cuprum/errors/uncaught_exception.rb +55 -0
  20. data/lib/cuprum/exception_handling.rb +50 -0
  21. data/lib/cuprum/matcher.rb +90 -0
  22. data/lib/cuprum/matcher_list.rb +150 -0
  23. data/lib/cuprum/matching.rb +232 -0
  24. data/lib/cuprum/matching/match_clause.rb +65 -0
  25. data/lib/cuprum/middleware.rb +210 -0
  26. data/lib/cuprum/operation.rb +17 -15
  27. data/lib/cuprum/processing.rb +10 -14
  28. data/lib/cuprum/result.rb +2 -4
  29. data/lib/cuprum/result_helpers.rb +22 -0
  30. data/lib/cuprum/rspec/be_a_result.rb +10 -1
  31. data/lib/cuprum/rspec/be_a_result_matcher.rb +5 -7
  32. data/lib/cuprum/rspec/be_callable.rb +14 -0
  33. data/lib/cuprum/steps.rb +233 -0
  34. data/lib/cuprum/utils.rb +3 -1
  35. data/lib/cuprum/utils/instance_spy.rb +37 -30
  36. data/lib/cuprum/version.rb +13 -10
  37. metadata +34 -19
  38. data/lib/cuprum/chaining.rb +0 -420
data/lib/cuprum.rb CHANGED
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A lightweight, functional-lite toolkit for making business logic a first-class
2
4
  # citizen of your application.
3
5
  module Cuprum
4
- autoload :Command, 'cuprum/command'
5
- autoload :Operation, 'cuprum/operation'
6
- autoload :Result, 'cuprum/result'
6
+ autoload :Command, 'cuprum/command'
7
+ autoload :Error, 'cuprum/error'
8
+ autoload :Matcher, 'cuprum/matcher'
9
+ autoload :Middleware, 'cuprum/middleware'
10
+ autoload :Operation, 'cuprum/operation'
11
+ autoload :Result, 'cuprum/result'
12
+ autoload :Steps, 'cuprum/steps'
7
13
 
8
14
  class << self
9
15
  # @return [String] The current version of the gem.
10
16
  def version
11
17
  VERSION
12
- end # method version
13
- end # eigenclass
14
- end # module
18
+ end
19
+ end
20
+ 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,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,5 +1,8 @@
1
- require 'cuprum/chaining'
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/currying'
2
4
  require 'cuprum/processing'
5
+ require 'cuprum/steps'
3
6
 
4
7
  module Cuprum
5
8
  # Functional object that encapsulates a business logic operation with a
@@ -18,14 +21,14 @@ module Cuprum
18
21
  # class MultiplyCommand < Cuprum::Command
19
22
  # def initialize multiplier
20
23
  # @multiplier = multiplier
21
- # end # constructor
24
+ # end
22
25
  #
23
26
  # private
24
27
  #
25
28
  # def process int
26
29
  # int * @multiplier
27
- # end # method process
28
- # end # class
30
+ # end
31
+ # end
29
32
  #
30
33
  # triple_command = MultiplyCommand.new(3)
31
34
  # result = command_command.call(5)
@@ -36,7 +39,7 @@ module Cuprum
36
39
  # class DivideCommand < Cuprum::Command
37
40
  # def initialize divisor
38
41
  # @divisor = divisor
39
- # end # constructor
42
+ # end
40
43
  #
41
44
  # private
42
45
  #
@@ -46,8 +49,8 @@ module Cuprum
46
49
  # end
47
50
  #
48
51
  # int / @divisor
49
- # end # method process
50
- # end # class
52
+ # end
53
+ # end
51
54
  #
52
55
  # halve_command = DivideCommand.new(2)
53
56
  # result = halve_command.call(10)
@@ -61,57 +64,11 @@ module Cuprum
61
64
  # result.error #=> 'errors.messages.divide_by_zero'
62
65
  # result.value #=> nil
63
66
  #
64
- # @example Command Chaining
65
- # class AddCommand < Cuprum::Command
66
- # def initialize addend
67
- # @addend = addend
68
- # end # constructor
69
- #
70
- # private
71
- #
72
- # def process int
73
- # int + @addend
74
- # end # method process
75
- # end # class
76
- #
77
- # double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
78
- # result = double_and_add_one(5)
79
- #
80
- # result.value #=> 5
81
- #
82
- # @example Conditional Chaining
83
- # class EvenCommand < Cuprum::Command
84
- # private
85
- #
86
- # def process int
87
- # return int if int.even?
88
- #
89
- # Cuprum::Errors.new(error: 'errors.messages.not_even')
90
- # end # method process
91
- # end # class
92
- #
93
- # # The next step in a Collatz sequence is determined as follows:
94
- # # - If the number is even, divide it by 2.
95
- # # - If the number is odd, multiply it by 3 and add 1.
96
- # collatz_command =
97
- # EvenCommand.new.
98
- # chain(DivideCommand.new(2), :on => :success).
99
- # chain(
100
- # MultiplyCommand.new(3).chain(AddCommand.new(1),
101
- # :on => :failure
102
- # )
103
- #
104
- # result = collatz_command.new(5)
105
- # result.value #=> 16
106
- #
107
- # result = collatz_command.new(16)
108
- # result.value #=> 8
109
- #
110
- # @see Cuprum::Chaining
111
67
  # @see Cuprum::Processing
112
68
  class Command
113
69
  include Cuprum::Processing
114
- include Cuprum::Chaining
70
+ include Cuprum::Currying
71
+ include Cuprum::Steps
115
72
 
116
73
  # Returns a new instance of Cuprum::Command.
117
74
  #
@@ -119,12 +76,33 @@ module Cuprum
119
76
  # #call method will wrap the block and set the result #value to the return
120
77
  # value of the block. This overrides the implementation in #process, if
121
78
  # any.
122
- def initialize &implementation
79
+ def initialize(&implementation)
123
80
  return unless implementation
124
81
 
125
82
  define_singleton_method :process, &implementation
126
83
 
127
84
  singleton_class.send(:private, :process)
128
- end # method initialize
129
- end # class
130
- end # module
85
+ end
86
+
87
+ # (see Cuprum::Processing#call)
88
+ def call(*args, **kwargs, &block)
89
+ steps { super }
90
+ end
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'
@@ -28,7 +30,7 @@ module Cuprum
28
30
  #
29
31
  # factory::Dream #=> DreamCommand
30
32
  # factory.dream #=> an instance of DreamCommand
31
- class CommandFactory < Module
33
+ class CommandFactory < Module # rubocop:disable Metrics/ClassLength
32
34
  # Defines the Domain-Specific Language and helper methods for dynamically
33
35
  # defined commands.
34
36
  class << self
@@ -147,20 +149,14 @@ 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
- name = normalize_command_name(name)
154
+ method_name = normalize_command_name(name)
153
155
 
154
- (@command_definitions ||= {})[name] =
156
+ (@command_definitions ||= {})[method_name] =
155
157
  metadata.merge(__const_defn__: defn)
156
158
 
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
159
+ define_lazy_command_method(method_name)
164
160
  end
165
161
 
166
162
  protected
@@ -184,21 +180,47 @@ module Cuprum
184
180
 
185
181
  (@command_definitions ||= {})[command_name] = metadata
186
182
 
187
- define_method(command_name) do |*args|
188
- instance_exec(*args, &builder)
183
+ define_method(command_name) do |*args, **kwargs|
184
+ if kwargs.empty?
185
+ instance_exec(*args, &builder)
186
+ else
187
+ instance_exec(*args, **kwargs, &builder)
188
+ end
189
189
  end
190
190
  end
191
191
 
192
192
  def define_command_from_class(command_class, name:, metadata: {})
193
193
  guard_invalid_definition!(command_class)
194
194
 
195
- command_name = normalize_command_name(name)
195
+ method_name = normalize_command_name(name)
196
196
 
197
- (@command_definitions ||= {})[command_name] =
197
+ (@command_definitions ||= {})[method_name] =
198
198
  metadata.merge(__const_defn__: command_class)
199
199
 
200
- define_method(command_name) do |*args, &block|
201
- build_command(command_class, *args, &block)
200
+ define_command_method(method_name, command_class)
201
+ end
202
+
203
+ def define_command_method(method_name, command_class)
204
+ define_method(method_name) do |*args, **kwargs, &block|
205
+ if kwargs.empty?
206
+ build_command(command_class, *args, &block)
207
+ else
208
+ build_command(command_class, *args, **kwargs, &block)
209
+ end
210
+ end
211
+ end
212
+
213
+ def define_lazy_command_method(method_name)
214
+ const_name = tools.string_tools.camelize(method_name)
215
+
216
+ define_method(method_name) do |*args, **kwargs, &block|
217
+ command_class = const_get(const_name)
218
+
219
+ if kwargs.empty?
220
+ build_command(command_class, *args, &block)
221
+ else
222
+ build_command(command_class, *args, **kwargs, &block)
223
+ end
202
224
  end
203
225
  end
204
226
 
@@ -207,21 +229,21 @@ module Cuprum
207
229
 
208
230
  raise NotImplementedError,
209
231
  'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
210
- 'define commands for a factory.'.freeze
232
+ 'define commands for a factory.'
211
233
  end
212
234
 
213
235
  def guard_invalid_definition!(command_class)
214
236
  return if command_class.is_a?(Class) && command_class < Cuprum::Command
215
237
 
216
- raise ArgumentError, 'definition must be a command class'.freeze
238
+ raise ArgumentError, 'definition must be a command class'
217
239
  end
218
240
 
219
241
  def normalize_command_name(command_name)
220
- tools.string.underscore(command_name).intern
242
+ tools.string_tools.underscore(command_name).intern
221
243
  end
222
244
 
223
245
  def require_definition!
224
- raise ArgumentError, 'must provide a command class or a block'.freeze
246
+ raise ArgumentError, 'must provide a command class or a block'
225
247
  end
226
248
 
227
249
  def tools
@@ -243,7 +265,7 @@ module Cuprum
243
265
  end
244
266
 
245
267
  # @private
246
- def const_defined?(const_name, inherit = true)
268
+ def const_defined?(const_name, inherit = true) # rubocop:disable Style/OptionalBooleanParameter
247
269
  command?(const_name) || super
248
270
  end
249
271
 
@@ -265,8 +287,12 @@ module Cuprum
265
287
 
266
288
  private
267
289
 
268
- def build_command(command_class, *args, &block)
269
- command_class.new(*args, &block)
290
+ def build_command(command_class, *args, **kwargs, &block)
291
+ if kwargs.empty?
292
+ command_class.new(*args, &block)
293
+ else
294
+ command_class.new(*args, **kwargs, &block)
295
+ end
270
296
  end
271
297
 
272
298
  def normalize_command_name(command_name)
@@ -0,0 +1,79 @@
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, &block)
69
+ return self if arguments.empty? && keywords.empty? && block.nil?
70
+
71
+ Cuprum::Currying::CurriedCommand.new(
72
+ arguments: arguments,
73
+ block: block,
74
+ command: self,
75
+ keywords: keywords
76
+ )
77
+ end
78
+ end
79
+ end