lev 0.0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/.rspec +2 -0
- data/Gemfile.lock +32 -1
- data/README.md +53 -2
- data/lev.gemspec +9 -3
- data/lib/lev.rb +10 -7
- data/lib/lev/delegate_to_routine.rb +25 -0
- data/lib/lev/error.rb +28 -0
- data/lib/lev/error_transferer.rb +41 -0
- data/lib/lev/{handler/error_translator.rb → error_translator.rb} +1 -1
- data/lib/lev/errors.rb +41 -0
- data/lib/lev/exceptions.rb +3 -1
- data/lib/lev/form_builder.rb +3 -3
- data/lib/lev/handle_with.rb +5 -6
- data/lib/lev/handler.rb +55 -53
- data/lib/lev/handler_helper.rb +1 -1
- data/lib/lev/routine.rb +424 -0
- data/lib/lev/term_mapper.rb +41 -0
- data/lib/lev/utilities.rb +14 -0
- data/lib/lev/version.rb +1 -1
- data/spec/create_sprocket_spec.rb +9 -0
- data/spec/deep_merge_spec.rb +41 -0
- data/spec/routine_spec.rb +7 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/sprocket_spec.rb +11 -0
- data/spec/support/create_sprocket.rb +14 -0
- data/spec/support/sprocket.rb +10 -0
- metadata +108 -14
- data/lib/lev/algorithm.rb +0 -26
- data/lib/lev/delegate_to_algorithm.rb +0 -25
- data/lib/lev/handler/error.rb +0 -30
- data/lib/lev/handler/error_transferer.rb +0 -28
- data/lib/lev/handler/errors.rb +0 -19
- data/lib/lev/routine_nesting.rb +0 -127
data/lib/lev/handler_helper.rb
CHANGED
data/lib/lev/routine.rb
ADDED
@@ -0,0 +1,424 @@
|
|
1
|
+
module Lev
|
2
|
+
|
3
|
+
# A "routine" in the Lev world is a piece of code that is responsible for
|
4
|
+
# doing one thing, normally acting on one or more other objects. Routines
|
5
|
+
# are particularly useful when the thing that needs to be done involves
|
6
|
+
# making changes to multiple other objects. In an OO/MVC world, an operation
|
7
|
+
# that involves multiple objects might be implemented by spreading that logic
|
8
|
+
# among those objects. However, that leads to classes having more
|
9
|
+
# responsibilities than they should (and more knowlege of other classes than
|
10
|
+
# they should) as well as making the code hard to follow.
|
11
|
+
#
|
12
|
+
# Routines typically don't have any persistent state that is used over and
|
13
|
+
# over again; they are created, used, and forgotten. A routine is a glorified
|
14
|
+
# function with a special single-responsibility purpose.
|
15
|
+
#
|
16
|
+
# Routines can be nested -- there is built-in functionality for calling
|
17
|
+
# one routine inside another.
|
18
|
+
#
|
19
|
+
# A class becomes a routine by adding:
|
20
|
+
#
|
21
|
+
# include Lev::Routine
|
22
|
+
#
|
23
|
+
# in its definition.
|
24
|
+
#
|
25
|
+
# Other than that, all a routine has to do is implement an "exec" method
|
26
|
+
# that takes arbitrary arguments and that adds errors to an internal
|
27
|
+
# array-like "errors" object and outputs to a "outputs" hash.
|
28
|
+
#
|
29
|
+
# A routine returns an "Result" object, which is just a simple wrapper
|
30
|
+
# of the outputs and errors objects.
|
31
|
+
#
|
32
|
+
# A routine will automatically get both class- and instance-level "call"
|
33
|
+
# methods that take the same arguments as the "exec" method. The class-level
|
34
|
+
# call method simply instantiates a new instance of the routine and calls
|
35
|
+
# the instance-level call method (side note here is that this means that
|
36
|
+
# routines aren't typically instantiated with state).
|
37
|
+
#
|
38
|
+
# A routine is automatically run within a transaction. The isolation level
|
39
|
+
# of the routine can be set by overriding the "default_transaction_isolation"
|
40
|
+
# class method and having it return an instance of Lev::TransactionIsolation.
|
41
|
+
# This is also how routines can be set to not be run in a transaction.
|
42
|
+
#
|
43
|
+
# As mentioned above, routines can call other routines. While this is of
|
44
|
+
# course possible just by calling the other routine's call method directly,
|
45
|
+
# it is strongly recommended that one routine call another routine using the
|
46
|
+
# provided "run" method. This method takes the name of the routine class
|
47
|
+
# and the arguments/block it expects in its call/exec methods. By using the
|
48
|
+
# run method, the called routine will be hooked into the common error and
|
49
|
+
# transaction mechanisms.
|
50
|
+
#
|
51
|
+
# When one routine is called within another using the run method, there is
|
52
|
+
# only one transaction used (barring any explicitly made in the code) and
|
53
|
+
# its isolation level is sufficiently strict for all routines involved.
|
54
|
+
#
|
55
|
+
# It is highly recommend, though not required, to call the "uses_routine"
|
56
|
+
# method to let the routine know which subroutines will be called within it.
|
57
|
+
# This will let a routine set its isolation level appropriately, and will
|
58
|
+
# enforce that only one transaction be used and that it be rolled back
|
59
|
+
# appropriately if any errors occur.
|
60
|
+
#
|
61
|
+
# Once a routine has been registered with the "uses_routine" call, it can
|
62
|
+
# be run by passing run the routine's Class or a symbol identifying the
|
63
|
+
# routine. This symbol can be set with the :as option. If not set, the
|
64
|
+
# symbol will be automatically set by converting the routine class' full
|
65
|
+
# name to a symbol. e.g:
|
66
|
+
#
|
67
|
+
# uses_routine CreateUser
|
68
|
+
# as: :cu
|
69
|
+
#
|
70
|
+
# and then you can say either:
|
71
|
+
#
|
72
|
+
# run(:cu, ...)
|
73
|
+
#
|
74
|
+
# or
|
75
|
+
#
|
76
|
+
# run(:create_user, ...)
|
77
|
+
#
|
78
|
+
# uses_routine also provides a way to specify how errors relate to routine
|
79
|
+
# inputs. Take the following example. A user calls Routine1 which calls
|
80
|
+
# Routine2.
|
81
|
+
#
|
82
|
+
# User --> Routine1.call(foo: "abcd4") --> Routine2.call(bar: "abcd4")
|
83
|
+
#
|
84
|
+
# An error occurs in Routine2, and Routine2 notes that the error is related
|
85
|
+
# to its "bar" input. If that error and its metadata bubble up to the User,
|
86
|
+
# the User won't have any idea what "bar" relates to -- the User only knows
|
87
|
+
# about the interface to Routine1 and the "foo" parameter it gave it.
|
88
|
+
#
|
89
|
+
# Routine1 knows that it will call Routine2 and knows what its interface is.
|
90
|
+
# It can then specify how to map terminology from Routine2 into Routine1's
|
91
|
+
# context. E.g., in the following class:
|
92
|
+
#
|
93
|
+
# class Routine1
|
94
|
+
# include Lev::Routine
|
95
|
+
# uses_routine Routine2,
|
96
|
+
# translations: {
|
97
|
+
# inputs: { map: {bar: :foo} }
|
98
|
+
# }
|
99
|
+
# def exec(options)
|
100
|
+
# run(Routine2, bar: options[:foo])
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# Routine1 notes that any errors coming back from the call to Routine2
|
105
|
+
# related to :bar should be transfered into Routine1's errors object
|
106
|
+
# as being related to :foo. In this way, the caller of Routine1 will see
|
107
|
+
# errors related to the arguments he understands.
|
108
|
+
#
|
109
|
+
# Translations can also be supplied for "outputs" in addition to "inputs".
|
110
|
+
# Output translations control how a called routine's Result outputs are
|
111
|
+
# transfered to the calling routine's outputs. Note if multiple outputs are
|
112
|
+
# transferred into the same named output, an array of those outputs will be
|
113
|
+
# store. The contents of the "inputs" and "outputs" hashes can be of the
|
114
|
+
# following form:
|
115
|
+
#
|
116
|
+
# 1) Scoped. Appends the provided scoping symbol (or symbol array) to
|
117
|
+
# the input symbol.
|
118
|
+
#
|
119
|
+
# {scope: SCOPING_SYMBOL_OR_SYMBOL_ARRAY}
|
120
|
+
#
|
121
|
+
# e.g. with {scope: :register} and a call to a routine that has an input
|
122
|
+
# named :first_name, an error in that called routine related to its
|
123
|
+
# :first_name input will be translated so that the offending input is
|
124
|
+
# [:register, :first_name].
|
125
|
+
#
|
126
|
+
# 2) Verbatim. Uses the same term in the caller as the callee.
|
127
|
+
#
|
128
|
+
# {type: :verbatim}
|
129
|
+
#
|
130
|
+
# 3) Mapped. Give an explicit, custom mapping:
|
131
|
+
#
|
132
|
+
# {map: {called_input1: caller_input1, called_input2: :caller_input2}}
|
133
|
+
#
|
134
|
+
# 4) Scoped and mapped. Give an explicit mapping, and also scope the
|
135
|
+
# translated terms. Just use scope: and map: from above in the same hash.
|
136
|
+
#
|
137
|
+
# Via the uses_routine call, you can also ignore specified errors that occur
|
138
|
+
# in the called routine. e.g.:
|
139
|
+
#
|
140
|
+
# uses_routine DestroyUser,
|
141
|
+
# ignored_errors: [:cannot_destroy_non_temp_user]
|
142
|
+
#
|
143
|
+
# ignores errors with the provided code. The ignore_errors key must point
|
144
|
+
# to an array of code symbols or procs. If a proc is given, the proc will
|
145
|
+
# be called with the error that the routine is trying to add. If the proc
|
146
|
+
# returns true, the error will be ignored.
|
147
|
+
#
|
148
|
+
# Any option passed to uses_routine can also be passed directly to the run
|
149
|
+
# method. To achieve this, pass an array as the first argument to "run".
|
150
|
+
# The array should have the routine class or symbol as the first argument,
|
151
|
+
# and the hash of options as the second argument. Options passed in this
|
152
|
+
# manner override any options provided in uses_routine (though those options
|
153
|
+
# are still used if not replaced in the run call).
|
154
|
+
#
|
155
|
+
# Two methods are provided for adding errors: "fatal_error" and "nonfatal_error".
|
156
|
+
# Both take a hash of args used to create an Error and the former stops routine
|
157
|
+
# execution. In its current implementation, "nonfatal_error" may still cause
|
158
|
+
# a routine higher up in the execution hierarchy to halt running.
|
159
|
+
#
|
160
|
+
# Routine class have access to a few other methods:
|
161
|
+
#
|
162
|
+
# 1) a "runner" accessor which points to the routine which called it. If
|
163
|
+
# runner is nil that means that no other routine called it (some other
|
164
|
+
# code did)
|
165
|
+
#
|
166
|
+
# 2) a "topmost_runner" which points to the highest routine in the calling
|
167
|
+
# hierarchy (that routine whose 'runner' is nil)
|
168
|
+
#
|
169
|
+
# References:
|
170
|
+
# http://ducktypo.blogspot.com/2010/08/why-inheritance-sucks.html
|
171
|
+
#
|
172
|
+
module Routine
|
173
|
+
|
174
|
+
class Result
|
175
|
+
attr_reader :outputs
|
176
|
+
attr_reader :errors
|
177
|
+
|
178
|
+
def initialize
|
179
|
+
@outputs = {}
|
180
|
+
@errors = Errors.new
|
181
|
+
end
|
182
|
+
|
183
|
+
def add_output(name, value)
|
184
|
+
outputs[name] = [outputs[name], value].flatten.compact
|
185
|
+
outputs[name] = outputs[name].first if outputs[name].size == 1
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.included(base)
|
190
|
+
base.extend(ClassMethods)
|
191
|
+
end
|
192
|
+
|
193
|
+
module ClassMethods
|
194
|
+
def call(*args, &block)
|
195
|
+
new.call(*args, &block)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Called at a routine's class level to foretell which other routines will
|
199
|
+
# be used when this routine executes. Helpful for figuring out ahead of
|
200
|
+
# time what kind of transaction isolation level should be used.
|
201
|
+
def uses_routine(routine_class, options={})
|
202
|
+
symbol = options[:as] || class_to_symbol(routine_class)
|
203
|
+
|
204
|
+
raise IllegalArgument, "Routine #{routine_class} has already been registered" \
|
205
|
+
if nested_routines[symbol]
|
206
|
+
|
207
|
+
nested_routines[symbol] = {
|
208
|
+
routine_class: routine_class,
|
209
|
+
options: options
|
210
|
+
}
|
211
|
+
|
212
|
+
transaction_isolation.replace_if_more_isolated(routine_class.transaction_isolation)
|
213
|
+
end
|
214
|
+
|
215
|
+
def transaction_isolation
|
216
|
+
@transaction_isolation ||= default_transaction_isolation
|
217
|
+
end
|
218
|
+
|
219
|
+
def default_transaction_isolation
|
220
|
+
TransactionIsolation.mysql_default
|
221
|
+
end
|
222
|
+
|
223
|
+
def nested_routines
|
224
|
+
@nested_routines ||= {}
|
225
|
+
end
|
226
|
+
|
227
|
+
def class_to_symbol(klass)
|
228
|
+
klass.name.underscore.gsub('/','_').to_sym
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
attr_reader :runner
|
233
|
+
|
234
|
+
def call(*args, &block)
|
235
|
+
in_transaction do
|
236
|
+
catch :fatal_errors_encountered do
|
237
|
+
exec(*args, &block)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
result
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns true iff the given instance is responsible for running itself in a
|
245
|
+
# transaction
|
246
|
+
def transaction_run_by?(who)
|
247
|
+
who == topmost_runner && who.class.transaction_isolation != TransactionIsolation.no_transaction
|
248
|
+
end
|
249
|
+
|
250
|
+
def run(other_routine, *args, &block)
|
251
|
+
options = {}
|
252
|
+
|
253
|
+
if other_routine.is_a? Array
|
254
|
+
if other_routine.size != 2
|
255
|
+
raise IllegalArgument, "when first arg to run is an array, it must have two arguments"
|
256
|
+
end
|
257
|
+
|
258
|
+
other_routine = other_routine[0]
|
259
|
+
options = other_routine[1]
|
260
|
+
end
|
261
|
+
|
262
|
+
symbol = case other_routine
|
263
|
+
when Symbol
|
264
|
+
other_routine
|
265
|
+
when Class
|
266
|
+
self.class.class_to_symbol(other_routine)
|
267
|
+
else
|
268
|
+
self.class.class_to_symbol(other_routine.class)
|
269
|
+
end
|
270
|
+
|
271
|
+
nested_routine = self.class.nested_routines[symbol] || {}
|
272
|
+
|
273
|
+
if nested_routine.empty? && other_routine == symbol
|
274
|
+
raise IllegalArgument,
|
275
|
+
"Routine symbol #{other_routine} does not point to a registered routine"
|
276
|
+
end
|
277
|
+
|
278
|
+
#
|
279
|
+
# Get an instance of the routine and make sure it is a routine
|
280
|
+
#
|
281
|
+
|
282
|
+
other_routine = nested_routine[:routine_class] || other_routine
|
283
|
+
other_routine = other_routine.new if other_routine.is_a? Class
|
284
|
+
|
285
|
+
raise IllegalArgument, "Can only run another nested routine" \
|
286
|
+
if !(other_routine.includes_module? Lev::Routine)
|
287
|
+
|
288
|
+
#
|
289
|
+
# Merge passed-in options with those set in uses_routine, the former taking
|
290
|
+
# priority.
|
291
|
+
#
|
292
|
+
|
293
|
+
nested_routine_options = nested_routine[:options] || {}
|
294
|
+
options = Lev::Utilities.deep_merge(nested_routine_options, options)
|
295
|
+
|
296
|
+
#
|
297
|
+
# Setup the input/output mappers
|
298
|
+
#
|
299
|
+
|
300
|
+
options[:translations] ||= {}
|
301
|
+
|
302
|
+
input_mapper = new_term_mapper(options[:translations][:inputs]) ||
|
303
|
+
new_term_mapper({ scope: symbol })
|
304
|
+
|
305
|
+
output_mapper = new_term_mapper(options[:translations][:outputs]) ||
|
306
|
+
new_term_mapper({ scope: symbol })
|
307
|
+
|
308
|
+
#
|
309
|
+
# Set up the ignored errors in the routine instance
|
310
|
+
#
|
311
|
+
|
312
|
+
(options[:ignored_errors] || []).each do |ignored_error|
|
313
|
+
other_routine.errors.ignore(ignored_error)
|
314
|
+
end
|
315
|
+
|
316
|
+
#
|
317
|
+
# Attach the subroutine to self, call it, transfer errors and results
|
318
|
+
#
|
319
|
+
|
320
|
+
other_routine.runner = self
|
321
|
+
run_result = other_routine.call(*args, &block)
|
322
|
+
|
323
|
+
options[:errors_are_fatal] = true if !options.has_key?(:errors_are_fatal)
|
324
|
+
transfer_errors_from(run_result.errors, input_mapper, options[:errors_are_fatal])
|
325
|
+
|
326
|
+
run_result.outputs.each do |name, value|
|
327
|
+
self.result.add_output(output_mapper.map(name), value)
|
328
|
+
end
|
329
|
+
|
330
|
+
run_result
|
331
|
+
end
|
332
|
+
|
333
|
+
# Convenience accessor for errors object
|
334
|
+
def errors
|
335
|
+
result.errors
|
336
|
+
end
|
337
|
+
|
338
|
+
# Convenience test for presence of errors
|
339
|
+
def errors?
|
340
|
+
result.errors.any?
|
341
|
+
end
|
342
|
+
|
343
|
+
def fatal_error(args={})
|
344
|
+
errors.add(true, args)
|
345
|
+
end
|
346
|
+
|
347
|
+
def nonfatal_error(args={})
|
348
|
+
errors.add(false, args)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Utility method to transfer errors from a source to this routine. The
|
352
|
+
# provided input_mapper maps the language of the errors in the source to
|
353
|
+
# the language of this routine. If fail_if_errors is true, this routine
|
354
|
+
# will throw an error condition that causes execution of this routine to stop
|
355
|
+
# *after* having transfered all of the errors.
|
356
|
+
def transfer_errors_from(source, input_mapper, fail_if_errors=false)
|
357
|
+
if input_mapper.is_a? Hash
|
358
|
+
input_mapper = new_term_mapper(input_mapper)
|
359
|
+
end
|
360
|
+
|
361
|
+
ErrorTransferer.transfer(source, self, input_mapper, fail_if_errors)
|
362
|
+
end
|
363
|
+
|
364
|
+
protected
|
365
|
+
|
366
|
+
def result
|
367
|
+
@result ||= Result.new
|
368
|
+
end
|
369
|
+
|
370
|
+
attr_writer :runner
|
371
|
+
|
372
|
+
def outputs
|
373
|
+
result.outputs
|
374
|
+
end
|
375
|
+
|
376
|
+
def topmost_runner
|
377
|
+
runner.nil? ? self : runner.topmost_runner
|
378
|
+
end
|
379
|
+
|
380
|
+
def runner=(runner)
|
381
|
+
@runner = runner
|
382
|
+
|
383
|
+
if topmost_runner.class.transaction_isolation.weaker_than(self.class.default_transaction_isolation)
|
384
|
+
raise IsolationMismatch,
|
385
|
+
"The routine being run has a stronger isolation requirement than " +
|
386
|
+
"the isolation being used by the routine(s) running it; call the " +
|
387
|
+
"'uses' method in the running routine's initializer"
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def in_transaction(options={})
|
392
|
+
if transaction_run_by?(self)
|
393
|
+
ActiveRecord::Base.isolation_level( self.class.transaction_isolation.symbol ) do
|
394
|
+
ActiveRecord::Base.transaction do
|
395
|
+
yield
|
396
|
+
raise ActiveRecord::Rollback if errors?
|
397
|
+
end
|
398
|
+
end
|
399
|
+
else
|
400
|
+
yield
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def new_term_mapper(options)
|
405
|
+
return nil if options.nil?
|
406
|
+
|
407
|
+
if options[:type]
|
408
|
+
case options[:type]
|
409
|
+
when :verbatim
|
410
|
+
return TermMapper.verbatim
|
411
|
+
else
|
412
|
+
raise IllegalArgument, "unknown :type value: #{options[:type]}"
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
if options[:scope] || options[:map]
|
417
|
+
return TermMapper.scope_and_map(options[:scope], options[:map])
|
418
|
+
end
|
419
|
+
|
420
|
+
nil
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|
424
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Lev
|
2
|
+
|
3
|
+
class TermMapper
|
4
|
+
|
5
|
+
def self.verbatim
|
6
|
+
ScopedAndMapped.new(nil, nil)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.scope_and_map(scope, mapping)
|
10
|
+
ScopedAndMapped.new(scope, mapping)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.scope(scope)
|
14
|
+
ScopedAndMapped.new(scope, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def map(inputs)
|
18
|
+
raise AbstractMethodCalled
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
class ScopedAndMapped < TermMapper
|
24
|
+
def initialize(scope=nil, mapping=nil)
|
25
|
+
@scope = scope
|
26
|
+
@mapping = mapping
|
27
|
+
end
|
28
|
+
|
29
|
+
def map(inputs)
|
30
|
+
inputs = [inputs].flatten.compact
|
31
|
+
result = inputs.collect do |input|
|
32
|
+
mapped = (@mapping || {})[input] || input
|
33
|
+
@scope.nil? ? mapped : [@scope, mapped].flatten
|
34
|
+
end
|
35
|
+
result.size == 1 ? result.first : result
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|