Module: Lev::Routine
- Defined in:
- lib/lev/routine.rb
Overview
A “routine” in the Lev world is a piece of code that is responsible for doing one thing, normally acting on one or more other objects. Routines are particularly useful when the thing that needs to be done involves making changes to multiple other objects. In an OO/MVC world, an operation that involves multiple objects might be implemented by spreading that logic among those objects. However, that leads to classes having more responsibilities than they should (and more knowlege of other classes than they should) as well as making the code hard to follow.
Routines typically don’t have any persistent state that is used over and over again; they are created, used, and forgotten. A routine is a glorified function with a special single-responsibility purpose.
Routines can be nested – there is built-in functionality for calling one routine inside another.
A class becomes a routine by adding:
lev_routine
in its definition.
Other than that, all a routine has to do is implement an “exec” method that takes arbitrary arguments and that adds errors to an internal array-like “errors” object and outputs to a “outputs” hash.
A routine returns an “Result” object, which is just a simple wrapper of the outputs and errors objects.
A routine will automatically get both class- and instance-level “call” methods that take the same arguments as the “exec” method. The class-level call method simply instantiates a new instance of the routine and calls the instance-level call method (side note here is that this means that routines aren’t typically instantiated with state).
A routine is automatically run within a transaction. The isolation level of the routine can be set by passing a :transaction option to the lev_routine call (or to the lev_handler call, if appropriate). The value must be one of
:no_transaction
:read_uncommitted
:read_committed
:repeatable_read
:serializable
e.g.
class MyRoutine
lev_routine transaction: :no_transaction
As mentioned above, routines can call other routines. While this is of course possible just by calling the other routine’s call method directly, it is strongly recommended that one routine call another routine using the provided “run” method. This method takes the name of the routine class and the arguments/block it expects in its call/exec methods. By using the run method, the called routine will be hooked into the common error and transaction mechanisms.
When one routine is called within another using the run method, there is only one transaction used (barring any explicitly made in the code) and its isolation level is sufficiently strict for all routines involved.
It is highly recommend, though not required, to call the “uses_routine” method to let the routine know which subroutines will be called within it. This will let a routine set its isolation level appropriately, and will enforce that only one transaction be used and that it be rolled back appropriately if any errors occur.
Once a routine has been registered with the “uses_routine” call, it can be run by passing run the routine’s Class or a symbol identifying the routine. This symbol can be set with the :as option. If not set, the symbol will be automatically set by converting the routine class’ full name to a symbol. e.g:
uses_routine CreateUser
as: :cu
and then you can say either:
run(:cu, ...)
or
run(:create_user, ...)
uses_routine also provides a way to specify how errors relate to routine inputs. Take the following example. A user calls Routine1 which calls Routine2.
User --> Routine1.call(foo: "abcd4") --> Routine2.call(bar: "abcd4")
An error occurs in Routine2, and Routine2 notes that the error is related to its “bar” input. If that error and its metadata bubble up to the User, the User won’t have any idea what “bar” relates to – the User only knows about the interface to Routine1 and the “foo” parameter it gave it.
Routine1 knows that it will call Routine2 and knows what its interface is. It can then specify how to map terminology from Routine2 into Routine1’s context. E.g., in the following class:
class Routine1
lev_routine
uses_routine Routine2,
translations: {
inputs: { map: {bar: :foo} }
}
def exec()
run(Routine2, bar: [:foo])
end
end
Routine1 notes that any errors coming back from the call to Routine2 related to :bar should be transfered into Routine1’s errors object as being related to :foo. In this way, the caller of Routine1 will see errors related to the arguments he understands.
Translations can also be supplied for “outputs” in addition to “inputs”. Output translations control how a called routine’s Result outputs are transfered to the calling routine’s outputs. Note if multiple outputs are transferred into the same named output, an array of those outputs will be store. The contents of the “inputs” and “outputs” hashes can be of the following form:
1) Scoped. Appends the provided scoping symbol (or symbol array) to
the input symbol.
{scope: SCOPING_SYMBOL_OR_SYMBOL_ARRAY}
e.g. with {scope: :register} and a call to a routine that has an input
named :first_name, an error in that called routine related to its
:first_name input will be translated so that the offending input is
[:register, :first_name].
2) Verbatim. Uses the same term in the caller as the callee.
{type: :verbatim}
3) Mapped. Give an explicit, custom mapping:
{map: {called_input1: caller_input1, called_input2: :caller_input2}}
4) Scoped and mapped. Give an explicit mapping, and also scope the
translated terms. Just use scope: and map: from above in the same hash.
Via the uses_routine call, you can also ignore specified errors that occur in the called routine. e.g.:
uses_routine DestroyUser,
ignored_errors: [:cannot_destroy_non_temp_user]
ignores errors with the provided code. The ignore_errors key must point to an array of code symbols or procs. If a proc is given, the proc will be called with the error that the routine is trying to add. If the proc returns true, the error will be ignored.
Any option passed to uses_routine can also be passed directly to the run method. To achieve this, pass an array as the first argument to “run”. The array should have the routine class or symbol as the first argument, and the hash of options as the second argument. Options passed in this manner override any options provided in uses_routine (though those options are still used if not replaced in the run call).
Two methods are provided for adding errors: “fatal_error” and “nonfatal_error”. Both take a hash of args used to create an Error and the former stops routine execution. In its current implementation, “nonfatal_error” may still cause a routine higher up in the execution hierarchy to halt running.
Routine class have access to a few other methods:
1) a "runner" accessor which points to the routine which called it. If
runner is nil that means that no other routine called it (some other
code did)
2) a "topmost_runner" which points to the highest routine in the calling
hierarchy (that routine whose 'runner' is nil)
References:
http://ducktypo.blogspot.com/2010/08/why-inheritance-sucks.html
Defined Under Namespace
Modules: ClassMethods Classes: Result
Instance Attribute Summary collapse
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#runner ⇒ Object
readonly
Returns the value of attribute runner.
Class Method Summary collapse
Instance Method Summary collapse
- #add_after_transaction_block(block) ⇒ Object
- #call(*args, **kwargs, &block) ⇒ Object
-
#errors ⇒ Object
Convenience accessor for errors object.
-
#errors? ⇒ Boolean
Convenience test for presence of errors.
- #fatal_error(args = {}) ⇒ Object
-
#initialize(status = nil) ⇒ Object
Note that the parent may neglect to call super, leading to this method never being called.
- #nonfatal_error(args = {}) ⇒ Object
- #run(other_routine, *args, **kwargs, &block) ⇒ Object
-
#transaction_run_by?(who) ⇒ Boolean
Returns true iff the given instance is responsible for running itself in a transaction.
-
#transfer_errors_from(source, input_mapper, fail_if_errors = false) ⇒ Object
Utility method to transfer errors from a source to this routine.
Instance Attribute Details
#id ⇒ Object (readonly)
Returns the value of attribute id.
193 194 195 |
# File 'lib/lev/routine.rb', line 193 def id @id end |
#runner ⇒ Object
Returns the value of attribute runner.
280 281 282 |
# File 'lib/lev/routine.rb', line 280 def runner @runner end |
Class Method Details
.included(base) ⇒ Object
195 196 197 198 199 200 |
# File 'lib/lev/routine.rb', line 195 def self.included(base) base.extend(ClassMethods) base.class_attribute :create_status_proc, :find_status_proc base.create_status_proc = ->(*) { NullStatus.new } base.find_status_proc = ->(*) { NullStatus.new } end |
Instance Method Details
#add_after_transaction_block(block) ⇒ Object
441 442 443 444 |
# File 'lib/lev/routine.rb', line 441 def add_after_transaction_block(block) raise IllegalOperation if topmost_runner != self @after_transaction_blocks.push(block) end |
#call(*args, **kwargs, &block) ⇒ Object
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/lev/routine.rb', line 282 def call(*args, **kwargs, &block) @after_transaction_blocks = [] status.started! begin in_transaction do reset_result! if transaction_run_by?(self) catch :fatal_errors_encountered do if self.class.delegates_to run(self.class.delegates_to, *args, **kwargs, &block) else exec(*args, **kwargs, &block) end end end @after_transaction_blocks.each do |block| block.call end rescue Exception => e # Let exceptions escape but make sure to note the error in the status if not already done if !e.is_a?(Lev::FatalError) error = Error.new(code: :exception, message: e., data: e.backtrace&.first) status.add_error(error) status.failed! end raise e end status.succeeded! if !errors? result end |
#errors ⇒ Object
Convenience accessor for errors object
411 412 413 |
# File 'lib/lev/routine.rb', line 411 def errors result.errors end |
#errors? ⇒ Boolean
Convenience test for presence of errors
416 417 418 |
# File 'lib/lev/routine.rb', line 416 def errors? result.errors.any? end |
#fatal_error(args = {}) ⇒ Object
420 421 422 |
# File 'lib/lev/routine.rb', line 420 def fatal_error(args={}) errors.add(true, args) end |
#initialize(status = nil) ⇒ Object
Note that the parent may neglect to call super, leading to this method never being called. Do not perform any initialization here that cannot be safely skipped
448 449 450 451 452 |
# File 'lib/lev/routine.rb', line 448 def initialize(status = nil) # If someone cares about the status, they'll pass it in; otherwise all # status updates go into the bit bucket. @status = status end |
#nonfatal_error(args = {}) ⇒ Object
424 425 426 |
# File 'lib/lev/routine.rb', line 424 def nonfatal_error(args={}) errors.add(false, args) end |
#run(other_routine, *args, **kwargs, &block) ⇒ Object
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/lev/routine.rb', line 327 def run(other_routine, *args, **kwargs, &block) = {} if other_routine.is_a? Array if other_routine.size != 2 raise Lev.configuration.illegal_argument_error, "when first arg to run is an array, it must have two arguments" end other_routine = other_routine[0] = other_routine[1] end symbol = case other_routine when Symbol other_routine when Class self.class.class_to_symbol(other_routine) else self.class.class_to_symbol(other_routine.class) end nested_routine = self.class.nested_routines[symbol] || {} if nested_routine.empty? && other_routine == symbol raise Lev.configuration.illegal_argument_error, "Routine symbol #{other_routine} does not point to a registered routine" end # # Get an instance of the routine and make sure it is a routine # other_routine = nested_routine[:routine_class] || other_routine other_routine = other_routine.new if other_routine.is_a? Class if !(other_routine.includes_module? Lev::Routine) raise Lev.configuration.illegal_argument_error, "Can only run another nested routine" end # # Merge passed-in options with those set in uses_routine, the former taking # priority. # = nested_routine[:options] || {} = Lev::Utilities.deep_merge(, ) # # Setup the input/output mappers # [:translations] ||= {} input_mapper = new_term_mapper([:translations][:inputs]) || new_term_mapper({ scope: symbol }) output_mapper = new_term_mapper([:translations][:outputs]) # # Set up the ignored errors in the routine instance # ([:ignored_errors] || []).each do |ignored_error| other_routine.errors.ignore(ignored_error) end # # Attach the subroutine to self, call it, transfer outputs and errors # other_routine.runner = self run_result = other_routine.call(*args, **kwargs, &block) run_result.outputs.transfer_to(outputs) do |name| output_mapper.map(name) end unless output_mapper.nil? [:errors_are_fatal] = true if !.has_key?(:errors_are_fatal) transfer_errors_from(run_result.errors, input_mapper, [:errors_are_fatal]) run_result end |
#transaction_run_by?(who) ⇒ Boolean
Returns true iff the given instance is responsible for running itself in a transaction
323 324 325 |
# File 'lib/lev/routine.rb', line 323 def transaction_run_by?(who) who == topmost_runner && who.class.transaction_isolation != TransactionIsolation.no_transaction end |
#transfer_errors_from(source, input_mapper, fail_if_errors = false) ⇒ Object
Utility method to transfer errors from a source to this routine. The provided input_mapper maps the language of the errors in the source to the language of this routine. If fail_if_errors is true, this routine will throw an error condition that causes execution of this routine to stop after having transfered all of the errors.
433 434 435 436 437 438 439 |
# File 'lib/lev/routine.rb', line 433 def transfer_errors_from(source, input_mapper, fail_if_errors=false) if input_mapper.is_a? Hash input_mapper = new_term_mapper(input_mapper) end ErrorTransferer.transfer(source, self, input_mapper, fail_if_errors) end |