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.
@@ -1,6 +1,7 @@
1
1
  require 'cuprum/chaining'
2
+ require 'cuprum/currying'
2
3
  require 'cuprum/processing'
3
- require 'cuprum/result_helpers'
4
+ require 'cuprum/steps'
4
5
 
5
6
  module Cuprum
6
7
  # Functional object that encapsulates a business logic operation with a
@@ -33,7 +34,7 @@ module Cuprum
33
34
  #
34
35
  # result.value #=> 15
35
36
  #
36
- # @example A Command with errors
37
+ # @example A Command with an error state
37
38
  # class DivideCommand < Cuprum::Command
38
39
  # def initialize divisor
39
40
  # @divisor = divisor
@@ -43,10 +44,8 @@ module Cuprum
43
44
  #
44
45
  # def process int
45
46
  # if @divisor.zero?
46
- # errors << 'errors.messages.divide_by_zero'
47
- #
48
- # return
49
- # end # if
47
+ # return Cuprum::Result.new(error: 'errors.messages.divide_by_zero')
48
+ # end
50
49
  #
51
50
  # int / @divisor
52
51
  # end # method process
@@ -55,14 +54,14 @@ module Cuprum
55
54
  # halve_command = DivideCommand.new(2)
56
55
  # result = halve_command.call(10)
57
56
  #
58
- # result.errors #=> []
59
- # result.value #=> 5
57
+ # result.error #=> nil
58
+ # result.value #=> 5
60
59
  #
61
- # command_with_errors = DivideCommand.new(0)
62
- # result = command_with_errors.call(10)
60
+ # divide_command = DivideCommand.new(0)
61
+ # result = divide_command.call(10)
63
62
  #
64
- # result.errors #=> ['errors.messages.divide_by_zero']
65
- # result.value #=> nil
63
+ # result.error #=> 'errors.messages.divide_by_zero'
64
+ # result.value #=> nil
66
65
  #
67
66
  # @example Command Chaining
68
67
  # class AddCommand < Cuprum::Command
@@ -87,9 +86,9 @@ module Cuprum
87
86
  # private
88
87
  #
89
88
  # def process int
90
- # errors << 'errors.messages.not_even' unless int.even?
89
+ # return int if int.even?
91
90
  #
92
- # int
91
+ # Cuprum::Errors.new(error: 'errors.messages.not_even')
93
92
  # end # method process
94
93
  # end # class
95
94
  #
@@ -112,11 +111,10 @@ module Cuprum
112
111
  #
113
112
  # @see Cuprum::Chaining
114
113
  # @see Cuprum::Processing
115
- # @see Cuprum::ResultHelpers
116
114
  class Command
117
115
  include Cuprum::Processing
118
- include Cuprum::Chaining
119
- include Cuprum::ResultHelpers
116
+ include Cuprum::Currying
117
+ include Cuprum::Steps
120
118
 
121
119
  # Returns a new instance of Cuprum::Command.
122
120
  #
@@ -125,7 +123,15 @@ module Cuprum
125
123
  # value of the block. This overrides the implementation in #process, if
126
124
  # any.
127
125
  def initialize &implementation
128
- define_singleton_method :process, &implementation if implementation
126
+ return unless implementation
127
+
128
+ define_singleton_method :process, &implementation
129
+
130
+ singleton_class.send(:private, :process)
129
131
  end # method initialize
132
+
133
+ def call(*args, **kwargs, &block)
134
+ steps { super }
135
+ end
130
136
  end # class
131
137
  end # module
@@ -0,0 +1,300 @@
1
+ require 'cuprum'
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+
5
+ module Cuprum
6
+ # Builder class for instantiating command objects.
7
+ #
8
+ # @example
9
+ # class SpaceFactory < Cuprum::CommandFactory
10
+ # command :build, BuildCommand
11
+ #
12
+ # command :fly { |launch_site:| FlyCommand.new(launch_site) }
13
+ #
14
+ # command_class :dream { DreamCommand }
15
+ # end
16
+ #
17
+ # factory = SpaceFactory.new
18
+ #
19
+ # factory::Build #=> BuildCommand
20
+ # factory.build #=> an instance of BuildCommand
21
+ #
22
+ # rocket = factory.build.call({ size: 'big' }) #=> an instance of Rocket
23
+ # rocket.size #=> 'big'
24
+ #
25
+ # command = factory.fly(launch_site: 'KSC') #=> an instance of FlyCommand
26
+ # command.call(rocket)
27
+ # #=> launches the rocket from KSC
28
+ #
29
+ # factory::Dream #=> DreamCommand
30
+ # factory.dream #=> an instance of DreamCommand
31
+ class CommandFactory < Module # rubocop:disable Metrics/ClassLength
32
+ # Defines the Domain-Specific Language and helper methods for dynamically
33
+ # defined commands.
34
+ class << self
35
+ # Defines a command for the factory.
36
+ #
37
+ # @overload command(name, command_class)
38
+ # Defines a command using the given factory class. For example, when a
39
+ # command is defined with the name "whirlpool" and the WhirlpoolCommand
40
+ # class:
41
+ #
42
+ # A factory instance will define the constant ::Whirlpool, and accessing
43
+ # factory::Whirlpool will return the WhirlpoolCommand class.
44
+ #
45
+ # A factory instance will define the method #whirlpool, and calling
46
+ # factory#whirlpool will return an instance of WhirlpoolCommand. Any
47
+ # arguments passed to the #whirlpool method will be forwarded to the
48
+ # constructor when building the command.
49
+ #
50
+ # @param name [String, Symbol] The name of the command.
51
+ # @param command_class [Class] The command class. Must be a subclass of
52
+ # Cuprum::Command.
53
+ #
54
+ # @example
55
+ # class MoveFactory < Cuprum::CommandFactory
56
+ # command :cut, CutCommand
57
+ # end
58
+ #
59
+ # factory = MoveFactory.new
60
+ # factory::Cut #=> CutCommand
61
+ # factory.cut #=> an instance of CutCommand
62
+ #
63
+ # @overload command(name) { |*args| }
64
+ # Defines a command using the given block, which must return an instance
65
+ # of a Cuprum::Command subclass. For example, when a command is defined
66
+ # with the name "dive" and a block that returns an instance of the
67
+ # DiveCommand class:
68
+ #
69
+ # A factory instance will define the method #dive, and calling
70
+ # factory#dive will call the block and return the resulting command
71
+ # instance. Any arguments passed to the #dive method will be forwarded
72
+ # to the block when building the command.
73
+ #
74
+ # The block will be evaluated in the context of the factory instance, so
75
+ # it has access to any methods or instance variables defined for the
76
+ # factory instance.
77
+ #
78
+ # @param name [String, Symbol] The name of the command.
79
+ #
80
+ # @yield The block will be executed in the context of the factory
81
+ # instance.
82
+ # @yieldparam *args [Array] Any arguments given to the method
83
+ # factory.name() will be passed on the block.
84
+ # @yieldreturn [Cuprum::Command] The block return an instance of a
85
+ # Cuprum::Command subclass, or else raise an error.
86
+ #
87
+ # @example
88
+ # class MoveFactory < Cuprum::CommandFactory
89
+ # command :fly { |destination| FlyCommand.new(destination) }
90
+ # end
91
+ #
92
+ # factory = MoveFactory.new
93
+ # factory.fly_command('Indigo Plateau')
94
+ # #=> an instance of FlyCommand with a destination of 'Indigo Plateau'
95
+ def command(name, klass = nil, **metadata, &defn)
96
+ guard_abstract_factory!
97
+
98
+ if klass
99
+ define_command_from_class(klass, name: name, metadata: metadata)
100
+ elsif block_given?
101
+ define_command_from_block(defn, name: name, metadata: metadata)
102
+ else
103
+ require_definition!
104
+ end
105
+ end
106
+
107
+ # Defines a command using the given block, which must return a subclass of
108
+ # Cuprum::Command. For example, when a command is defined with the name
109
+ # "rock_climb" and a block returning a subclass of RockClimbCommand:
110
+ #
111
+ # A factory instance will define the constant ::RockClimb, and accessing
112
+ # factory::RockClimb will call the block and return the resulting command
113
+ # class. This value is memoized, so subsequent factory::RockClimb accesses
114
+ # on the same factory instance will return the same command class.
115
+ #
116
+ # A factory instance will define the method #rock_climb, and calling
117
+ # factory#rock_climb will access the constant at ::RockClimb and return an
118
+ # instance of that subclass of RockClimbCommand. Any arguments passed to
119
+ # the #whirlpool method will be forwarded to the constructor when building
120
+ # the command.
121
+ #
122
+ # @param name [String, Symbol] The name of the command.
123
+ # @yield The block will be executed in the context of the factory
124
+ # instance.
125
+ # @yieldparam *args [Array] Any arguments given to the method
126
+ # factory.name() will be passed on the block.
127
+ # @yieldreturn [Cuprum::Command] The block return an instance of a
128
+ # Cuprum::Command subclass, or else raise an error.
129
+ #
130
+ # @example
131
+ # class MoveFactory < Cuprum::CommandFactory
132
+ # command_class :flash do
133
+ # Class.new(FlashCommand) do
134
+ # def brightness
135
+ # :intense
136
+ # end
137
+ # end
138
+ # end
139
+ # end
140
+ #
141
+ # factory = MoveFactory.new
142
+ # factory::Flash #=> a subclass of FlashCommand
143
+ # factory.flash #=> an instance of factory::Flash
144
+ #
145
+ # command = factory.flash
146
+ # command.brightness #=> :intense
147
+ def command_class(name, **metadata, &defn)
148
+ guard_abstract_factory!
149
+
150
+ raise ArgumentError, 'must provide a block'.freeze unless block_given?
151
+
152
+ method_name = normalize_command_name(name)
153
+
154
+ (@command_definitions ||= {})[method_name] =
155
+ metadata.merge(__const_defn__: defn)
156
+
157
+ define_lazy_command_method(method_name)
158
+ end
159
+
160
+ protected
161
+
162
+ def command_definitions
163
+ definitions = (@command_definitions ||= {})
164
+
165
+ return definitions unless superclass < Cuprum::CommandFactory
166
+
167
+ superclass.command_definitions.merge(definitions)
168
+ end
169
+
170
+ private
171
+
172
+ def abstract_factory?
173
+ self == Cuprum::CommandFactory
174
+ end
175
+
176
+ def define_command_from_block(builder, name:, metadata: {})
177
+ command_name = normalize_command_name(name)
178
+
179
+ (@command_definitions ||= {})[command_name] = metadata
180
+
181
+ define_method(command_name) do |*args, **kwargs|
182
+ if kwargs.empty?
183
+ instance_exec(*args, &builder)
184
+ else
185
+ instance_exec(*args, **kwargs, &builder)
186
+ end
187
+ end
188
+ end
189
+
190
+ def define_command_from_class(command_class, name:, metadata: {})
191
+ guard_invalid_definition!(command_class)
192
+
193
+ method_name = normalize_command_name(name)
194
+
195
+ (@command_definitions ||= {})[method_name] =
196
+ metadata.merge(__const_defn__: command_class)
197
+
198
+ define_command_method(method_name, command_class)
199
+ end
200
+
201
+ def define_command_method(method_name, command_class)
202
+ define_method(method_name) do |*args, **kwargs, &block|
203
+ if kwargs.empty?
204
+ build_command(command_class, *args, &block)
205
+ else
206
+ build_command(command_class, *args, **kwargs, &block)
207
+ end
208
+ end
209
+ end
210
+
211
+ def define_lazy_command_method(method_name)
212
+ const_name = tools.string_tools.camelize(method_name)
213
+
214
+ define_method(method_name) do |*args, **kwargs, &block|
215
+ command_class = const_get(const_name)
216
+
217
+ if kwargs.empty?
218
+ build_command(command_class, *args, &block)
219
+ else
220
+ build_command(command_class, *args, **kwargs, &block)
221
+ end
222
+ end
223
+ end
224
+
225
+ def guard_abstract_factory!
226
+ return unless abstract_factory?
227
+
228
+ raise NotImplementedError,
229
+ 'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
230
+ 'define commands for a factory.'.freeze
231
+ end
232
+
233
+ def guard_invalid_definition!(command_class)
234
+ return if command_class.is_a?(Class) && command_class < Cuprum::Command
235
+
236
+ raise ArgumentError, 'definition must be a command class'.freeze
237
+ end
238
+
239
+ def normalize_command_name(command_name)
240
+ tools.string_tools.underscore(command_name).intern
241
+ end
242
+
243
+ def require_definition!
244
+ raise ArgumentError, 'must provide a command class or a block'.freeze
245
+ end
246
+
247
+ def tools
248
+ SleepingKingStudios::Tools::Toolbelt.instance
249
+ end
250
+ end
251
+
252
+ # @return [Boolean] true if the factory defines the given command, otherwise
253
+ # false.
254
+ def command?(command_name)
255
+ command_name = normalize_command_name(command_name)
256
+
257
+ commands.include?(command_name)
258
+ end
259
+
260
+ # @return [Array<Symbol>] a list of the commands defined by the factory.
261
+ def commands
262
+ self.class.send(:command_definitions).keys
263
+ end
264
+
265
+ # @private
266
+ def const_defined?(const_name, inherit = true)
267
+ command?(const_name) || super
268
+ end
269
+
270
+ # @private
271
+ def const_missing(const_name)
272
+ definitions = self.class.send(:command_definitions)
273
+ command_name = normalize_command_name(const_name)
274
+ command_defn = definitions.dig(command_name, :__const_defn__)
275
+
276
+ return super unless command_defn
277
+
278
+ command_class =
279
+ command_defn.is_a?(Proc) ? instance_exec(&command_defn) : command_defn
280
+
281
+ const_set(const_name, command_class)
282
+
283
+ command_class
284
+ end
285
+
286
+ private
287
+
288
+ def build_command(command_class, *args, **kwargs, &block)
289
+ if kwargs.empty?
290
+ command_class.new(*args, &block)
291
+ else
292
+ command_class.new(*args, **kwargs, &block)
293
+ end
294
+ end
295
+
296
+ def normalize_command_name(command_name)
297
+ self.class.send(:normalize_command_name, command_name)
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,78 @@
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)
69
+ return self if arguments.empty? && keywords.empty?
70
+
71
+ Cuprum::Currying::CurriedCommand.new(
72
+ arguments: arguments,
73
+ command: self,
74
+ keywords: keywords
75
+ )
76
+ end
77
+ end
78
+ end