cuprum 0.9.1 → 0.11.0

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