⬆️ ⬇️

What are the bad nested classes and how to learn rails magic

I recently encountered a funny problem while writing an API with grape. Grape really has nothing to do with it, the article is more about how ActiveSupport works, creating all the magic thanks to which we don’t need constant require in rails, and how we can explode it. Who cares, I ask under the cat.



And so we have classes - Grape :: Entity, which we use to display the model in the API, they can be inherited, connect different modules, and themselves are included in different modules during versioning. The directory structure looks like this:



./api/ └── path1 β”œβ”€β”€ entities β”‚  β”œβ”€β”€ entity1.rb β”‚  └── entity2.rb β”œβ”€β”€ v1 β”‚  └── entities β”‚  β”œβ”€β”€ entity1.rb β”‚  └── entity3.rb └── v2 └── entities β”œβ”€β”€ entity1.rb β”œβ”€β”€ entity2.rb └── entity3.rb 


We can simply find a specific entry with ::Api::Path1::V2::Entity::Entity1 . And all is well, as long as there are only modules in this path, and a final class. But we don't always work alone, and sometimes nested classes occur. Module1::Module2::Class1::Class2 , this is ruby, it’s possible here, and there’s nothing wrong with that either. But then we make a new version of our API, and in order not to write everything from scratch we inherit the old class ::Api::Path1::V3::Entity::Class1::Class2 , where V3::Class1 < V1::Class1 . And here everything suddenly breaks. We are trying to get ::Api::Path1:: V3 ::Entity::Class1::Class2 , and we have ::Api::Path1:: V1 ::Entity::Class1::Class2 . A typical example of Rails magic, we did not receive errors, but did not receive the required class, but received a completely different one, and this despite the fact that the full path was specified with all the namespaces!



Fortunately, we have pry - it is not only a very powerful alternative to irb, but also a great debager that allows us to go through the executable code, step by step, immersing ourselves in all the methods of all auxiliary classes, about which we don’t know anything, see them implementation, communications, etc. Read more here .

')

So:



 ... binding.pry '::Api::Path1::V3::Entity::Class1::Class2'.constantize ... > step 65: def constantize => 66: ActiveSupport::Inflector.constantize(self) 67: end > step ... @ line 251 ActiveSupport::Inflector#constantize: 248: # NameError is raised when the name is not in CamelCase or the constant is 249: # unknown. 250: def constantize(camel_cased_word) => 251: names = camel_cased_word.split('::') 252: 253: # Trigger a built-in NameError exception including the ill-formed constant in the message. 254: Object.const_get(camel_cased_word) if names.empty? 


And after just a couple of steps, we dive into the depths of ActiveSupport, which parses the name of the class and looks for its implementation, it looks like this:



 250 def constantize(camel_cased_word) 251 names = camel_cased_word.split('::') 252 253 # Trigger a built-in NameError exception including the ill-formed constant in the message. 254 Object.const_get(camel_cased_word) if names.empty? 255 256 # Remove the first blank element in case of '::ClassName' notation. 257 names.shift if names.size > 1 && names.first.empty? 258 259 names.inject(Object) do |constant, name| 260 if constant == Object 261 constant.const_get(name) 262 else 263 candidate = constant.const_get(name) 264 next candidate if constant.const_defined?(name, false) 265 next candidate unless Object.const_defined?(name) 266 267 # Go down the ancestors to check if it is owned directly. The check 268 # stops when we reach Object or the end of ancestors tree. 269 constant = constant.ancestors.inject do |const, ancestor| 270 break const if ancestor == Object 271 break ancestor if ancestor.const_defined?(name, false) 272 const 273 end 274 275 # owner is in Object, so raise 276 constant.const_get(name, false) 277 end 278 end 279 end 


What's going on here:



ActiveSupport splits our line of the form ::Api::Path1::V2::Entity::Entity1 into separate words, and then sequentially gathers it back, calls const_get for each following name, starting from the parent Object, and checking that it is defined.



And it is here that the problem arises when ActiveSupport on line 263 does ::Api::Path1:: V3 ::Entity::Class1.const_get('Class2') , without the second parameter false it turns out that Class2 is defined in the inherited class ::Api::Path1:: V1 ::Entity::Class1 , and that is what we get and return from the method.



There would be no such problem when using candidate = constant.const_get(name, false) , but this is more of a feature than a bug. ActiveSupport is trying to find a constant, including one defined by the ancestors, otherwise the magic will be much less.



And such a problem will not arise if you do not inherit classes that have nested classes, and thus do not invent problems for yourself)



Ps. printercu advised a great article in the comments.

Source: https://habr.com/ru/post/335192/



All Articles