lev 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  def handler_errors
2
- @errors || Lev::Handler::Errors.new
2
+ @errors || (@handler_result ? @handler_result.errors : Lev::Errors.new)
3
3
  end
@@ -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