动态扩展 Virtus 实例属性
Dynamically extend Virtus instance attributes
假设我们有一个 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
或通过 as_json
(或 to_json
)或 Hash
通过 [= 将对象转换为 JSON 时20=] 我只得到 post-扩展属性 active
:
user.to_h # => { active: true }
是什么导致了这个问题,我怎样才能在不丢失数据的情况下转换对象?
P.S.
我找到了一个github issue,不过好像还是没修好(那里推荐的方法也不太稳定)。
我还没有进一步调查,但似乎每次包含或扩展 Virtus.model 时,它都会初始化一个新的 AttributeSet 并将其设置为 @attribute_set 您的 User class(source). What the to_h
or attributes
do is they call the get
method of the new attribute_set instance (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
可以看到,扩展前后attribute_set
实例的object_id
是不一样的,也就是说前者和后者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
根据 Adrian 的发现,这是一种修改 Virtus 以允许您想要的方法。所有规格都通过此修改。
本质上,Virtus 已经有了父 AttributeSet
的概念,但只有当 Virtus.model
包含在 class 中时才会出现。
我们也可以扩展它以考虑实例,甚至允许在同一对象中使用多个 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,你觉得怎么样?
假设我们有一个 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
或通过 as_json
(或 to_json
)或 Hash
通过 [= 将对象转换为 JSON 时20=] 我只得到 post-扩展属性 active
:
user.to_h # => { active: true }
是什么导致了这个问题,我怎样才能在不丢失数据的情况下转换对象?
P.S.
我找到了一个github issue,不过好像还是没修好(那里推荐的方法也不太稳定)。
我还没有进一步调查,但似乎每次包含或扩展 Virtus.model 时,它都会初始化一个新的 AttributeSet 并将其设置为 @attribute_set 您的 User class(source). What the to_h
or attributes
do is they call the get
method of the new attribute_set instance (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
可以看到,扩展前后attribute_set
实例的object_id
是不一样的,也就是说前者和后者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
根据 Adrian 的发现,这是一种修改 Virtus 以允许您想要的方法。所有规格都通过此修改。
本质上,Virtus 已经有了父 AttributeSet
的概念,但只有当 Virtus.model
包含在 class 中时才会出现。
我们也可以扩展它以考虑实例,甚至允许在同一对象中使用多个 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,你觉得怎么样?