./api/ βββ path1 βββ entities β βββ entity1.rb β βββ entity2.rb βββ v1 β βββ entities β βββ entity1.rb β βββ entity3.rb βββ v2 βββ entities βββ entity1.rb βββ entity2.rb βββ entity3.rb
::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!
... 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?
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
::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.
::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.
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.
Source: https://habr.com/ru/post/335192/