Динамически расширять атрибуты экземпляра Virtus

Допустим, у нас есть модель Virtus User

class User
  include Virtus.model
  attribute :name, String, default: 'John', lazy: true
end

Затем мы создаем экземпляр этой модели и расширяем Virtus.model добавить еще один атрибут на лету:

user = User.new
user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)

Токовый выход:

user.active? # => true
user.name # => 'John'

Но когда я пытаюсь получить либо attributes или преобразовать объект в JSON через as_json (или же to_json) или же Hash с помощью to_h Я получаю только пост-расширенный атрибут active:

user.to_h # => { active: true }

В чем причина проблемы и как я могу преобразовать объект без потери данных?

PS

Я обнаружил проблему с github, но, похоже, она не была исправлена ​​(рекомендуемый подход не работает стабильно).

2 ответа

Решение

Опираясь на находки Адриана, вот способ изменить Virtus, чтобы позволить то, что вы хотите. Все спецификации проходят с этой модификацией.

По сути, Виртус уже имеет понятие родителя AttributeSet, но это только при включении Virtus.model в классе. Мы можем расширить его, чтобы рассмотреть и экземпляры, и даже разрешить множественное extend(Virtus.model) в том же объекте (хотя это звучит неоптимально):

require 'virtus'
module Virtus
  class AttributeSet
    def self.create(descendant)
      if descendant.respond_to?(:superclass) && descendant.superclass.respond_to?(:attribute_set)
        parent = descendant.superclass.public_send(:attribute_set)
      elsif !descendant.is_a?(Module)
        if descendant.respond_to?(:attribute_set, true) && descendant.send(:attribute_set)
          parent = descendant.send(:attribute_set)
        elsif descendant.class.respond_to?(:attribute_set)
          parent = descendant.class.attribute_set
        end
      end
      descendant.instance_variable_set('@attribute_set', AttributeSet.new(parent))
    end
  end
end

class User
  include Virtus.model
  attribute :name, String, default: 'John', lazy: true
end

user = User.new
user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)

p user.to_h # => {:name=>"John", :active=>true}

user.extend(Virtus.model) # useless, but to show it works too
user.attribute(:foo, Virtus::Attribute::Boolean, default: false, lazy: true)

p user.to_h # => {:name=>"John", :active=>true, :foo=>false}

Может быть, стоит сделать пиар на Virtus, как вы думаете?

Я не исследовал это далее, но кажется, что каждый раз, когда вы включаете или расширяете Virtus.model, он инициализирует новый AttributeSet и устанавливает его в переменную экземпляра @attribute_set вашего класса User ( источник). Что за to_h или же attributes сделать, они называют get метод нового экземпляра attribute_set ( источник). Таким образом, вы можете получить атрибуты только после последнего включения или расширения Virtus.model,

class User
  include Virtus.model
  attribute :name, String, default: 'John', lazy: true
end

user = User.new
user.instance_variables
#=> []
user.send(:attribute_set).object_id
#=> 70268060523540

user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)

user.instance_variables
#=> [:@attribute_set, :@active, :@name]
user.send(:attribute_set).object_id
#=> 70268061308160

Как видите, object_id из attribute_set Экземпляр до и после расширения отличается, что означает, что первый и последний attribute_set являются двумя разными объектами.

Взломать я могу предложить сейчас это:

(user.instance_variables - [:@attribute_set]).each_with_object({}) do |sym, hash|
  hash[sym.to_s[1..-1].to_sym] = user.instance_variable_get(sym)
end
Другие вопросы по тегам