2010-06-06 5 views
16

Les parties dans le générateur XML s'avèrent non triviales.Ruby on Rails: Utilisation des parties partielles XML Builder

Après quelques recherches Google initial, j'ai trouvé ce qui suit au travail, même si ce n'est pas 100%

xml.foo do 
    xml.id(foo.id) 
    xml.created_at(foo.created_at) 
    xml.last_updated(foo.updated_at) 
    foo.bars.each do |bar| 
     xml << render(:partial => 'bar/_bar', :locals => { :bar => bar }) 
    end 
end 

cela fera l'affaire, à l'exception de la sortie XML est pas correctement en retrait. la sortie ressemble à quelque chose de similaire à:

<foo> 
    <id>1</id> 
    <created_at>sometime</created_at> 
    <last_updated>sometime</last_updated> 
<bar> 
    ... 
</bar> 
<bar> 
    ... 
</bar> 
</foo> 

L'élément <bar> doit être aligné sous l'élément <last_updated>, il est un enfant de <foo> comme ceci:

<foo> 
    <id>1</id> 
    <created_at>sometime</created_at> 
    <last_updated>sometime</last_updated> 
    <bar> 
    ... 
    </bar> 
    <bar> 
    ... 
    </bar> 
</foo> 

fonctionne très bien si je copie le contenu de la barre/_bar.xml.builder dans le template, mais alors les choses ne sont pas DRY.

+0

Ce problème est résolu dans Rails 4. – hcarreras

Répondre

16

Il n'y a malheureusement pas de solution simple à cela. Lorsque vous regardez le code qu'ActionPack va initialiser l'objet Builder avec alors la taille de retrait est codée en dur à 2 et la taille de la marge n'est pas définie. C'est dommage qu'il n'y ait pas de mécanisme pour passer outre à cela à l'heure actuelle.

La solution idéale ici serait une solution à ActionPack pour permettre à ces options d'être transmises au constructeur, mais cela nécessiterait un investissement en temps. J'ai 2 corrections possibles pour vous. Les deux sales vous pouvez prendre votre choix qui se sent moins sale.

Modifiez le rendu du partiel à rendre à une chaîne, puis faites-lui une expression rationnelle. Cela ressemblerait à ceci

_bar.xml.builder

xml.bar do 
    xml.id(bar.id) 
    xml.name(bar.name) 
    xml.created_at(bar.created_at) 
    xml.last_updated(bar.updated_at) 
end 

foos/index.xml.builder

xml.foos do 
    @foos.each do |foo| 
    xml.foo do 
     xml.id(foo.id) 
     xml.name(foo.name) 
     xml.created_at(foo.created_at) 
     xml.last_updated(foo.updated_at) 
     xml.bars do 
     foo.bars.each do |bar| 
      xml << render(:partial => 'bars/bar', 
       :locals => { :bar => bar }).gsub(/^/, '  ') 
     end 
     end 
    end 
    end 
end 

Notez le gsub à la fin de la ligne de rendu. Cela produit les résultats suivants

<?xml version="1.0" encoding="UTF-8"?> 
<foos> 
    <foo> 
    <id>1</id> 
    <name>Foo 1</name> 
    <created_at>2010-06-11 21:54:16 UTC</created_at> 
    <last_updated>2010-06-11 21:54:16 UTC</last_updated> 
    <bars> 
     <bar> 
     <id>1</id> 
     <name>Foo 1 Bar 1</name> 
     <created_at>2010-06-11 21:57:29 UTC</created_at> 
     <last_updated>2010-06-11 21:57:29 UTC</last_updated> 
     </bar> 
    </bars> 
    </foo> 
</foos> 

qui est un peu hacky et certainement assez sale, mais a l'avantage d'être contenu dans votre code.La solution suivante consiste à ActionPack singe-patch pour obtenir l'instance Builder pour travailler la façon dont nous voulons

config/initializers/builder_mods.rb

module ActionView 
    module TemplateHandlers 
    class BuilderOptions 
     cattr_accessor :margin, :indent 
    end 
    end 
end 

module ActionView 
    module TemplateHandlers 
    class Builder < TemplateHandler 

     def compile(template) 
     "_set_controller_content_type(Mime::XML);" + 
      "xml = ::Builder::XmlMarkup.new(" + 
      ":indent => #{ActionView::TemplateHandlers::BuilderOptions.indent}, " + 
      ":margin => #{ActionView::TemplateHandlers::BuilderOptions.margin});" + 
      "self.output_buffer = xml.target!;" + 
      template.source + 
      ";xml.target!;" 
     end 
    end 
    end 
end 

ActionView::TemplateHandlers::BuilderOptions.margin = 0 
ActionView::TemplateHandlers::BuilderOptions.indent = 2 

Cela crée une nouvelle classe à l'initialisation Rails appelé BuilderOptions dont le seul but est d'héberger 2 valeurs pour le retrait et la marge (bien que nous ayons seulement vraiment besoin de la valeur de la marge). J'ai essayé d'ajouter ces variables de variable de classe directement à la classe de modèle Builder, mais cet objet était figé et je ne pouvais pas changer les valeurs. Une fois cette classe créée, nous patcherons la méthode de compilation dans TemplateHandler pour utiliser ces valeurs.

Le modèle ressemble à ce qui suit: -

xml.foos do 
    @foos.each do |foo| 
    xml.foo do 
     xml.id(foo.id) 
     xml.name(foo.name) 
     xml.created_at(foo.created_at) 
     xml.last_updated(foo.updated_at) 
     xml.bars do 
     ActionView::TemplateHandlers::BuilderOptions.margin = 3  
     foo.bars.each do |bar| 
      xml << render(:partial => 'bars/bar', :locals => { :bar => bar }) 
     end 
     ActionView::TemplateHandlers::BuilderOptions.margin = 0 
     end 
    end 
    end 
end 

L'idée de base est de définir la valeur de la marge au niveau de retrait que nous sommes à la partie lors du rendu. Le XML généré est identique à celui montré ci-dessus.

Veuillez ne pas copier/coller ce code sans le vérifier par rapport à votre version de Rails afin de vous assurer qu'ils proviennent du même code. (Je pense que ce qui précède est 2.3.5)

+0

J'aime la route patch de singe, mais une mise en garde: rails de mise à niveau pourraient briser ce patch ou une nouvelle fonctionnalité. –

+0

En effet, je m'attends à ce que cela ne fonctionne pas dans Rails3 (sans regarder), c'est pourquoi j'ai commenté que c'était spécifique à 2.3.5. Il est dommage qu'il n'y ait pas de méthode simple pour accrocher ce comportement car j'ai vu d'autres personnes vouloir mettre l'indentation à 0 et cela résoudrait leur cas aussi. –

0

Peut-être que vous devriez faire:

xml.foo do 
    xml.id(foo.id) 
    xml.created_at(foo.created_at) 
    xml.last_updated(foo.updated_at) 
    xml.bars do 
    foo.bars.each do |bar| 
     xml.bar bar.to_xml # or "xml.bar render(:xml => bar)" 
         # or "xml.bar render(bar)" (loads bar/_bar partial) 
    end 
    end 
end 

Jetez un oeil à this link about the xml builder.

Dans la dernière alternative vous pouvez remplacer la boucle interne avec:

xml.bars render(foo.bars) # will loop over bars automatically using bar/_bar 

Vous pouvez probablement essayer aussi:

xml << foo.to_xml(:include => :bars) 

si vous souhaitez inclure tous les champs du résultat. Je ne suis pas sûr de l'indentation de tout cela, donc vous devrez peut-être vous replier pour créer le contenu de la boucle interne de la même manière que dans le bloc externe, par exemple en n'utilisant pas de partiel.

28

J'ai travaillé autour de cela en passant dans la référence du constructeur comme un local dans le partiel. Aucun correctif de singe nécessaire. En utilisant l'exemple original:

xml.foo do 
    xml.id(foo.id) 
    xml.created_at(foo.created_at) 
    xml.last_updated(foo.updated_at) 
    foo.bars.each do |bar| 
     render(:partial => 'bar/_bar', :locals => {:builder => xml, :bar => bar }) 
    end 
end 

Ensuite, assurez-vous d'utiliser l'objet 'builder'.

builder.bar do 
    builder.id bar.id 
end 
+1

Nice, merci Alex! Une mise en garde: assurez-vous de renommer la variable xml dans le hachage local. Je faisais ': locals => {: xml => xml}' et cela ne fonctionnait PAS. – ipd

+6

Cela devrait être la réponse acceptée! – rewritten

+2

C'est la bonne réponse aux rails 4! –