Ruby on Rails: использование XML Builder Partials - PullRequest
17 голосов
/ 07 июня 2010

Частицы в XML-компоновщике оказываются нетривиальными.

После некоторого начального поиска в Google я обнаружил, что работает следующее, хотя это не 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

это поможет, за исключением того, что вывод XML не имеет правильного отступа. вывод выглядит примерно так:

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

Элемент <bar> должен быть выровнен под элементом <last_updated>, это дочерний элемент <foo>, например:

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

Отлично работает, если я скопирую содержимое из bar / _bar.xml.builder в шаблон, но тогда все просто не СУХОЙ.

Ответы [ 3 ]

30 голосов
/ 03 июня 2011

Я работал над этим, передавая ссылку на конструктор как локальный в партиале. Нет необходимости в исправлении обезьян. Используя оригинальный пример:

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

Тогда в своей части обязательно используйте объект 'builder'.

builder.bar do
  builder.id bar.id
end

Кроме того, приведенное выше работает только до Rails 4. Rails 5 и выше, см. Комментарий @ srghma ниже

17 голосов
/ 12 июня 2010

К сожалению, нет прямого решения этой проблемы.Если посмотреть на код, с помощью которого ActionPack инициализирует объект Builder, размер отступа будет жестко задан равным 2, а размер поля не задан.Жаль, что в настоящее время не существует механизма, позволяющего переопределить это.

Идеальным решением здесь было бы исправление ActionPack, позволяющее передавать эти параметры сборщику, но это потребует некоторых временных затрат.У меня есть 2 возможных исправления для вас.Оба грязных, вы можете сделать свой выбор, который кажется менее грязным.

Измените рендеринг партиала для рендеринга в строку, а затем примените к нему некоторое регулярное выражение.Это будет выглядеть так:

_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

Обратите внимание на gsub в конце строки рендера.Это приводит к следующим результатам

<?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>

Это немного глупо и определенно довольно грязно, но имеет то преимущество, что содержится в вашем коде.Следующее решение состоит в том, чтобы обезопасить ActionPack, чтобы заставить экземпляр Builder работать так, как мы хотим

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

Это создаетновый класс при инициализации Rails под названием BuilderOptions, единственной целью которого является размещение 2 значений для отступа и маржи (хотя нам действительно нужно только значение маржи).Я пытался добавить эти переменные как переменные класса непосредственно в шаблонный класс Builder, но этот объект был заморожен, и я не мог изменить значения.

Как только этот класс создан, мы исправляем метод компиляции в TemplateHandler для использованияэти значения.

Затем шаблон выглядит следующим образом: -

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

Основная идея состоит в том, чтобы установить значение поля на уровне отступа, который мы имеем при рендеринге партиала.Сгенерированный XML идентичен показанному выше.

Пожалуйста, не копируйте и не вставляйте этот код, не сверяя его с вашей версией Rails, чтобы убедиться, что они из одной и той же кодовой базы.(Думаю выше 2.3.5)

0 голосов
/ 11 июня 2010

Может быть, вы должны сделать:

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

Посмотрите эту ссылку о сборщике XML .

В последнем варианте вы можете заменить внутренний цикл на:

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

Вы также можете попробовать:

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

если вы хотите включить в результат все поля.

Я не уверен насчет отступа всего этого, поэтому вам может понадобиться отступить, чтобы создать содержимое внутреннего цикла так же, как вы делаете это во внешнем блоке, например, без использования частичного.

...