Ruby on Rails:使用 XML 构建器部分

发布于 2024-09-04 21:16:11 字数 1066 浏览 4 评论 0原文

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>

元素应该在 元素下方对齐,它是 < 的子元素/code> 像这样:

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

如果我将 bar/_bar.xml.builder 中的内容复制到模板中,效果很好,但事情并不干燥。

Partials in XML builder are proving to be non-trivial.

After some initial Google searching, I found the following to work, although it's not 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

this will do the trick, except the XML output is not properly indented. the output looks something similar to:

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

The <bar> element should align underneath the <last_updated> element, it is a child of <foo> like this:

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

Works great if I copy the content from bar/_bar.xml.builder into the template, but then things just aren't DRY.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

只是一片海 2024-09-11 21:16:12

也许你应该这样做:

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)

如果您想在结果中包含所有字段。

我不确定所有这些的缩进,因此您可能需要像在外部块中一样创建内部循环的内容,例如不使用部分。

Maybe you should do:

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

Have a look at this link about the xml builder.

In the last alternative you could replace the inner loop with:

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

You can probably also try:

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

if you want to include all fields in the result.

I'm not sure about the indention of all these, so you may need to fall back to create the contents of the inner loop the same way as you do in the outer block, eg using no partial.

云之铃。 2024-09-11 21:16:11

我通过将构建器引用作为局部变量传递来解决这个问题。无需猴子修补。使用原始示例:

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.bar do
  builder.id bar.id
end

另外,上面的内容似乎只适用于 Rails 4。Rails 5 及更高版本请参阅下面 @srghma 的评论

I worked around this by passing in the builder reference as a local in the partial. No monkey patching needed. Using the original example:

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

Then in your partial make sure to use the 'builder' object.

builder.bar do
  builder.id bar.id
end

Also, the above appears to only work up to Rails 4. Rails 5 and up see @srghma's comment below

十二 2024-09-11 21:16:11

不幸的是,对此没有直接的解决方案。当查看代码时,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)

There is unfortunately not a straight-forward solution to this. When looking at the code that ActionPack will initialize the Builder object with then the indent size is hard-coded to 2 and the margin size is not set. Its a shame that there is no mechanism to override this at present.

The ideal solution here would be a fix to ActionPack to allow these options to be passed to the builder but this would require some time investment. I have 2 possible fixes for you. Both dirty you can take your pick which feels less dirty.

Modify the rendering of the partial to render to a string and then do some Regex on it. This would look like this

_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

Note the gsub at the end of render line. This produces the following results

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

That is a little hacky and definitely quite dirty but has the advantage of being contained within your code. The next solution is to monkey-patch ActionPack to get the Builder instance to work the way we want

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

This creates a new class at Rails initialisation called BuilderOptions whose sole purpose is to host 2 values for indent and margin (although we only really need the margin value). I did try adding these variable as class variables directly to the Builder template class but that object was frozen and I couldn't change the values.

Once that class is created we patch the compile method within the TemplateHandler to use these values.

The template then looks as follows :-

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

The basic idea is to set the margin value to the indentation level that we are at when rendering the partial. The XML generated is identical to that shown above.

Please do not copy/paste this code in without checking it against your Rails version to ensure that they are from the same codebase. (I think the above is 2.3.5)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文