lev 0.0.3 → 1.0.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.
- 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
|