Ruby: Bagaimana mengubah hash menjadi parameter HTTP?

205

Itu cukup mudah dengan hash seperti biasa

{:a => "a", :b => "b"} 

yang akan diterjemahkan menjadi

"a=a&b=b"

Tetapi apa yang Anda lakukan dengan sesuatu yang lebih kompleks seperti

{:a => "a", :b => ["c", "d", "e"]} 

yang harus diterjemahkan menjadi

"a=a&b[0]=c&b[1]=d&b[2]=e" 

Atau bahkan lebih buruk, (apa yang harus dilakukan) dengan sesuatu seperti:

{:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]

Terima kasih atas bantuan yang sangat kami hargai!

Julien Genestoux
sumber
Kedengarannya Anda ingin mengubah JSON menjadi HTTP params ... mungkin Anda perlu penyandian yang berbeda?
CookieOfFortune
Hum, ini sebenarnya bukan Json, tapi Ruby Hash ... tidak yakin saya mengerti mengapa encoding penting di sini.
Julien Genestoux
Jawaban oleh lmanners harus dipromosikan. Ada banyak jawaban roll-your-own di sini (banyak dengan skor tinggi) tetapi ActiveSupport sejak itu menambahkan dukungan standar untuk ini, menjadikan perundingan percakapan. Sayangnya, jawaban lmanner masih terkubur dalam daftar.
Noach Magedman
2
@Noach Menurut saya, jawaban apa pun yang mengatakan mengandalkan perpustakaan yang kelas inti tambal sulam harus tetap terkubur. Pembenaran untuk sejumlah besar tambalan itu paling tidak goyah (lihat komentar Yehuda Katz dalam artikel ini ), ini menjadi contoh yang bagus. YMMV, tetapi bagi saya, sesuatu dengan metode kelas atau yang tidak membuka Object dan Hash, dan di mana penulis tidak akan mengatakan "jangan bentrok dengan kami!" akan jauh, jauh lebih baik.
iain

Jawaban:

86

Memperbarui: Fungsi ini telah dihapus dari permata.

Julien, jawaban-dirimu bagus, dan aku tidak tahu malu meminjamnya, tapi itu tidak benar-benar lepas dari karakter yang dilindungi, dan ada beberapa kasus tepi lain di mana ia rusak.

require "addressable/uri"
uri = Addressable::URI.new
uri.query_values = {:a => "a", :b => ["c", "d", "e"]}
uri.query
# => "a=a&b[0]=c&b[1]=d&b[2]=e"
uri.query_values = {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]}
uri.query
# => "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
uri.query_values = {:a => "a", :b => {:c => "c", :d => "d"}}
uri.query
# => "a=a&b[c]=c&b[d]=d"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}}
uri.query
# => "a=a&b[c]=c&b[d]"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}, :e => []}
uri.query
# => "a=a&b[c]=c&b[d]"

Permata itu 'bisa dialamatkan '

gem install addressable
Bob Aman
sumber
1
Terima kasih! Apa saja kasus tepi di mana solusi saya rusak? jadi saya bisa menambahkannya ke spesifikasi?
Julien Genestoux
2
Itu tidak menangani booleans, dan ini jelas tidak diinginkan: {"a" => "a & b = b"}. To_params
Bob Aman
3
FYI, sayangnya perilaku ini telah dihapus dari Addressable pada 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet
2
@oif_vet Bisakah Anda mengatakan perilaku apa yang telah dihapus? Saran Bob menyarankan untuk menggunakan permata yang bisa dialamatkan untuk menyelesaikan masalah poster asli bekerja untuk saya pada addressable-2.3.2.
sheldonh
1
@sheldonh, tidak, @oif_vet benar. Saya menghapus perilaku ini. Struktur yang tersarang tidak lagi didukung dalam Addressable sebagai input ke query_valuesmutator.
Bob Aman
269

Untuk hash dasar, non-bersarang, Rails / ActiveSupport miliki Object#to_query.

>> {:a => "a", :b => ["c", "d", "e"]}.to_query
=> "a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e"
>> CGI.unescape({:a => "a", :b => ["c", "d", "e"]}.to_query)
=> "a=a&b[]=c&b[]=d&b[]=e"

http://api.rubyonrails.org/classes/Object.html#method-i-to_query

Gabe Martin-Dempesy
sumber
1
Mengapa Anda mengatakan itu rusak? output yang Anda tunjukkan ok, bukan?
tokland
Saya baru saja mencobanya dan Anda tampaknya benar. Mungkin pernyataan saya awalnya karena cara versi rel sebelumnya mem-parsing string kueri (saya sepertinya mengingatnya menimpa nilai 'b' sebelumnya). Mulai DAPATKAN "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" untuk 127.0.0.1 di 2011-03-10 11:19:40 -0600 Memproses oleh SitesController # index as Parameter HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Gabe Martin-Dempesy
apa yang salah jika ada hash bersarang? Mengapa saya tidak bisa menggunakan ini ketika ada hash bersarang? Bagi saya, itu hanya url yang lolos dari hash bersarang, seharusnya tidak ada masalah menggunakan ini dalam permintaan http.
Sam
2
Tanpa Rel: require 'active_support/all'diperlukan
Dorian
Setidaknya dengan Rails 5.2 to_querytidak menangani nilai nil dengan benar. { a: nil, b: '1'}.to_query == "a=&b=1", tapi Rack dan CGI keduanya parse a=sebagai string kosong, bukan nil. Saya tidak yakin tentang dukungan untuk server lain, tetapi dengan rel, string kueri yang benar seharusnya a&b=1. Saya pikir itu salah bahwa Rails tidak dapat menghasilkan string kueri yang diuraikan dengan benar dengan sendirinya ...
jsmartt
154

Jika Anda menggunakan Ruby 1.9.2 atau lebih baru, Anda dapat menggunakan URI.encode_www_formjika Anda tidak membutuhkan array.

Misalnya (dari dokumen Ruby di 1.9.3):

URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => "ruby", "lang" => "en")
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
#=> "q=ruby&q=perl&lang=en"
URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
#=> "q=ruby&q=perl&lang=en"

Anda akan melihat bahwa nilai-nilai array tidak disetel dengan nama-nama kunci yang mengandung []seperti kita semua sudah terbiasa dengan string kueri. Spesifikasi yang encode_www_formdigunakan sesuai dengan definisi application/x-www-form-urlencodeddata HTML5 .

Bo Jeanes
sumber
8
+1, sejauh ini yang terbaik. Itu tidak tergantung pada sumber apa pun di luar Ruby itu sendiri.
Danyel
+1 berfungsi dengan baik dengan contoh '{: a => "a",: b => {: c => "c",: d => true},: e => []}'
Duke
1
Tampaknya tidak bekerja dengan ruby ​​2.0 - hash {:c => "c", :d => true}tampaknya diperiksa, jadi dikirim sebagai string.
user208769
1
Itu adalah bagian dari cuplikan yang lebih besar di atas -ruby -ruri -e 'puts RUBY_VERSION; puts URI.encode_www_form({:a => "a", :b => {:c => "c", :d => true}, :e => []})' # outputs 2.0.0 a=a&b=%7B%3Ac%3D%3E%22c%22%2C+%3Ad%3D%3Etrue%7D&
user208769
1
Perhatikan bahwa ini memiliki hasil yang berbeda untuk nilai array daripada keduanya Addressable::URIdan ActiveSupport Object#to_query.
Matt Huggins
61

Tidak perlu memuat ActiveSupport yang kembung atau menggulung sendiri, Anda dapat menggunakan Rack::Utils.build_querydan Rack::Utils.build_nested_query. Berikut ini posting blog yang memberikan contoh yang baik:

require 'rack'

Rack::Utils.build_query(
  authorization_token: "foo",
  access_level: "moderator",
  previous: "index"
)

# => "authorization_token=foo&access_level=moderator&previous=index"

Bahkan menangani array:

Rack::Utils.build_query( {:a => "a", :b => ["c", "d", "e"]} )
# => "a=a&b=c&b=d&b=e"
Rack::Utils.parse_query _
# => {"a"=>"a", "b"=>["c", "d", "e"]}

Atau hal-hal yang lebih sulit:

Rack::Utils.build_nested_query( {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}] } )
# => "a=a&b[][c]=c&b[][d]=d&b[][e]=e&b[][f]=f"
Rack::Utils.parse_nested_query _
# => {"a"=>"a", "b"=>[{"c"=>"c", "d"=>"d", "e"=>"e", "f"=>"f"}]}
iain
sumber
Contoh bersarang Anda menunjukkan bahwa itu tidak berfungsi dengan baik - ketika Anda mulai, :badalah array dari dua hash. Anda berakhir dengan :bmenjadi array hash yang lebih besar.
Ed Ruder
3
@ EdRuder tidak ada yang benar karena tidak ada standar yang diterima. Apa yang ditunjukkannya adalah bahwa itu jauh lebih dekat daripada upaya orang lain, dilihat dari jawaban yang lain.
iain
1
Metode ini sudah tidak digunakan lagi sejak Rails 2.3.8
davidgoli
8
@davidgoli Erm, bukan di Rack, itu bukan github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Jika Anda ingin menggunakannya di Rails, pasti sesederhana itu require 'rack'? Itu harus ada di sana, mengingat semua kerangka kerja Ruby utama dibangun di atas Rak sekarang.
iain
@EdRuder ActiveSupport to_queryjuga menggabungkan 2 array (v4.2).
Kelvin
9

Mencuri dari Merb:

# File merb/core_ext/hash.rb, line 87
def to_params
  params = ''
  stack = []

  each do |k, v|
    if v.is_a?(Hash)
      stack << [k,v]
    else
      params << "#{k}=#{v}&"
    end
  end

  stack.each do |parent, hash|
    hash.each do |k, v|
      if v.is_a?(Hash)
        stack << ["#{parent}[#{k}]", v]
      else
        params << "#{parent}[#{k}]=#{v}&"
      end
    end
  end

  params.chop! # trailing &
  params
end

Lihat http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html

Avdi
sumber
1
Sayangnya, ini tidak berfungsi ketika kita memiliki Array bersarang di dalam params (lihat contoh # 2) ... :(
Julien Genestoux
2
Dan tidak ada raja yang melarikan diri.
Ernest
9

Berikut ini adalah kalimat singkat dan manis jika Anda hanya perlu mendukung string kueri / nilai ASCII sederhana:

hash = {"foo" => "bar", "fooz" => 123}
# => {"foo"=>"bar", "fooz"=>123}
query_string = hash.to_a.map { |x| "#{x[0]}=#{x[1]}" }.join("&")
# => "foo=bar&fooz=123"
Hubro
sumber
4
class Hash
  def to_params
    params = ''
    stack = []

    each do |k, v|
      if v.is_a?(Hash)
        stack << [k,v]
      elsif v.is_a?(Array)
        stack << [k,Hash.from_array(v)]
      else
        params << "#{k}=#{v}&"
      end
    end

    stack.each do |parent, hash|
      hash.each do |k, v|
        if v.is_a?(Hash)
          stack << ["#{parent}[#{k}]", v]
        else
          params << "#{parent}[#{k}]=#{v}&"
        end
      end
    end

    params.chop! 
    params
  end

  def self.from_array(array = [])
    h = Hash.new
    array.size.times do |t|
      h[t] = array[t]
    end
    h
  end

end
Julien Genestoux
sumber
3
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

"a=a&b=b&c=c"

Ini cara lain. Untuk pertanyaan sederhana.

Зелёный
sumber
2
Anda benar-benar harus memastikan bahwa Anda benar URI-melarikan diri kunci dan nilai Anda sekalipun. Bahkan untuk kasus sederhana. Itu akan menggigitmu.
jrochkind
2

Saya tahu ini adalah pertanyaan lama, tetapi saya hanya ingin memposting sedikit kode ini karena saya tidak dapat menemukan permata sederhana untuk melakukan tugas ini untuk saya.

module QueryParams

  def self.encode(value, key = nil)
    case value
    when Hash  then value.map { |k,v| encode(v, append_key(key,k)) }.join('&')
    when Array then value.map { |v| encode(v, "#{key}[]") }.join('&')
    when nil   then ''
    else            
      "#{key}=#{CGI.escape(value.to_s)}" 
    end
  end

  private

  def self.append_key(root_key, key)
    root_key.nil? ? key : "#{root_key}[#{key.to_s}]"
  end
end

Digulung sebagai permata di sini: https://github.com/simen/queryparams

svale
sumber
1
URI.escape != CGI.escapedan untuk URL Anda ingin yang pertama.
Ernest
2
Sebenarnya tidak, @Ernest. Saat mis. Menyematkan url lain sebagai parameter ke url Anda (katakanlah ini adalah url kembali yang akan diarahkan ke setelah masuk) URI.escape akan menyimpan '?' dan '&' dari url tertanam di tempat melanggar url sekitarnya, sementara CGI.escape akan menyelipkannya dengan benar untuk nanti sebagai% 3F dan% 26. CGI.escape("http://localhost/search?q=banana&limit=7") => "http%3A%2F%2Flocalhost%2Fsearch%3Fq%3Dbanana%26limit%3D7" URI.escape("http://localhost/search?q=banana&limit=7") => "http://localhost/search?q=banana&limit=7"
svale
2

Pendekatan terbaik adalah dengan menggunakan Hash.to_params yang merupakan yang berfungsi baik dengan array.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
fhidalgo
sumber
Tanpa Rel: require 'active_support/all'diperlukan
Dorian
1

Jika Anda berada dalam konteks permintaan Faraday, Anda juga bisa meneruskan hash params sebagai argumen kedua dan faraday menangani pembuatan bagian URL yang tepat darinya:

faraday_instance.get(url, params_hsh)
Yo Ludke
sumber
0

Saya suka menggunakan permata ini:

https://rubygems.org/gems/php_http_build_query

Penggunaan sampel:

puts PHP.http_build_query({"a"=>"b","c"=>"d","e"=>[{"hello"=>"world","bah"=>"black"},{"hello"=>"world","bah"=>"black"}]})

# a=b&c=d&e%5B0%5D%5Bbah%5D=black&e%5B0%5D%5Bhello%5D=world&e%5B1%5D%5Bbah%5D=black&e%5B1%5D%5Bhello%5D=world
John
sumber
0
require 'uri'

class Hash
  def to_query_hash(key)
    reduce({}) do |h, (k, v)|
      new_key = key.nil? ? k : "#{key}[#{k}]"
      v = Hash[v.each_with_index.to_a.map(&:reverse)] if v.is_a?(Array)
      if v.is_a?(Hash)
        h.merge!(v.to_query_hash(new_key))
      else
        h[new_key] = v
      end
      h
    end
  end

  def to_query(key = nil)
    URI.encode_www_form(to_query_hash(key))
  end
end

2.4.2 :019 > {:a => "a", :b => "b"}.to_query_hash(nil)
 => {:a=>"a", :b=>"b"}

2.4.2 :020 > {:a => "a", :b => "b"}.to_query
 => "a=a&b=b"

2.4.2 :021 > {:a => "a", :b => ["c", "d", "e"]}.to_query_hash(nil)
 => {:a=>"a", "b[0]"=>"c", "b[1]"=>"d", "b[2]"=>"e"}

2.4.2 :022 > {:a => "a", :b => ["c", "d", "e"]}.to_query
 => "a=a&b%5B0%5D=c&b%5B1%5D=d&b%5B2%5D=e"
mhorbul
sumber