1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-06

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>

といった形で、

  1. 文字列以外の要素にデータ型を示す属性が入る
  2. キーのアンダーバーがハイフンになる
  3. 値がない要素に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!メソッド内を見ると

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

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

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メソッドでは

xml_mini.rb
...(省略)...
attributes[:nil] = true if value.
...(省略)...
options[:builder].tag!(key, formatted_value, attributes)

とtag!メソッドを呼び出しているんですが、

XmlMarkupのtag!メソッドは

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

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

[nil, { nil: true }]

という値になります。
なので

args = args - [{ nil: true }]

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?