cuprum 0.7.0 → 0.10.0.rc.0

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