Autoloading classes in Ruby without its `autoload` - ruby

I love the autoload functionality of Ruby; however, it's going away in future versions of Ruby since it was never thread-safe.
So right now I would like to pretend it's already gone and write my code without it, by implementing the lazy-loading mechanism myself. I'd like to implement it in the simplest way possible (I don't care about thread-safety right now). Ruby should allow us to do this.
Let's start by augmenting a class' const_missing:
class Dummy
def self.const_missing(const)
puts "const_missing(#{const.inspect})"
super(const)
end
end
Ruby will call this special method when we try to reference a constant under "Dummy" that's missing, for instance if we try to reference "Dummy::Hello", it will call const_missing with the Symbol :Hello. This is exactly what we need, so let's take it further:
class Dummy
def self.const_missing(const)
if :OAuth == const
require 'dummy/oauth'
const_get(const) # warning: possible endless loop!
else
super(const)
end
end
end
Now if we reference "Dummy::OAuth", it will require the "dummy/oauth.rb" file which is expected to define the "Dummy::OAuth" constant. There's a possibility of an endless loop when we call const_get (since it can call const_missing internally), but guarding against that is outside the scope of this question.
The big problem is, this whole solution breaks down if there exists a module named "OAuth" in the top-level namespace. Referencing "Dummy::OAuth" will skip its const_missing and just return the "OAuth" from the top-level. Most Ruby implementations will also make a warning about this:
warning: toplevel constant OAuth referenced by Dummy::OAuth
This was reported as a problem way back in 2003 but I couldn't find evidence that the Ruby core team was ever concerned about this. Today, most popular Ruby implementations carry the same behavior.
The problem is that const_missing is silently skipped in favor of a constant in the top-level namespace. This wouldn't happen if "Dummy::OAuth" was declared with Ruby's autoload functionality. Any ideas how to work around this?

This was raised in a Rails ticket some time ago and when I investigated it there appeared to be no way round it. The problem is that Ruby will search the ancestors before calling const_missing and since all classes have Object as an ancestor then any top-level constants will always be found. If you can restrict yourself to only using modules for namespacing then it will work since they do not have Object as an ancestor, e.g:
>> class A; end
>> class B; end
>> B::A
(irb):3: warning: toplevel constant A referenced by B::A
>> B.ancestors
=> [B, Object, Kernel, BasicObject]
>> module C; end
>> module D; end
>> D::C
NameError: uninitialized constant D::C
>> D.ancestors
=> [D]

I get your problem on ree 1.8.7 (you don't mention a specific version) if I use const_get inside const_missing, but not if I use ::. I don't love using eval, but it does work here:
class Dummy
def self.const_missing(const)
if :OAuth == const
require 'dummy/oauth'
eval "self::#{const}"
else
super(const)
end
end
end
module Hello
end
Dummy.const_get :Hello # => ::Hello
Dummy::Hello # => Dummy::Hello
I wish Module had a :: method so you could do self.send :"::", const.

Lazy loading is a very common design pattern, you can implementing it in many ways. like :
class Object
def bind(key, &block)
#hooks ||= Hash.new{|h,k|h[k]=[]}
#hooks[key.to_sym] << [self,block]
end
def trigger(key)
#hooks[key.to_sym].each { |context,block| block.call(context) }
end
end
Then you can
bind :json do
require 'json'
end
begin
JSON.parse("[1,2]")
rescue
trigger :json
retry
end

Related

Why would `reload!` in the Rails console cause "superclass mismatch for class" (Rails 4)

I am getting the "superclass mismatch for class" error when I run reload! in the Rails console. I have some super simple classes defined in ruby, something like this:
# base_class.rb
module A
module B
module C
class BaseClass
def close
#stub
end
end
end
end
end
And:
# more_specific.rb
module A
module B
module C
class MoreSpecific < BaseClass
def initialize
# ...
end
def close
end
end
end
end
end
I can see that in fact there's a problem because if I do this before I do reload!:
A::B::C::MoreSpecific.superclass.equal? A::B::C::BaseClass
I get true, and then if I do it after I get the error, I get a false. Additionally, the object_id of the BaseClass does in fact change.
Why might this happen? I've checked for additional references to the MoreSpecific class in the codebase because I thought that might lead to the BaseClass being established as a constant more than once, but did not see anything.
What could be causing the object_id of A:B:C::BaseClass to switch after the reload!?
Autoloading Modules Without a Require Statement
reload! is a Rails console method, not a standard Ruby method. While there could be other causes for the behavior you're seeing, it's worth noting that your C module in more_specific.rb doesn't require base_class at runtime, and may be losing its lookup; Rails may not autoload modules the way you're expecting without it.
Make sure that modules that depend on BaseClass contain a require base_class statement to be executed when the module reloads. If that doesn't resolve it, there may be other problems with your code as well that aren't shown in your current post.

Only allow module to define method if including class/module does not

I'm having lots of fun with ActiveModel's serialization, specifically the tangled web of as_json and serializable_hash.
My app has a large collection of models that share behavior by including a module, we'll call it SharedBehavior.
My team has decided we have a default format we want all these classes to follow when being cast to JSON (for rendering in a Rails app), but some of them should behave a little differently. Due to odd behavior in these two methods from the ActiveModel library, adding whitelisted or blacklisted attributes in the models themselves gets overridden by the method definition in this module, and then passed on to the super declarations in ActiveModel.
For this reason, I'd like this module to only apply its definition of these methods to models if they are not explicitly overridden in those models (in essence, take the module out of the ancestor chain for a few method calls), but I still need the shared behavior from this module.
I tried solving this by conditionally, dynamically applying the method on module inclusion in IRB:
class A
def foo
puts 'in A'
end
end
module D
def self.included(base)
unless base.instance_methods(false).include?(:foo)
define_method(:foo) do
puts 'in D'
super()
end
end
end
end
class B < A
include D
end
class C < A
include D
def foo
puts 'in C'
super
end
end
With this declaration, I expected the output of C.new.foo to be
in C
in A
but it was instead
in C
in D
in A
My only other thought is to move this logic out into another module and include that module in every class (there are about 54 of them) that does not explicitly override this method, but there are a couple downsides to that:
It introduces a bit of implicit coupling in the project that a new model include this module iff it does not want to override this method implementation
The current implementation of these serialization methods in the module have to do with behavior and attributes established by that module, so I feel like it would be unintuitive to have a second module that knows about and depends on those implementation details of SharedBehavior, though the second module would have almost nothing to do with the first.
Can anyone else think of another solution, or maybe spot an oversight of mine in the code example above that would allow me to make a call in the included hook? (I also tried switching the order in which the C class defined the foo method and included the D module, but saw exactly the same behavior).
There are two tricky bugs here.
Ruby evaluates classes, so the order of expressions matters. You include D before defining foo in C, so when the included hook is called, foo won't be defined in base. You need to include D at the end of the class.
You're defining foo in D. So after including D in B, D#foo is defined, meaning it's still included in C even if you fix the previous bug. You need base to be the receiver of define_method.
But there's an interesting twist: fixing the second bug makes the first bug irrelevant. By defining foo in base directly, it will be overwritten by any later definitions. It would be like doing
class C < A
def foo
puts 'in D'
super()
end
# overwrites previous definition!
def foo
puts 'in C'
super
end
end
So to summarize, you just need
# in D.included
base.class_eval do
define_method(:foo) do
puts 'in D'
super()
end
end

referring to module level variables from within module

I'm having some difficulty with referring to module-level variables in ruby. Say I have a situation like this, where I'm referring to M.a internally:
module M
##a=1
def self.a
##a
end
class A
def x
M.a
end
end
end
Now, this example works fine for me but it is failing in a slightly more complicated context (where the module is spread over a number of files installed in a local gem - but my understanding is that that should not effect the way the code is executed) with an error like this: undefined method `a' for M::M (NoMethodError).
So, is this the correct way to refer to module level variables in context? is there a simpler/more idiomatic way?
If the module is spread out over other files, you need to ensure that your initialization is run before the method is called. If they are in the same file, this should be as much as guaranteed, but if you somehow split them there could be trouble.
I've found you can usually get away with this:
module M
def self.a
#a ||= 1
end
end
If this variable is subject to change, you will need a mutator method. Rails provides mattr_accessor that basically does what you want, part of ActiveSupport.

How can I "replace" a module included via ruby include function

As a follow up to How can I reverse ruby's include function, which was well answered but turned out my simplification of the real problem mean't that the solution was not applicable.
I'm now faced with this (names changed to protect identities!):
module OldFormHelpers
def foo
puts "foo"
end
def bar
puts "bar"
end
end
module Helpers
include OldFormHelpers
end
This gives me:
Helpers.instance_methods
=> ["bar", "foo"]
Helpers.ancestors
=> [Helpers, OldFormHelpers]
This is code that I don't really have access to modify, without forking.
What I want to do is create a new module;
module BetterFormHelpers
def foo
puts "better foo"
end
end
This needs to remove the behaviours from OldFormHelpers, and then add in the new stuff from BetterFormHelpers
The previous solution was to use undef_method like so:
Helpers.module_eval do
OldFormHelpers.instance_methods do |m|
undef_method(m)
end
end
However, after including BetterFormHelpers, Helpers.instance_methods doesn't contain "foo". The reason for this is explained at http://ruby-doc.org/core/classes/Module.src/M001652.html
Using remove_method tells me that Helpers doesn't have the "foo" method, so I guess I need some way of removing the first inclusion from the ancestors chain...
This was getting a bit long so I stopped putting so many snippets in towards the end, but I add an irb session showing the behaviour of undef/remove and then an include.
Can't you undefine only the methods that will not be overwritten?
Helpers.module_eval do
(OldFormHelpers.instance_methods - BetterFormHelpers.instance_methods).each do |m|
undef_method(m)
end
end
(The latter included module will be searched first so that no OldFormHelpers method will be executed if BetterFormHelpers also defines it.)
If you want to dynamically overwrite further methods of the OldFormHelpers module, however, the problem remains the same.

Ruby exception inheritance with dynamically generated classes

I'm new to Ruby, so I'm having some trouble understanding this weird exception problem I'm having. I'm using the ruby-aaws gem to access Amazon ECS: http://www.caliban.org/ruby/ruby-aws/. This defines a class Amazon::AWS:Error:
module Amazon
module AWS
# All dynamically generated exceptions occur within this namespace.
#
module Error
# An exception generator class.
#
class AWSError
attr_reader :exception
def initialize(xml)
err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
err_msg = xml.elements['Message'].text
unless Amazon::AWS::Error.const_defined?( err_class )
Amazon::AWS::Error.const_set( err_class,
Class.new( StandardError ) )
end
ex_class = Amazon::AWS::Error.const_get( err_class )
#exception = ex_class.new( err_msg )
end
end
end
end
end
This means that if you get an errorcode like AWS.InvalidParameterValue, this will produce (in its exception variable) a new class Amazon::AWS::Error::InvalidParameterValue which is a subclass of StandardError.
Now here's where it gets weird. I have some code that looks like this:
begin
do_aws_stuff
rescue Amazon::AWS::Error => error
puts "Got an AWS error"
end
Now, if do_aws_stuff throws a NameError, my rescue block gets triggered. It seems that Amazon::AWS::Error isn't the superclass of the generated error - I guess since it's a module everything is a subclass of it? Certainly if I do:
irb(main):007:0> NameError.new.kind_of?(Amazon::AWS::Error)
=> true
It says true, which I find confusing, especially given this:
irb(main):009:0> NameError.new.kind_of?(Amazon::AWS)
=> false
What's going on, and how am I supposed to separate out AWS errors from other type of errors? Should I do something like:
begin
do_aws_stuff
rescue => error
if error.class.to_s =~ /^Amazon::AWS::Error/
puts "Got an AWS error"
else
raise error
end
end
That seems exceptionally janky. The errors thrown aren't class AWSError either - they're raised like this:
error = Amazon::AWS::Error::AWSError.new( xml )
raise error.exception
So the exceptions I'm looking to rescue from are the generated exception types that only inherit from StandardError.
To clarify, I have two questions:
Why is NameError, a Ruby built in exception, a kind_of?(Amazon::AWS::Error), which is a module?
Answer: I had said include Amazon::AWS::Error at the top of my file, thinking it was kind of like a Java import or C++ include. What this actually did was add everything defined in Amazon::AWS::Error (present and future) to the implicit Kernel class, which is an ancestor of every class. This means anything would pass kind_of?(Amazon::AWS::Error).
How can I best distinguish the dynamically-created exceptions in Amazon::AWS::Error from random other exceptions from elsewhere?
Ok, I'll try to help here :
First a module is not a class, it allows you to mix behaviour in a class. second see the following example :
module A
module B
module Error
def foobar
puts "foo"
end
end
end
end
class StandardError
include A::B::Error
end
StandardError.new.kind_of?(A::B::Error)
StandardError.new.kind_of?(A::B)
StandardError.included_modules #=> [A::B::Error,Kernel]
kind_of? tells you that yes, Error does possess All of A::B::Error behaviour (which is normal since it includes A::B::Error) however it does not include all the behaviour from A::B and therefore is not of the A::B kind. (duck typing)
Now there is a very good chance that ruby-aws reopens one of the superclass of NameError and includes Amazon::AWS:Error in there. (monkey patching)
You can find out programatically where the module is included in the hierarchy with the following :
class Class
def has_module?(module_ref)
if self.included_modules.include?(module_ref) and not self.superclass.included_modules.include?(module_ref)
puts self.name+" has module "+ module_ref.name
else
self.superclass.nil? ? false : self.superclass.has_module?(module_ref)
end
end
end
StandardError.has_module?(A::B::Error)
NameError.has_module?(A::B::Error)
Regarding your second question I can't see anything better than
begin
#do AWS error prone stuff
rescue Exception => e
if Amazon::AWS::Error.constants.include?(e.class.name)
#awsError
else
whatever
end
end
(edit -- above code doesn't work as is : name includes module prefix which is not the case of the constants arrays. You should definitely contact the lib maintainer the AWSError class looks more like a factory class to me :/ )
I don't have ruby-aws here and the caliban site is blocked by the company's firewall so I can't test much further.
Regarding the include : that might be the thing doing the monkey patching on the StandardError hierarchy. I am not sure anymore but most likely doing it at the root of a file outside every context is including the module on Object or on the Object metaclass. (this is what would happen in IRB, where the default context is Object, not sure about in a file)
from the pickaxe on modules :
A couple of points about the include statement before we go on. First, it has nothing to do with files. C programmers use a preprocessor directive called #include to insert the contents of one file into another during compilation. The Ruby include statement simply makes a reference to a named module. If that module is in a separate file, you must use require to drag that file in before using include.
(edit -- I can't seem to be able to comment using this browser :/ yay for locked in platforms)
Well, from what I can tell:
Class.new( StandardError )
Is creating a new class with StandardError as the base class, so it is not going to be a Amazon::AWS::Error at all. It is just defined in that module, which is probably why it is a kind_of? Amazon::AWS::Error. It probably isn't a kind_of? Amazon::AWS because maybe modules don't nest for purposes of kind_of? ?
Sorry, I don't know modules very well in Ruby, but most definitely the base class is going to be StandardError.
UPDATE: By the way, from the ruby docs:
obj.kind_of?(class) => true or false
Returns true if class is the class of obj, or if class is one of the superclasses of obj or modules included in obj.
Just wanted to chime in: I would agree this is a bug in the lib code. It should probably read:
unless Amazon::AWS::Error.const_defined?( err_class )
kls = Class.new( StandardError )
Amazon::AWS::Error.const_set(err_class, kls)
kls.include Amazon::AWS::Error
end
One issue you're running into is that Amazon::AWS::Error::AWSError is not actually an exception. When raise is called, it looks to see if the first parameter responds to the exception method and will use the result of that instead. Anything that is a subclass of Exception will return itself when exception is called so you can do things like raise Exception.new("Something is wrong").
In this case, AWSError has exception set up as an attribute reader which it defines the value to on initialization to something like Amazon::AWS::Error::SOME_ERROR. This means that when you call raise Amazon::AWS::Error::AWSError.new(SOME_XML) Ruby ends up calling Amazon::AWS::Error::AWSError.new(SOME_XML).exception which will returns an instance of Amazon::AWS::Error::SOME_ERROR. As was pointed out by one of the other responders, this class is a direct subclass of StandardError instead of being a subclass of a common Amazon error. Until this is rectified, Jean's solution is probably your best bet.
I hope that helped explain more of what's actually going on behind the scenes.

Resources