ruby - Lee y escribe archivos YAML sin destruir anclas y alias
parsing psych (3)
Esta pregunta se ha hecho antes: ¿ Leer y escribir archivos YAML sin destruir anclas y alias?
Me preguntaba cómo resolver ese problema con muchos anclajes y alias.
Gracias
aquí hay una versión ligeramente modificada para las versiones más nuevas de la gema psicológica. antes me dio el siguiente error:
NoMethodError - undefined method `[]='' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0>
el método de register
se movió a una subclase de YAMLTree
, así que esto funciona ahora con respecto a todo lo que Matt dice en su respuesta:
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
Tuve que modificar aún más el código que @markus publicó para trabajar con Psych v2.0.17.
Esto es con lo que terminé. Espero que ayude a otra persona a ahorrar bastante tiempo. :-)
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
El problema aquí es que los anclajes y alias en Yaml son un detalle de serialización, por lo que no forman parte de los datos después de su análisis, por lo que el nombre del ancla original no se conoce al volver a escribir los datos en Yaml. Para conservar los nombres de los anclajes cuando se realiza un disparo circular, debe almacenarlos en algún lugar al analizarlos para que estén disponibles más adelante al serializar. En Ruby, cualquier objeto puede tener variables de instancia asociadas con él, por lo que una forma fácil de lograr esto sería almacenar el nombre del anclaje en una variable de instancia del objeto en cuestión.
Siguiendo con el ejemplo de la pregunta anterior , para los hashes podemos cambiar nuestro método redibujado revive_hash
modo que si el hash es un ancla, además de registrar el nombre del anclaje en la variable @st
para que posteriormente se reconozcan las alises, lo agreguemos como una variable de instancia en el hash.
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
Tenga en cuenta que esto solo afecta a las asignaciones yaml que son anclajes. Si desea tener otros tipos para conservar su nombre de anclaje, deberá consultar psych/visitors/to_ruby.rb
y asegurarse de que el nombre se agregue en todos los casos. La mayoría de los tipos pueden incluirse anulando el register
pero hay un par de otros; buscar @st
.
Ahora que el hash tiene asociado el nombre de anclaje deseado, debe hacer que Psych lo use en lugar del Id. De objeto al serializarlo. Esto puede hacerse subclasificando YAMLTree
. Cuando YAMLTree
procesa un objeto, primero verifica si ese objeto ya se ha visto y, si lo tiene, emite un alias . Para cualquier objeto nuevo, registra que ha visto el objeto en caso de que necesite crear un alias más adelante . El object_id
se utiliza como la clave en esto, por lo que debe anular esos dos métodos para verificar la variable de instancia y, en su lugar, usarla si existe:
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
Ahora puede usarlo así (a diferencia de la pregunta anterior, no necesita crear un emisor personalizado en este caso):
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