Dynamically scoped variables in Ruby
Recently I had a need to use routing definition of one RoR from the other one (don’t ask why it’s irrelevant to this post
)
As you may know Rails routing mappings are stored in global variable ActionController::Routing::Routes. So together with Craig McMillan I’ve hacked really evil piece of code
but it does the job!
So here it goes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # evil Ruby dynamic variables class DynamicValue def initialize(val) @default_value = val end def method_missing(name, *args, &proc) if tv=Thread.current['DynamicValue'] tv.send( name, *args, &proc) else @default_value.send( name, *args, &proc) end end def with_dynamic_value(value) tmp = Thread.current['DynamicValue'] begin Thread.current['DynamicValue'] = value yield ensure Thread.current['DynamicValue'] = tmp end end def to_s "dynamic value" end def inspect to_s end end |
One can use it for example like this:
# let's deal with fresh empty RouteSet object ActionController::Routing::Routes.with_dynamic_value( ActionController::Routing::RouteSet.new ) do # let's load other RoR routes to it load File.join(Sonar::SONAR_ROOT, 'other_ror', 'config', 'routes.rb') # now we can use the other RoR routing definition to generate email tamplate used from the other RoR email = NewItemsMessage::create_new_message(person) end
Only thread invoking with_dynamic_value will experience the variable reassignment. For other threads it will be transparent. Once the thread exit the block, again it will see the value in the variable as it was before the trick.
In order to enable that evil you have to invoke once and only once (placing it at the end of config/environment.rb should be fine) the following line:
ActionController::Routing.const_set("Routes", DynamicValue.new(ActionController::Routing::Routes))
Or any other variable substitution. Doing it once is important because applying more than one proxy will discard the original value of the variable.
Enjoy more LISP’y Ruby