2012-11-27 3 views

Répondre

8

Le problème ici est que anchors and aliases en YAML sont un détail de sérialisation, et ainsi ne font pas partie des données après qu'il a été analysé, de sorte que le nom d'ancrage d'origine est inconnue lors de l'écriture des données de retour à Yaml. Afin de garder les noms d'ancre lors du déclenchement, vous devez les stocker quelque part lors de l'analyse afin qu'ils soient disponibles plus tard lors de la sérialisation. Dans Ruby, n'importe quel objet peut avoir des variables d'instance qui lui sont associées, donc un moyen facile d'y parvenir serait de stocker le nom de l'ancre dans une variable d'instance de l'objet en question.

suite de l'exemple dans the earlier question, pour hash nous pouvons changer notre redéfinie méthode revive_hash de sorte que si le hachage est un point d'ancrage alors ainsi que l'enregistrement du nom de l'ancre dans la variable @st afin Alises plus tard peuvent être reconnus, nous ajoutons la comme une variable d'instance sur le hachage.

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 

Notez que cela n'affecte que les mappages yaml qui sont des ancrages. Si vous voulez que d'autres types conservent leur nom d'ancre, vous devrez regarder psych/visitors/to_ruby.rb et vous assurer que le nom est ajouté dans tous les cas. La plupart des types peuvent être inclus en remplaçant register mais il y en a quelques autres; recherche @st. Maintenant que le hash est associé au nom d'ancrage souhaité, vous devez faire en sorte que Psych l'utilise à la place de l'identifiant de l'objet lors de la sérialisation. Cela peut être fait en sous-classant YAMLTree. Lorsque YAMLTree traite un objet, il first checks to see if that object has been seen already, and emits an alias for it if it has. Pour tous les nouveaux objets, il records that it has seen the object in case it needs to create an alias later. Le object_id est utilisé comme clé dans ce domaine, vous avez donc besoin de passer outre ces deux méthodes pour vérifier la variable d'instance, et utiliser à la place si elle 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 

Maintenant, vous pouvez l'utiliser comme ceci (contrairement à la question précédente, vous n'avez pas besoin de créer un émetteur personnalisé dans ce cas):

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 
+0

c'est parfait! Exactement ce que je cherchais :) Comment puis-je, au lieu d'utiliser $ stdout, stocker la sortie dans une variable de chaîne? Je veux écrire la sortie dans le fichier. Merci beaucoup! – Max

+0

@Max Je viens de jeter un oeil à la source, et il y a une façon plus simple de créer la sortie avec 'builder.yaml.La méthode tree' qui renvoie une chaîne si vous ne fournissez aucune entrée. Vous pouvez également fournir un objet 'IO' comme paramètre si vous le souhaitez, afin que vous puissiez écrire directement dans le fichier de cette façon. Voir ma réponse mise à jour. – matt

+0

vous êtes un génie! Appréciez l'aide, psych est vraiment un outil incroyable. – Max

1

est ici une version légèrement modifiée jusqu'à une version plus récente de la gemme psych. avant qu'il m'a donné l'erreur suivante:

NoMethodError - undefined method `[]=' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0> 

la méthode register déplacé dans une sous-classe de YAMLTree, de sorte que cela fonctionne maintenant par rapport à tout ce que dit Matt dans sa réponse:

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 
1

je devais modifier en outre le code que @markus a publié pour fonctionner avec Psych v2.0.17.

Voici ce que j'ai fini avec. J'espère que ça aide quelqu'un d'autre à économiser beaucoup de temps. :-)

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 
Questions connexes