Wierdness with dependency resolution with models in modules

I’m sure this is some basic misunderstanding I have to how dependency resolution should work in Rails, but am experiencing some wierdness with modules and models.

I hadn’t been using modules for models much because I had run into early issues with them, but woke up early this morning with the idea of a way to use them for something that we need.

This was in a brand new Rails 4.0 beta project (using github rails/rails master), but have the same behavior in Rails 3.2.6.

Here are the models. I just wanted to try to have the class load so I could see which class was being loaded, but just referencing these model classes causes issues:

app/models/associated_model.rb

class AssociatedModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

app/models/my_model.rb

class MyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

app/models/root_only_model.rb

class RootOnlyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

app/models/some_module/associated_model.rb

class SomeModule::AssociatedModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}” #bug
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

app/models/some_module/module_specific_model.rb

module SomeModule
class ModuleSpecificModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
end

app/models/some_module/my_model.rb

class SomeModule::MyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

And here is what happens in rails console:

$ rails c
Loading development environment (Rails 4.0.0.beta)
1.9.3p194 :001 > AssociatedModel
in AssociatedModel, AssociatedModel is AssociatedModel
NameError: uninitialized constant AssociatedModel::ModuleSpecificModel
from /path/to/new_rails_app/app/models/associated_model.rb:3:in <class:AssociatedModel>' from /path/to/new_rails_app/app/models/associated_model.rb:1:in <top (required)>’
from (irb):1
from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start' from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start’
from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>' from script/rails:6:in require’
from script/rails:6:in <main>' 1.9.3p194 :002 > MyModel in AssociatedModel, AssociatedModel is AssociatedModel NameError: uninitialized constant AssociatedModel::ModuleSpecificModel from /path/to/new_rails_app/app/models/associated_model.rb:3:in class:AssociatedModel
from /path/to/new_rails_app/app/models/associated_model.rb:1:in <top (required)>' from /path/to/new_rails_app/app/models/my_model.rb:2:in class:MyModel
from /path/to/new_rails_app/app/models/my_model.rb:1:in <top (required)>' from (irb):2 from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start’
from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start' from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>’
from script/rails:6:in require' from script/rails:6:in
1.9.3p194 :003 > RootOnlyModel
in AssociatedModel, AssociatedModel is AssociatedModel
NameError: uninitialized constant AssociatedModel::ModuleSpecificModel
from /path/to/new_rails_app/app/models/associated_model.rb:3:in <class:AssociatedModel>' from /path/to/new_rails_app/app/models/associated_model.rb:1:in <top (required)>’
from /path/to/new_rails_app/app/models/root_only_model.rb:2:in <class:RootOnlyModel>' from /path/to/new_rails_app/app/models/root_only_model.rb:1:in <top (required)>’
from (irb):3
from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start' from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start’
from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>' from script/rails:6:in require’
from script/rails:6:in <main>' 1.9.3p194 :004 > SomeModule::AssociatedModel NameError: uninitialized constant SomeModule::AssociatedModel::AssociatedModel from /path/to/new_rails_app/app/models/some_module/associated_model.rb:2:in class:AssociatedModel
from /path/to/new_rails_app/app/models/some_module/associated_model.rb:1:in <top (required)>' from (irb):4 from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start’
from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start' from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>’
from script/rails:6:in require' from script/rails:6:in
1.9.3p194 :005 > SomeModule::ModuleSpecificModel
NameError: uninitialized constant SomeModule::AssociatedModel::AssociatedModel
from /path/to/new_rails_app/app/models/some_module/associated_model.rb:2:in <class:AssociatedModel>' from /path/to/new_rails_app/app/models/some_module/associated_model.rb:1:in <top (required)>’
from /path/to/new_rails_app/app/models/some_module/module_specific_model.rb:3:in <class:ModuleSpecificModel>' from /path/to/new_rails_app/app/models/some_module/module_specific_model.rb:2:in module:SomeModule
from /path/to/new_rails_app/app/models/some_module/module_specific_model.rb:1:in <top (required)>' from (irb):5 from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start’
from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start' from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>’
from script/rails:6:in require' from script/rails:6:in
1.9.3p194 :006 > SomeModule::MyModel
NameError: uninitialized constant SomeModule::AssociatedModel::AssociatedModel
from /path/to/new_rails_app/app/models/some_module/associated_model.rb:2:in <class:AssociatedModel>' from /path/to/new_rails_app/app/models/some_module/associated_model.rb:1:in <top (required)>’
from /path/to/new_rails_app/app/models/some_module/my_model.rb:2:in <class:MyModel>' from /path/to/new_rails_app/app/models/some_module/my_model.rb:1:in <top (required)>’
from (irb):6
from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start' from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start’
from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>' from script/rails:6:in require’
from script/rails:6:in `’

Thanks in advance,
Gary

Note: I understand that referring to models in a module from a model in the no module (in app/models) would not work without declaring those as SomeModule::SomeModelClassName, but I was confused about the other things I thought would have resolved because I thought that it was supposed to search parent modules for the constant if not found.

This doesn’t look like a rails issue. It looks like ruby expected behaviour, unless I misunderstand.

The constant ModuleSpecificModel doesn’t exist. Only SomeModule::ModuleSpecificModel exists. Ruby auto loading or rails autoloading would not expect this code to ‘install’ the ModuleSpecificModule at the top level ( I.e ::ModuleSpecificModel )

I should have fixed the models before posting, but these are the models that I would have thought would work with the module identifiers set properly.

It looks like when a model class in a module refers to by class name only, which I think is supposed to work, it tries to use its fully-qualified class name as its module instead of its module. Is that a
bug or something I’m doing wrong?

So, basically this is the wierdness:

Loading development environment (Rails 4.0.0.beta)
1.9.3p194 :001 > AssociatedModel
in AssociatedModel, AssociatedModel is AssociatedModel
in SomeModule::ModuleSpecificModel, AssociatedModel is AssociatedModel
NameError: uninitialized constant SomeModule::ModuleSpecificModel::ModuleSpecificModel
from /path/to/spike/app/models/some_module/module_specific_model.rb:3:in <class:ModuleSpecificModel>' from /path/to/spike/app/models/some_module/module_specific_model.rb:1:in <top (required)>’
from /path/to/spike/app/models/associated_model.rb:3:in <class:AssociatedModel>' from /path/to/spike/app/models/associated_model.rb:1:in <top (required)>’
from (irb):1
from /path/to/github/rails/railties/lib/rails/commands/console.rb:78:in start' from /path/to/github/rails/railties/lib/rails/commands/console.rb:9:in start’
from /path/to/github/rails/railties/lib/rails/commands.rb:47:in <top (required)>' from script/rails:6:in require’
from script/rails:6:in `’

$ find app/models -exec more {} ;
app/models is a directory
class AssociatedModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{SomeModule::ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
class MyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{SomeModule::ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
class RootOnlyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{SomeModule::ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
app/models/some_module is a directory
class SomeModule::AssociatedModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
class SomeModule::ModuleSpecificModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end
class SomeModule::MyModel < ActiveRecord::Base
puts “in #{self.name}, AssociatedModel is #{AssociatedModel.name}”
puts “in #{self.name}, ModuleSpecificModel is #{ModuleSpecificModel.name}”
puts “in #{self.name}, MyModel is #{MyModel.name}”
puts “in #{self.name}, RootOnlyModel is #{RootOnlyModel.name}”
end

Thanks!

When perusing activesupport/lib/activesupport/dependencies.rb, I had assumed that if you were in the same module as the class (or in the last example, within the class itself) that it would attempt to find that class in the current module even if you didn’t specify a module, but it doesn’t seem like it should be looking for: SomeModule::ModuleSpecificModel::ModuleSpecificModel when referring to ModuleSpecificModel within the ModuleSpecificModel class itself.

But I think my confusion was assuming that there is a different from a constant that is a class and a constant that is some other type. That is the Java side of my brain interfering.

Sorry to bother about this. I was hoping that dependency resolution could be used to refer to a model class from another model class and it automatically find it in the root module if it didn’t exist in the current module, but I had forgotten about other types of constants and how that would throw a wrench in things if it worked that way.

The problem you are seeing can be summarized in this snippet:

module M

end

class M::A

A

end

If you run that code with plain Ruby you’ll get

uninitialized constant M::a::A (NameError)

Let’s see why that’s that way in Ruby first, then we will go with dependencies.rb.

Ruby looks for constants in the nesting, then ancestors, then in Object if the container module is a module, and fallbacks to const_missing if present. That’s the resolution algorithm quickly described.

OK, the nesting at the point where we evaluate A is [M::A]. The class and module keywords push to the nesting the module object they are creating or reopening. That is one module object regardless of the constant or constant path that yielded it. If the constant path is composed of 15 constants, in the end it evaluates to one single class or module object, that’s the one pushed to the nesting.

In particular, M does not belong to the nesting.

So Ruby goes like this: A does not belong to M::A (nesting scan done), A does not belong to any of [Object, Kernel, BasicObject] (ancestors scan done). Since the containing M::A is a class we do not do the extra Object check. And finally const_missing is not present. Therefore: NameError.

The key point here is that A belongs to M, and M is not in the nesting.

Now, Active Support is not able to discern whether M belongs or does not belong to the nesting because Ruby does not tell you in const_missing. AS assumes the nesting matches the name of the class or module const_missing was triggered in. In the example about that would be, [M::A, M].

But, when const_missing is triggered dependencies.rb manually checks if the constant is present in one of those “parent” modules. So, what happens in Rails is that const_missing is triggered, and dependencies.rb is able to see that A is defined in M, therefore a posteriori M must not belong to the nesting because otherwise const_missing had not been triggered in the first place, and is able to raise a NameError, which is the correct answer.

Constant autoloading in Rails does not emulate the constant name resolution algorithm. But in this case it behaves as Ruby.

Thanks! All of that was a great summary and I appreciate your time for making it so clear.

How I went down that bad road:

Imagine a world where Rails constant resolution took the current module of the class that is referring to the class into account when def load_missing_constant(from_mod, const_name) was doing its thing, such that if you refer to a class from a controller that was in a module or a model that was in a module, it would get the one in the same module if it existed, if not then its parent module, etc. up to root. To refer to a class in the root module, a module call Root could exist to do Root::SomeClass.

In this roller coaster ride through fantasyland… the following could happen (even though it won’t, this is 200 proof unicorns and rainbows):

  • in the controller when it refers to a model class, it would look in its module then its parent, etc. on to the root (no) module
  • the would allow you to define a module with model behavior and then include it in both a root level model and maybe some other module, such that you could cherry pick models to override and put in a module directory that could have slight changes in the module version of the model.
  • by changing the standard serialization to just look at mass assignment attributes and associations to define what is serializable/deserializable/persistable with a few additions, you could have different views and not even have to define which models you are using in the controller, because the module would define those
  • this could get rid of the need for strong_parameters, activemodel:serializers, etc. to some extent similar to Roar, but using existing models in a very DRY way

Anyway. Instead of this, I’m trying out roar-rails, and am in-process of creating default representers so that representers are only needed when the user doesn’t want to use what they’ve defined via mass assignment security, etc.

The only problem so far is that I had functionality to allow the user to specify some fields to be serialized into json (or limit them) via the request itself. That worked before when I was using as_json directly, but now to do that I would have to create new representer classes each time, which is not good because they would need to be new classes, and I was going to dup a default representer, change the name and use that, but that would build up classes that wouldn’t go away. Will figure it out though…

This is under development/may change/go away later and untested, but: https://github.com/garysweaver/restful_json/blob/3.0.0/lib/restful_json/roar/entity_representer.rb

Right now there is no difference between default entity and collection representer. The reason I’m using roar/roar-rails vs. something else is to try to both persist JSON and (de)serialize json and support different views of the same model in a DRY way.

One last thing on the topic of constant resolution: if anyone sees a big problem with what I do in this gem, let me know:
https://github.com/garysweaver/classmeta

I don’t think I’m going to go down the road of using it, but maybe if I stripped out the transformer stuff that I’m not sure anyone would use, they could just use it to dup and rename classes in a way that works with Rails caching and autoloading.

BTW- this is not the suggested way to do things in roar/roar-rails; mirroring the associations and attributes in the representer wouldn’t work in roar 0.11.3 because there is no guarding against circular references since it doesn’t intend to be used like this, but I’m talking with Nick about it to see if there is a workaround. Ok, will stop going off on a tangent now…