はじめに
以下のように同じ名前のフィールドをそれぞれ2つ定義するとします。
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
Rails側に送られてくるパラメータは以下です。
"sample" => {
"form_items_attributes" => {
"0" => {
"option_settings" => [
{"label"=>"a", "description"=>"b"},
{"label"=>"a", "description"=>"b"}
]
}
}
}
option_settingsの配列の中のハッシュが2つに分割されてパラメータとして送られてきます。
これを見るに同じキーがあれば別のハッシュに分割しているように見えます。この記事ではそれが本当なのかRackの実装を通して確認してみました。
Rackのバージョンは2.2.3.1で確認しました。
クエリ文字列のパース
Rails側で参照するパラメータはRackのRack::QueryParser#parse_nested_query
でパースしています。
64: def parse_nested_query(qs, d = nil)
65: params = make_params
66:
67: unless qs.nil? || qs.empty?
68: (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
69: k, v = p.split('=', 2).map! { |s| unescape(s) }
70:
71: normalize_params(params, k, v, param_depth_limit)
72: end
73: end
74:
75: return params.to_h
76: rescue ArgumentError => e
77: raise InvalidParameterError, e.message, e.backtrace
78: end
qsはクエリ文字列でそこからキーと値を一つずつ取り出しています。
pry(#<Rack::QueryParser>)> k
=> "sample[form_items_attributes][0][option_settings][][label]"
pry(#<Rack::QueryParser>)> v
=> "a"
クエリ文字列のキーと値からパラメータを生成するのはRack::QueryParser#normalize_params
で行われています。
83: def normalize_params(params, name, v, depth)
84: raise RangeError if depth <= 0
85:
86: name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
87: k = $1 || ''
88: after = $' || ''
89:
90: if k.empty?
91: if !v.nil? && name == "[]"
92: return Array(v)
93: else
94: return
95: end
pry(#<Rack::QueryParser>)> name
=> "sample[form_items_attributes][0][option_settings][][label]"
引数name
として渡されたキーを以下のように先頭のキー(k
)とそれ以外(after
)に分割しています。
pry(#<Rack::QueryParser>)> k
=> "sample"
pry(#<Rack::QueryParser>)> after
=> "[form_items_attributes][0][option_settings][][label]"
ここで、分割したk
をキーにパラメータを生成しています。パラメータの生成は、after
をRack::QueryParser#normalize_params
にキーとして渡して再帰処理で生成しているようです。
115: else
116: params[k] ||= make_params
117: raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
118: params[k] = normalize_params(params[k], after, v, depth - 1)
119: end
今回知りたいのは、option_settingsの配列の中のハッシュをどのように生成しているかです。
以下のように、k
とafter
が分割されたときにその処理が行われます。
pry(#<Rack::QueryParser>)> k
=> "option_settings"
pry(#<Rack::QueryParser>)> after
=> "[][label]"
このとき以下の処理が実行されます。
106: elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
107: child_key = $1
108: params[k] ||= []
109: raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
110:
111: if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
112: normalize_params(params[k].last, child_key, v, depth - 1)
113: else
114: params[k] << normalize_params(make_params, child_key, v, depth - 1)
115: end
この処理のなかで生成済みのパラメータのハッシュの中に、これから追加するキーと同じキーがあるのかどうかを!params_hash_has_key?(params[k].last, child_key)
で判定しています。
この判定により"option_settings" => []
の配列の中のハッシュに要素を追加するか、新しくハッシュを生成して追加するということを行っています。
例えば以下の場合、child_key
("label")が生成済みのパラメータにキーとして含まれているので、params[k] << normalize_params(make_params, child_key, v, depth - 1)
が実行され、新しいハッシュとして配列に追加されます。
pry(#<Rack::QueryParser>)> params
=> #<Rack::QueryParser::Params:0x0000004089fee808 @limit=65536, @params={"option_settings"=>[#<Rack::QueryParser::Params:0x00000040883d7598 @limit=65536, @params={"label"=>"a", "description"=>"b"}, @size=16>]}, @size=15>
pry(#<Rack::QueryParser>)> k
=> "option_settings"
pry(#<Rack::QueryParser>)> params[k].last
=> #<Rack::QueryParser::Params:0x00000040883d7598 @limit=65536, @params={"label"=>"a", "description"=>"b"}, @size=16>
pry(#<Rack::QueryParser>)> child_key
=> "label"
ここまでで、配列の中のハッシュはキーが同じなら別のハッシュに分割してくれているということが確認できました。
ということなので3つのlabelのフィールドがあり、以下のようにother
というフィールドを最後の項目の先頭に定義した場合、
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]"%>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][other]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
以下のように2番目のハッシュにother
が格納されます。
"sample" => {
"form_items_attributes" => {
"0" => {
"option_settings" => [
{"label"=>"a", "description"=>"b"},
{"label"=>"a", "description"=>"b", "other"=>"c"},
{"label"=>"a", "description"=>"b"}
]
}
}
}
3番目のハッシュの先頭にother
を追加したい場合は、1番目と2番目にも空の文字列なりを設定したダミーのother
フィールドを追加する必要があります。
<%= hidden_field_tag "sample[form_items_attributes][0][option_settings][][other]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
<%= hidden_field_tag "sample[form_items_attributes][0][option_settings][][other]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][other]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][label]" %>
<%= text_field_tag "sample[form_items_attributes][0][option_settings][][description]" %>
この場合、以下のようにパースされます。
"sample" => {
"form_items_attributes" => {
"0" => {
"option_settings" => [
{"other"=>"", "label"=>"a", "description"=>"b"},
{"other"=>"", "label"=>"a", "description"=>"b"},
{"other"=>"c", "label"=>"a", "description"=>"b"}
]
}
}
}
まとめ
Rackのパースの実装を見てみました。配列の中のハッシュはキーが同じなら別のハッシュに分割してくれるということがわかりました。