Railsでxmlを表示させるときにtype="integer"
やnil="true
がつかないように対応したときのメモです。
def index
...(処理)...
render xml: data
end
のようにxml形式で出力するときに以下のデータ
カラム | 値 |
---|---|
id | 1 |
name | 太郎 |
name_kana | タロウ |
age | 20 |
notice | nil |
を表示させると
<object>
<id>1</id>
<name>太郎</name>
<name-kana>タロウ</name-kana>
<age type="integer">20</age>
<notice nil="true"/>
</object>
といった形で、
- 文字列以外の要素にデータ型を示す属性が入る
- キーのアンダーバーがハイフンになる
- 値がない要素にnilという属性が入る
のようになりました。
要件でフォーマットが決まっている場合などはこのままだとNGなので変更する必要があるのですが、
その際に色々とハマった点があったのでメモとして残します。
文字列以外の要素にデータ型を示す属性が入る
以下で解消できました。
def index
...(処理)...
- render xml: data
+ render xml: data, skip_types: true
end
Railsではxmlのフォーマットを作成するのにArrayやHashを渡しますが、
active_supportの中でArrayやHashに対してto_xmlメソッドを追加しています。
Arrayのto_xmlであればここ
この中で、オプションとしてskip_types
をtrueにすればタイプが付与されないことがコメントにも記載されています。
キーのアンダーバーがハイフンになる
以下で解消できました。
def index
...(処理)...
- render xml: data
+ render xml: data, dasherize: false
end
こちらはskip_types
とは異なり、Array.to_xmlメソッドの中ではなく、
to_xmlメソッドの中で呼ばれているActiveSupport::XmlMiniのrename_keyメソッド内に設定がありました。
メソッドはここ
このメソッドはHash等のキーをxmlのタグに置き換える箇所ですが、dasherize
がtrueの場合、アンダーバーと半角スペースをハイフンに置き換えています。
※正確には「アンダーバーと半角スペース以外の文字で囲まれているアンダーバーや半角スペースを置き換える」です。
なのでdasherize
をfalseにすれば置換が発生しなくなります。
値がない要素にnilという属性が入る
これだけ、オプションで対応できないため、以下の方法で対応しました。
-
nil="true"
を取り除くbuilderを作成してそれを使う
具体的には以下です。
builderを作成
require 'builder/xmlmarkup'
# 値がnilのものを表示させるのにnil=trueをつけないbuilder
module Sample
class XmlMarkup < ::Builder::XmlMarkup
def tag!(sym, *args, &block)
# nil=trueの属性を除去
args = args - [{ nil: true }]
# あとは元々の処理に任せる
super
end
end
end
上記のbuilderを呼び出す
def index
...(処理)...
- render xml: data
+ render xml: data, builder: Sample::XmlMarkup.new()
end
nil="true"
を付与する処理だけ、
ActiveSupport::XmlMiniのto_tagメソッド内ので
attributes[:nil] = true if value.nil?
のように問答無用で付与するようになっているのと、to_tagメソッドをオーバーライドするようなことが難しそうだったので、
ここで付与された属性を取り除くbuilderを作って対応しました。
Builder::XmlBaseクラスのtag!メソッド内を見ると
when nil
attrs ||= {}
attrs.merge!({:nil => true}) if explicit_nil_handling?
else
と条件分岐が書かれていて処理をたどっていくと
Builder::XmlMarkupのコンストラクタで
def initialize(options={})
indent = options[:indent] || 0
margin = options[:margin] || 0
@quote = (options[:quote] == :single) ? "'" : '"'
@explicit_nil_handling = options[:explicit_nil_handling]
super(indent, margin)
@target = options[:target] || ""
end
と、explicit_nil_handling
オプションで設定できるようなのですが、その前の
ActiveSupport::XmlMini側でnil="true"
の要素を付与してtag!メソッドを呼び出しているため、このオプションが意味をなさなくなっていました。
ActiveSupport::XmlMiniのto_tagメソッドはオプションで渡されたxmlのbuilderのtag!メソッドを呼び出すので、
tag!メソッド内で{ nil: true }
を取り除く処理を入れました。
注意点としてXmlMiniのto_tagメソッドでは
...(省略)...
attributes[:nil] = true if value.
...(省略)...
options[:builder].tag!(key, formatted_value, attributes)
とtag!メソッドを呼び出しているんですが、
XmlMarkupのtag!メソッドは
def tag!(sym, *args, &block)
というように*args
で受け取っているので値がnilのタグのargsは
[nil, { nil: true }]
という値になります。
なので
args = args - [{ nil: true }]
というやり方にしていますが、もう少しスマートな方法があるかもしれないです。
もしもっといいやり方ご存知でしたら教えて下さい。