この質問は以前に尋ねられました:アンカーとエイリアスを破壊せずにYAMLファイルを読み書きしますか?
多くのアンカーとエイリアスでその問題を解決する方法を考えていましたか?
ありがとう
ここでの問題は、Yaml のアンカーとエイリアスはシリアル化の詳細であり、解析後のデータの一部ではないため、データを Yaml に書き戻すときに元のアンカー名がわからないことです。ラウンド トリップ時にアンカー名を保持するには、後でシリアル化するときに使用できるように、解析時にアンカー名をどこかに保存する必要があります。Ruby では、任意のオブジェクトにインスタンス変数を関連付けることができるため、これを実現する簡単な方法は、問題のオブジェクトのインスタンス変数にアンカー名を格納することです。
前の質問の例から続けて、ハッシュの場合、再定義されたメソッドを変更revive_hash
して、ハッシュがアンカーの場合、アンカー名を@st
変数に記録して、後でエイリアスを認識できるようにすることができます。それをインスタンスとして追加します。ハッシュ上の変数。
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
@st[o.anchor] = hash
hash.instance_variable_set "@_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
これは、アンカーである yaml マッピングにのみ影響することに注意してください。アンカー名を保持するために他のタイプが必要なpsych/visitors/to_ruby.rb
場合は、すべての場合に名前が追加されていることを確認する必要があります。ほとんどのタイプはオーバーライドによって含めることができますregister
が、他にもいくつかあります。を検索し@st
ます。
ハッシュに必要なアンカー名が関連付けられたので、シリアル化するときに Psych がオブジェクト ID の代わりにそれを使用するようにする必要があります。これは、サブクラス化によって実行できますYAMLTree
。YAMLTree
オブジェクトを処理するとき、最初にそのオブジェクトが既に認識されているかどうかを確認し、存在する場合はエイリアスを発行します。新しいオブジェクトについては、後でエイリアスを作成する必要がある場合に備えて、オブジェクトを見たことを記録します。これobject_id
では がキーとして使用されるため、これら 2 つのメソッドをオーバーライドしてインスタンス変数をチェックし、存在する場合は代わりにそれを使用する必要があります。
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
if @st.key? anchor_name
oid = anchor_name
node = @st[oid]
anchor = oid.to_s
node.anchor = anchor
return @emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '@_yaml_anchor_name' set
super
end
# record object for future, using '@_yaml_anchor_name' rather
# than object_id if it exists
def register target, yaml_obj
anchor_name = target.instance_variable_get('@_yaml_anchor_name') || target.object_id
@st[anchor_name] = yaml_obj
yaml_obj
end
end
これで、次のように使用できます (前の質問とは異なり、この場合、カスタム エミッターを作成する必要はありません)。
builder = MyYAMLTree.new
builder << data
tree = builder.tree
puts tree.yaml # returns a string
# alternativelty write direct to file:
File.open('a_file.yml', 'r+') do |f|
tree.yaml f
end
これは、psych gem の新しいバージョンまでのわずかに変更されたバージョンです。次のエラーが表示される前に:
NoMethodError - undefined method `[]=' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0>
メソッドはのregister
サブクラスに移動したYAMLTree
ため、マットが答えで言っていることすべてに関して、これが機能するようになりました。
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
@st[o.anchor] = hash
hash.instance_variable_set "@_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
class Registrar
# record object for future, using '@_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
anchor_name = target.instance_variable_get('@_yaml_anchor_name') || target.object_id
@obj_to_node[anchor_name] = node
end
end
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
if @st.key? anchor_name
oid = anchor_name
node = @st[oid]
anchor = oid.to_s
node.anchor = anchor
return @emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '@_yaml_anchor_name' set
super
end
end
Psych v2.0.17 で動作するように、@markus が投稿したコードをさらに変更する必要がありました。
これが私が最終的に得たものです。他の誰かがかなりの時間を節約するのに役立つことを願っています. :-)
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
@st[o.anchor] = hash
hash.instance_variable_set "@_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) do |k,v|
key = accept(k)
hash[key] = accept(v)
end
hash
end
end
class Psych::Visitors::YAMLTree::Registrar
# record object for future, using '@_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
@targets << target
@obj_to_node[_anchor_name(target)] = node
end
def key? target
@obj_to_node.key? _anchor_name(target)
rescue NoMethodError
false
end
def node_for target
@obj_to_node[_anchor_name(target)]
end
private
def _anchor_name(target)
target.instance_variable_get('@_yaml_anchor_name') || target.object_id
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
if @st.key? target
node = @st.node_for target
node.anchor = anchor_name
return @emitter.alias anchor_name
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '@_yaml_anchor_name' set
super
end
def visit_String o
if o == '<<'
style = Psych::Nodes::Scalar::PLAIN
tag = 'tag:yaml.org,2002:str'
plain = true
quote = false
return @emitter.scalar o, nil, tag, plain, quote, style
end
# visit_String is a pretty big method, call super to avoid copying it all
# here. super will handle the cases when it's a string other than '<<'
super
end
end