И так у нас есть классы — Grape::Entity, которые мы используем для отображения модели в API, они могут наследоваться, подключать разные модули, и сами входят в различные модули при версионировании. Структура каталога выглядит примерно так:
./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. И все хорошо, пока в этом пути присутствуют только модули, и конечный класс. Но мы не всегда работаем в одиночку, и иногда возникают вложенные классы. Module1::Module2::Class1::Class2, это руби, здесь так можно, и в этом тоже нет ничего страшного. Но потом мы делаем новую версию нашего API, и что бы не писать все с нуля наследуем старый класс ::Api::Path1::V3::Entity::Class1::Class2, где V3::Class1 < V1::Class1. И вот тут все внезапно ломается. Мы пытаемся получить ::Api::Path1::V3::Entity::Class1::Class2, а имеем ::Api::Path1::V1::Entity::Class1::Class2. Типичный пример Rails магии, мы не получили ошибки, но не получили и нужного класса, а получили совершенно другой, и это при том, что был прописан полный путь со всеми namespaсe!К счастью у нас есть pry — это не только очень мощная альтернатива irb, но так же замечательный дебагер, который позволяет нам пройти по выполняемому коду, шаг за шагом, погрузившись во все методы всех вспомогательных классов, о которых мы ничего не знаем, посмотреть их реализацию, связи, и.т.д. Подробнее тут.
Итак:
... 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?
И всего через пару шагов мы погружаемся в недра ActiveSupport, который парсит имя класса и ищет его реализацию, выглядит это вот так:
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
Что здесь происходит:
ActiveSupport разбивает нашу строку вида
::Api::Path1::V2::Entity::Entity1 на отдельные слова, и потом последовательно собирает обратно, вызывает const_get на каждое следующее имя, начиная от родительского Object, и проверяя, что оно определено.И именно тут возникает проблема, когда ActiveSupport на строке 263 делает
::Api::Path1::V3::Entity::Class1.const_get('Class2'), без второго параметра false оказывается что Class2 определен в наследуемом классе ::Api::Path1::V1::Entity::Class1, и именно его мы получаем, и возвращаем из метода.Такой проблемы не было бы, при использовании
candidate = constant.const_get(name, false), но это скорее фича, чем баг. ActiveSupport пытается найти константу в том числе и определенную у предков, иначе магии станет гораздо меньше.А еще такой проблемы не возникнет, если не наследовать классы, у которых есть вложенные классы, и не придумывать тем самым проблем самим себе )
Ps. printercu посоветовал отличную статью в комментариях.

