cuprum 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/DEVELOPMENT.md +45 -42
- data/README.md +469 -62
- data/lib/cuprum.rb +1 -1
- data/lib/cuprum/built_in.rb +1 -1
- data/lib/cuprum/built_in/identity_command.rb +1 -1
- data/lib/cuprum/chaining.rb +300 -92
- data/lib/cuprum/command.rb +36 -18
- data/lib/cuprum/operation.rb +6 -5
- data/lib/cuprum/processing.rb +180 -0
- data/lib/cuprum/result.rb +17 -13
- data/lib/cuprum/result_helpers.rb +113 -0
- data/lib/cuprum/utils/instance_spy.rb +26 -26
- data/lib/cuprum/utils/result_not_empty_warning.rb +65 -0
- data/lib/cuprum/version.rb +3 -3
- metadata +7 -5
- data/lib/cuprum/basic_command.rb +0 -212
data/lib/cuprum.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# A lightweight, functional-lite toolkit for making business logic a first-class
|
2
2
|
# citizen of your application.
|
3
3
|
module Cuprum
|
4
|
-
autoload :
|
4
|
+
autoload :Command, 'cuprum/command'
|
5
5
|
autoload :Operation, 'cuprum/operation'
|
6
6
|
autoload :Result, 'cuprum/result'
|
7
7
|
|
data/lib/cuprum/built_in.rb
CHANGED
@@ -2,7 +2,7 @@ require 'cuprum/built_in'
|
|
2
2
|
require 'cuprum/command'
|
3
3
|
|
4
4
|
module Cuprum::BuiltIn
|
5
|
-
# A predefined
|
5
|
+
# A predefined command that returns the value or result it was called with.
|
6
6
|
#
|
7
7
|
# @example With a value.
|
8
8
|
# result = IdentityCommand.new.call('custom value')
|
data/lib/cuprum/chaining.rb
CHANGED
@@ -5,128 +5,322 @@ module Cuprum
|
|
5
5
|
# Chaining commands allows you to define complex logic by composing it from
|
6
6
|
# simpler commands, including branching logic and error handling.
|
7
7
|
#
|
8
|
+
# @example Chaining Commands
|
9
|
+
# # By chaining commands together with the #chain instance method, we set up
|
10
|
+
# # a series of commands to run in sequence. Each chained command is passed
|
11
|
+
# # the value of the previous command.
|
12
|
+
#
|
13
|
+
# class GenerateUrlCommand
|
14
|
+
# include Cuprum::Processing
|
15
|
+
#
|
16
|
+
# private
|
17
|
+
#
|
18
|
+
# # Acts as a pipeline, taking a value (the title of the given post) and
|
19
|
+
# # calling the underscore, URL safe, and prepend date commands. By
|
20
|
+
# # passing parameters to PrependDateCommand, we can customize the command
|
21
|
+
# # in the pipeline to the current context (in this case, the Post).
|
22
|
+
# def process post
|
23
|
+
# UnderscoreCommand.new.
|
24
|
+
# chain(UrlSafeCommand.new).
|
25
|
+
# chain(PrependDateCommand.new(post.created_at)).
|
26
|
+
# call(post.title)
|
27
|
+
# end # method process
|
28
|
+
# end # class
|
29
|
+
#
|
30
|
+
# title = 'Greetings, programs!'
|
31
|
+
# date = '1982-07-09'
|
32
|
+
# post = Post.new(:title => title, :created_at => date)
|
33
|
+
# url = GenerateUrlCommand.new.call(post).value
|
34
|
+
# #=> '1982_07_09_greetings_programs'
|
35
|
+
#
|
36
|
+
# title = 'Plasma-based Einhanders in Popular Media'
|
37
|
+
# date = '1977-05-25'
|
38
|
+
# post = Post.new(:title => title, :created_at => date)
|
39
|
+
# url = GenerateUrlCommand.new.call(post).value
|
40
|
+
# #=> '1977_05_25_plasma_based_einhanders_in_popular_media'
|
41
|
+
#
|
42
|
+
# @example Conditional Chaining
|
43
|
+
# # Commands can be conditionally chained based on the success or failure of
|
44
|
+
# # the previous command using the on: keyword. If the command is chained
|
45
|
+
# # using on: :success, it will only be called if the result is passing.
|
46
|
+
# # If the command is chained using on: :failure, it will only be called if
|
47
|
+
# # the command is failing. This can be used to perform error handling.
|
48
|
+
#
|
49
|
+
# class CreateTaggingCommand
|
50
|
+
# include Cuprum::Processing
|
51
|
+
#
|
52
|
+
# private
|
53
|
+
#
|
54
|
+
# # Tries to find the tag with the given name. If that fails, creates a
|
55
|
+
# # new tag with the given name. If the tag is found, or if the new tag is
|
56
|
+
# # successfully created, then creates a tagging using the tag. If the tag
|
57
|
+
# # is not found and cannot be created, then the tagging is not created
|
58
|
+
# # and the result of the CreateTaggingCommand is a failure with the
|
59
|
+
# # appropriate error messages.
|
60
|
+
# def process taggable, tag_name
|
61
|
+
# FindTag.new.call(tag_name).
|
62
|
+
# # The chained command is called with the value of the previous
|
63
|
+
# # command, in this case the Tag or nil returned by FindTag.
|
64
|
+
# chain(:on => :failure) do |tag|
|
65
|
+
# # Chained commands share a result object, including errors. To
|
66
|
+
# # rescue a command chain and return the execution to the "happy
|
67
|
+
# # path", use on: :failure and clear the errors.
|
68
|
+
# result.errors.clear
|
69
|
+
#
|
70
|
+
# Tag.create(tag_name)
|
71
|
+
# end.
|
72
|
+
# chain(:on => :success) do |tag|
|
73
|
+
# tag.create_tagging(taggable)
|
74
|
+
# end
|
75
|
+
# end # method process
|
76
|
+
# end # method class
|
77
|
+
#
|
78
|
+
# post = Post.create(:title => 'Tagging Example')
|
79
|
+
# example_tag = Tag.create(:name => 'Example Tag')
|
80
|
+
#
|
81
|
+
# result = CreateTaggingCommand.new.call(post, 'Example Tag')
|
82
|
+
# result.success? #=> true
|
83
|
+
# result.errors #=> []
|
84
|
+
# result.value #=> an instance of Tagging
|
85
|
+
# post.tags.map(&:name)
|
86
|
+
# #=> ['Example Tag']
|
87
|
+
#
|
88
|
+
# result = CreateTaggingCommand.new.call(post, 'Another Tag')
|
89
|
+
# result.success? #=> true
|
90
|
+
# result.errors #=> []
|
91
|
+
# result.value #=> an instance of Tagging
|
92
|
+
# post.tags.map(&:name)
|
93
|
+
# #=> ['Example Tag', 'Another Tag']
|
94
|
+
#
|
95
|
+
# result = CreateTaggingCommand.new.call(post, 'An Invalid Tag Name')
|
96
|
+
# result.success? #=> false
|
97
|
+
# result.errors #=> [{ tag: { name: ['is invalid'] }}]
|
98
|
+
# post.tags.map(&:name)
|
99
|
+
# #=> ['Example Tag', 'Another Tag']
|
100
|
+
#
|
101
|
+
# @example Yield Result and Tap Result
|
102
|
+
# # The #yield_result method allows for advanced control over a step in the
|
103
|
+
# # command chain. The block will be yielded the result at that point in the
|
104
|
+
# # chain, and will wrap the returned value in a result to the next chained
|
105
|
+
# # command (or return it directly if the returned value is a result).
|
106
|
+
# #
|
107
|
+
# # The #tap_result method inserts arbitrary code into the command chain
|
108
|
+
# # without interrupting it. The block will be yielded the result at that
|
109
|
+
# # point in the chain and will pass that same result to the next chained
|
110
|
+
# # command after executing the block. The return value of the block is
|
111
|
+
# # ignored.
|
112
|
+
#
|
113
|
+
# class UpdatePostCommand
|
114
|
+
# include Cuprum::Processing
|
115
|
+
#
|
116
|
+
# private
|
117
|
+
#
|
118
|
+
# def process id, attributes
|
119
|
+
# # First, find the referenced post.
|
120
|
+
# Find.new(Post).call(id).
|
121
|
+
# yield_result(:on => :failure) do |result|
|
122
|
+
# redirect_to posts_path
|
123
|
+
#
|
124
|
+
# # A halted result prevents further :on => :failure commands from
|
125
|
+
# # being called.
|
126
|
+
# result.halt!
|
127
|
+
# end.
|
128
|
+
# yield_result do |result|
|
129
|
+
# # Assign our attributes and save the post.
|
130
|
+
# UpdateAttributes.new.call(result.value, attributes)
|
131
|
+
# end.
|
132
|
+
# tap_result(:on => :success) do |result|
|
133
|
+
# # Create our tags, but still return the result of our update.
|
134
|
+
# attributes[:tags].each do |tag_name|
|
135
|
+
# CreateTaggingCommand.new.call(result.value, tag_name)
|
136
|
+
# end
|
137
|
+
# end.
|
138
|
+
# tap_result(:on => :always) do |result|
|
139
|
+
# # Chaining :on => :always ensures that the command will be run,
|
140
|
+
# # even if the previous result is failing or halted.
|
141
|
+
# if result.failure?
|
142
|
+
# log_errors(
|
143
|
+
# :command => UpdatePostCommand,
|
144
|
+
# :errors => result.errors
|
145
|
+
# )
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
#
|
8
151
|
# @see Cuprum::Command
|
9
152
|
module Chaining
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
153
|
+
# Creates a copy of the first command, and then chains the given command or
|
154
|
+
# block to execute after the first command's implementation. When #call is
|
155
|
+
# executed, each chained command will be called with the previous result
|
156
|
+
# value, and its result property will be set to the previous result. The
|
157
|
+
# return value will be wrapped in a result and returned or yielded to the
|
158
|
+
# next block.
|
159
|
+
#
|
160
|
+
# @return [Cuprum::Chaining] A copy of the command, with the chained
|
161
|
+
# command.
|
162
|
+
#
|
163
|
+
# @see #yield_result
|
164
|
+
#
|
165
|
+
# @overload chain(command, on: nil)
|
166
|
+
# @param command [Cuprum::Command] The command to chain.
|
167
|
+
#
|
168
|
+
# @param on [Symbol] Sets a condition on when the chained block can run,
|
169
|
+
# based on the previous result. Valid values are :success, :failure, and
|
170
|
+
# :always. If the value is :success, the block will be called only if
|
171
|
+
# the previous result succeeded and is not halted. If the value is
|
172
|
+
# :failure, the block will be called only if the previous result failed
|
173
|
+
# and is not halted. If the value is :always, the block will be called
|
174
|
+
# regardless of the previous result status, even if the previous result
|
175
|
+
# is halted. If no value is given, the command will run whether the
|
176
|
+
# previous command was a success or a failure, but not if the command
|
177
|
+
# chain has been halted.
|
178
|
+
#
|
179
|
+
# @overload chain(on: nil) { |value| }
|
180
|
+
# Creates an anonymous command from the given block. The command will be
|
181
|
+
# passed the value of the previous result.
|
182
|
+
#
|
183
|
+
# @param on [Symbol] Sets a condition on when the chained block can run,
|
184
|
+
# based on the previous result. Valid values are :success, :failure, and
|
185
|
+
# :always. If the value is :success, the block will be called only if
|
186
|
+
# the previous result succeeded and is not halted. If the value is
|
187
|
+
# :failure, the block will be called only if the previous result failed
|
188
|
+
# and is not halted. If the value is :always, the block will be called
|
189
|
+
# regardless of the previous result status, even if the previous result
|
190
|
+
# is halted. If no value is given, the command will run whether the
|
191
|
+
# previous command was a success or a failure, but not if the command
|
192
|
+
# chain has been halted.
|
193
|
+
#
|
194
|
+
# @yieldparam value [Object] The value of the previous result.
|
195
|
+
def chain command = nil, on: nil, &block
|
196
|
+
command ||= Cuprum::Command.new(&block)
|
14
197
|
|
15
|
-
# Registers a function or block to run after the current function, or after
|
16
|
-
# the last chained function if the current function already has one or more
|
17
|
-
# chained function(s). This creates and modifies a copy of the current
|
18
|
-
# function.
|
19
|
-
#
|
20
|
-
# @param on [Symbol] Sets a condition on when the chained function can run,
|
21
|
-
# based on the status of the previous function. Valid values are :success,
|
22
|
-
# :failure, and :always. A value of :success will constrain the function
|
23
|
-
# to run only if the previous function succeeded. A value of :failure will
|
24
|
-
# constrain the function to run only if the previous function failed. A
|
25
|
-
# value of :always will ensure the function is always run, even if the
|
26
|
-
# function chain has been halted. If no value is given, the function will
|
27
|
-
# run whether the previous function was a success or a failure, but not if
|
28
|
-
# the function chain has been halted.
|
29
|
-
#
|
30
|
-
# @overload chain(function, on: nil)
|
31
|
-
# The function will be passed the #value of the previous function result
|
32
|
-
# as its parameter, and the result of the chained function will be
|
33
|
-
# returned (or passed to the next chained function, if any).
|
34
|
-
#
|
35
|
-
# @param function [Cuprum::Command] The function to call after the
|
36
|
-
# current or last chained function.
|
37
|
-
#
|
38
|
-
# @overload chain(on: :nil, &block)
|
39
|
-
# The block will be passed the #result of the previous function as its
|
40
|
-
# parameter. If your use case depends on the status of the previous
|
41
|
-
# function or on any errors generated, use the block form of #chain.
|
42
|
-
#
|
43
|
-
# If the block returns a Cuprum::Result (or an object responding to #value
|
44
|
-
# and #success?), the block result will be returned (or passed to the next
|
45
|
-
# chained function, if any). If the block returns any other value
|
46
|
-
# (including nil), the #result of the previous function will be returned
|
47
|
-
# or passed to the next function.
|
48
|
-
#
|
49
|
-
# @yieldparam result [Cuprum::Result] The #result of the previous
|
50
|
-
# function.
|
51
|
-
#
|
52
|
-
# @return [Cuprum::Command] The chained function.
|
53
|
-
def chain function = nil, on: nil, &block
|
54
198
|
clone.tap do |fn|
|
55
|
-
fn.
|
199
|
+
fn.chained_procs <<
|
56
200
|
{
|
57
|
-
:proc =>
|
201
|
+
:proc => chain_command(command),
|
58
202
|
:on => on
|
59
203
|
} # end hash
|
60
204
|
end # tap
|
61
205
|
end # method chain
|
62
206
|
|
63
|
-
# Shorthand for
|
64
|
-
#
|
65
|
-
#
|
207
|
+
# Shorthand for command.chain(:on => :failure). Creates a copy of the first
|
208
|
+
# command, and then chains the given command or block to execute after the
|
209
|
+
# first command's implementation, but only if the previous command is
|
210
|
+
# failing.
|
66
211
|
#
|
67
|
-
# @
|
212
|
+
# @return [Cuprum::Chaining] A copy of the command, with the chained
|
213
|
+
# command.
|
68
214
|
#
|
69
|
-
#
|
70
|
-
#
|
215
|
+
# @see #chain
|
216
|
+
#
|
217
|
+
# @overload failure(command)
|
218
|
+
# @param command [Cuprum::Command] The command to chain.
|
71
219
|
#
|
72
|
-
# @overload
|
220
|
+
# @overload failure() { |value| }
|
221
|
+
# Creates an anonymous command from the given block. The command will be
|
222
|
+
# passed the value of the previous result.
|
73
223
|
#
|
74
|
-
# @yieldparam
|
75
|
-
|
224
|
+
# @yieldparam value [Object] The value of the previous result.
|
225
|
+
def failure command = nil, &block
|
226
|
+
chain(command, :on => :failure, &block)
|
227
|
+
end # method failure
|
228
|
+
|
229
|
+
# Shorthand for command.chain(:on => :success). Creates a copy of the first
|
230
|
+
# command, and then chains the given command or block to execute after the
|
231
|
+
# first command's implementation, but only if the previous command is
|
232
|
+
# failing.
|
76
233
|
#
|
77
|
-
# @return [Cuprum::
|
234
|
+
# @return [Cuprum::Chaining] A copy of the command, with the chained
|
235
|
+
# command.
|
78
236
|
#
|
79
237
|
# @see #chain
|
80
|
-
def else function = nil, &block
|
81
|
-
chain(function, :on => :failure, &block)
|
82
|
-
end # method else
|
83
|
-
|
84
|
-
# Shorthand for function.chain(:on => :success). Registers a function or
|
85
|
-
# block to run after the current function. The chained function will only
|
86
|
-
# run if the previous function was successfully run.
|
87
238
|
#
|
88
|
-
# @overload
|
239
|
+
# @overload success(command)
|
240
|
+
# @param command [Cuprum::Command] The command to chain.
|
89
241
|
#
|
90
|
-
#
|
91
|
-
#
|
242
|
+
# @overload success() { |value| }
|
243
|
+
# Creates an anonymous command from the given block. The command will be
|
244
|
+
# passed the value of the previous result.
|
92
245
|
#
|
93
|
-
#
|
246
|
+
# @yieldparam value [Object] The value of the previous result.
|
247
|
+
def success command = nil, &block
|
248
|
+
chain(command, :on => :success, &block)
|
249
|
+
end # method success
|
250
|
+
|
251
|
+
# As #yield_result, but always returns the previous result when the block is
|
252
|
+
# called. The return value of the block is discarded.
|
94
253
|
#
|
95
|
-
#
|
96
|
-
# function.
|
254
|
+
# @param (see #yield_result)
|
97
255
|
#
|
98
|
-
# @
|
256
|
+
# @yieldparam result [Cuprum::Result] The #result of the previous command.
|
99
257
|
#
|
100
|
-
# @see #
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
protected
|
258
|
+
# @return (see #yield_result)
|
259
|
+
#
|
260
|
+
# @see #yield_result
|
261
|
+
def tap_result on: nil, &block
|
262
|
+
tapped = ->(result) { result.tap { block.call(result) } }
|
106
263
|
|
107
|
-
|
108
|
-
|
109
|
-
|
264
|
+
clone.tap do |fn|
|
265
|
+
fn.chained_procs <<
|
266
|
+
{
|
267
|
+
:proc => tapped,
|
268
|
+
:on => on
|
269
|
+
} # end hash
|
270
|
+
end # tap
|
271
|
+
end # method tap_result
|
110
272
|
|
111
|
-
|
273
|
+
# Creates a copy of the command, and then chains the block to execute after
|
274
|
+
# the command implementation. When #call is executed, each chained block
|
275
|
+
# will be yielded the previous result, and the return value wrapped in a
|
276
|
+
# result and returned or yielded to the next block.
|
277
|
+
#
|
278
|
+
# @param on [Symbol] Sets a condition on when the chained block can run,
|
279
|
+
# based on the previous result. Valid values are :success, :failure, and
|
280
|
+
# :always. If the value is :success, the block will be called only if the
|
281
|
+
# previous result succeeded and is not halted. If the value is :failure,
|
282
|
+
# the block will be called only if the previous result failed and is not
|
283
|
+
# halted. If the value is :always, the block will be called regardless of
|
284
|
+
# the previous result status, even if the previous result is halted. If no
|
285
|
+
# value is given, the command will run whether the previous command was a
|
286
|
+
# success or a failure, but not if the command chain has been halted.
|
287
|
+
#
|
288
|
+
# @yieldparam result [Cuprum::Result] The #result of the previous command.
|
289
|
+
#
|
290
|
+
# @return [Cuprum::Chaining] A copy of the command, with the chained block.
|
291
|
+
#
|
292
|
+
# @see #tap_result
|
293
|
+
def yield_result on: nil, &block
|
294
|
+
clone.tap do |fn|
|
295
|
+
fn.chained_procs <<
|
296
|
+
{
|
297
|
+
:proc => block,
|
298
|
+
:on => on
|
299
|
+
} # end hash
|
300
|
+
end # tap
|
301
|
+
end # method yield_result
|
112
302
|
|
113
|
-
|
114
|
-
chained_functions.reduce(first_result) do |result, hsh|
|
115
|
-
next result if skip_chained_function?(result, :on => hsh[:on])
|
303
|
+
protected
|
116
304
|
|
117
|
-
|
305
|
+
def chained_procs
|
306
|
+
@chained_procs ||= []
|
307
|
+
end # method chained_procs
|
118
308
|
|
119
|
-
|
120
|
-
|
121
|
-
end # method
|
309
|
+
def process_with_result *args, &block
|
310
|
+
yield_chain(super)
|
311
|
+
end # method call
|
122
312
|
|
123
|
-
|
124
|
-
return function_or_proc if function_or_proc.is_a?(Proc)
|
313
|
+
private
|
125
314
|
|
126
|
-
|
127
|
-
|
315
|
+
def chain_command command
|
316
|
+
if command.arity.zero?
|
317
|
+
->(result) { command.process_with_result(result) }
|
318
|
+
else
|
319
|
+
->(result) { command.process_with_result(result, result.value) }
|
320
|
+
end # if-else
|
321
|
+
end # method chain_command
|
128
322
|
|
129
|
-
def
|
323
|
+
def skip_chained_proc? last_result, on:
|
130
324
|
return false if on == :always
|
131
325
|
|
132
326
|
return true if last_result.respond_to?(:halted?) && last_result.halted?
|
@@ -137,6 +331,20 @@ module Cuprum
|
|
137
331
|
when :failure
|
138
332
|
!last_result.failure?
|
139
333
|
end # case
|
140
|
-
end # method
|
334
|
+
end # method skip_chained_proc?
|
335
|
+
|
336
|
+
def yield_chain first_result
|
337
|
+
chained_procs.reduce(first_result) do |result, hsh|
|
338
|
+
next result if skip_chained_proc?(result, :on => hsh[:on])
|
339
|
+
|
340
|
+
value = hsh.fetch(:proc).call(result)
|
341
|
+
|
342
|
+
if value_is_result?(value)
|
343
|
+
value.to_result
|
344
|
+
else
|
345
|
+
build_result(value, :errors => build_errors)
|
346
|
+
end # if-else
|
347
|
+
end # reduce
|
348
|
+
end # method yield_chain
|
141
349
|
end # module
|
142
350
|
end # modue
|