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.
@@ -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