Railsのxmlのフォーマット調整でハマったこと

Railsでxmlを表示させるときに`type="integer"`や`nil="true`がつかないように対応したときのメモです。

```rb
  def index
    ...(処理)...
    render xml: data
  end
```
のようにxml形式で出力するときに以下のデータ

|カラム|値|
|---|---|
|id|1|
|name|太郎|
|name_kana|タロウ|
|age|20|
|notice|nil|

を表示させると

```xml
<object>
  <id>1</id>
  <name>太郎</name>
  <name-kana>タロウ</name-kana>
  <age type="integer">20</age>
  <notice nil="true"/>
</object>
```
といった形で、
1. 文字列以外の要素にデータ型を示す属性が入る
2. キーのアンダーバーがハイフンになる
3. 値がない要素にnilという属性が入る

のようになりました。

要件でフォーマットが決まっている場合などはこのままだとNGなので変更する必要があるのですが、
その際に色々とハマった点があったのでメモとして残します。

# 文字列以外の要素にデータ型を示す属性が入る

以下で解消できました。

```diff
  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であれば[ここ](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/conversions.rb#L183-L211)

この中で、オプションとして`skip_types`をtrueにすればタイプが付与されないことがコメントにも記載されています。

# キーのアンダーバーがハイフンになる

以下で解消できました。

```diff
  def index
    ...(処理)...
-   render xml: data
+   render xml: data, dasherize: false
  end
```

こちらは`skip_types`とは異なり、Array.to_xmlメソッドの中ではなく、
to_xmlメソッドの中で呼ばれているActiveSupport::XmlMiniのrename_keyメソッド内に設定がありました。
メソッドは[ここ](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini.rb#L147-L155)

このメソッドはHash等のキーをxmlのタグに置き換える箇所ですが、`dasherize`がtrueの場合、アンダーバーと半角スペースをハイフンに置き換えています。
※正確には「アンダーバーと半角スペース以外の文字で囲まれているアンダーバーや半角スペースを置き換える」です。

なので`dasherize`をfalseにすれば置換が発生しなくなります。

# 値がない要素にnilという属性が入る

これだけ、オプションで対応できないため、以下の方法で対応しました。

* `nil="true"`を取り除くbuilderを作成してそれを使う

具体的には以下です。

## builderを作成

```rb
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を呼び出す

```diff
  def index
    ...(処理)...
-   render xml: data
+   render xml: data, builder: Sample::XmlMarkup.new()
  end
```

`nil="true"`を付与する処理だけ、

ActiveSupport::XmlMiniのto_tagメソッド内ので

```rb
attributes[:nil] = true if value.nil?
```

のように問答無用で付与するようになっているのと、to_tagメソッドをオーバーライドするようなことが難しそうだったので、
ここで付与された属性を取り除くbuilderを作って対応しました。

Builder::XmlBaseクラスのtag!メソッド内を見ると

```rb:builder/xmlbase.rb
        when nil
          attrs ||= {}
          attrs.merge!({:nil => true}) if explicit_nil_handling?
        else
```

と条件分岐が書かれていて処理をたどっていくと
Builder::XmlMarkupのコンストラクタで

```rb:builder/xmlmarkup.rb
    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メソッドでは

```rb:xml_mini.rb
...(省略)...
attributes[:nil] = true if value.
...(省略)...
options[:builder].tag!(key, formatted_value, attributes)
```
とtag!メソッドを呼び出しているんですが、

XmlMarkupのtag!メソッドは

```rb:xml_markup.rb
def tag!(sym, *args, &block)
```

というように`*args`で受け取っているので値がnilのタグのargsは

```rb
[nil, { nil: true }]
```
という値になります。
なので

```rb
args = args - [{ nil: true }]
```

というやり方にしていますが、もう少しスマートな方法があるかもしれないです。

もしもっといいやり方ご存知でしたら教えて下さい。