9

假设我们有一个 Virtus 模型User

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

然后我们创建这个模型的一个实例并扩展 fromVirtus.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_jsonto_jsonHashto_hactive

user.to_h # => { active: true }

是什么导致了问题,如何在不丢失数据的情况下转换对象?

附言

我发现了一个github issue,但似乎它毕竟没有修复(那里推荐的方法也不能稳定工作)。

4

2 回答 2

5

在 Adrian 的发现的基础上,这里有一种修改 Virtus 以允许您想要的方法。所有规格都通过此修改。

本质上,Virtus 已经有了 parent 的概念AttributeSet,但只有在包含Virtus.modelclass中时才会这样。我们也可以扩展它来考虑实例,甚至允许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 做个 PR,你怎么看?

于 2017-05-25T12:39:08.147 回答
3

我没有进一步调查它,但似乎每次包含或扩展 Virtus.model 时,它都会初始化一个新的 AttributeSet 并将其设置为您的 User 类(source)的 @attribute_set 实例变量。to_hor做的attributes是他们调用get新的 attribute_set 实例(source)的方法。因此,您只能在最后一次包含或扩展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
于 2017-05-23T16:36:07.437 回答