Ruby: Wie verwandle ich einen Hash in HTTP-Parameter?

205

Das ist ziemlich einfach mit einem einfachen Hash wie

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

was in übersetzen würde

"a=a&b=b"

Aber was machst du mit etwas Komplexerem wie

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

was übersetzen sollte in

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

Oder noch schlimmer (was zu tun ist) mit so etwas wie:

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

Vielen Dank für die sehr geschätzte Hilfe dabei!

Julien Genestoux
quelle
Es hört sich so an, als ob Sie JSON in HTTP-Parameter konvertieren möchten ... Vielleicht benötigen Sie eine andere Codierung?
CookieOfFortune
Hum, das ist eigentlich nicht Json, sondern ein Ruby Hash ... ich bin mir nicht sicher, warum Codierung hier wichtig ist.
Julien Genestoux
Die Antwort von lmanners sollte gefördert werden. Hier gibt es viele großartige Antworten (viele mit hohen Punktzahlen), aber ActiveSupport hat seitdem eine standardisierte Unterstützung hinzugefügt, die die Konversation in Frage stellt. Leider ist die Antwort von lmanner immer noch in der Liste vergraben.
Noach Magedman
2
@Noach meiner Meinung nach sollte jede Antwort, die besagt, dass man sich auf eine Bibliothek verlassen soll, die Kernklassen stark mit Affen-Patches versehen, begraben bleiben. Die Rechtfertigung für eine große Anzahl dieser Patches ist bestenfalls wackelig (siehe Yehuda Katz 'Kommentare in diesem Artikel ), dies ist ein hervorragendes Beispiel. YMMV, aber für mich etwas mit einer Klassenmethode oder das Objekt und Hash nicht öffnet und wo die Autoren nicht sagen würden "kollidiere einfach nicht mit uns!" wäre viel, viel besser.
iain

Antworten:

86

Update: Diese Funktionalität wurde aus dem Edelstein entfernt.

Julien, deine Selbstantwort ist gut, und ich habe sie schamlos entlehnt, aber sie entgeht reservierten Charakteren nicht richtig, und es gibt einige andere Randfälle, in denen sie zusammenbricht.

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]"

Das Juwel ist " adressierbar "

gem install addressable
Bob Aman
quelle
1
Vielen Dank! Was sind die Randfälle, in denen meine Lösung kaputt geht? also kann ich es zu den Spezifikationen hinzufügen?
Julien Genestoux
2
Es behandelt keine Booleschen Werte, und dies ist eindeutig unerwünscht: {"a" => "a & b = b"}. To_params
Bob Aman
3
Zu
Ihrer Information
2
@oif_vet Kannst du sagen, welches Verhalten entfernt wurde? Bobs Vorschlag, das adressierbare Juwel zur Lösung des Problems des Originalplakats zu verwenden, funktioniert für mich ab adressierbar-2.3.2.
Sheldonh
1
@sheldonh, nein, @oif_vet ist korrekt. Ich habe dieses Verhalten entfernt. Tief verschachtelte Strukturen werden in Addressable nicht mehr als Eingaben in den query_valuesMutator unterstützt.
Bob Aman
268

Für einfache, nicht verschachtelte Hashes hat Rails / ActiveSupport 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
quelle
1
Warum sagst du, dass es kaputt ist? Die Ausgabe, die Sie gezeigt haben, ist in Ordnung, nicht wahr?
Tokand
Ich habe es gerade versucht und du scheinst recht zu haben. Vielleicht lag meine Aussage ursprünglich daran, wie eine frühere Version von Rails die Abfragezeichenfolge analysierte (ich schien mich daran zu erinnern, dass sie die vorherigen 'b'-Werte überschrieb). Startete GET "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" für 127.0.0.1 am 2011-03-10 11:19:40 -0600 Verarbeitung durch SitesController # index as HTML-Parameter: {"a" => "a", "b" => ["c", "d", "e"]}
Gabe Martin-Dempesy
Was geht schief, wenn verschachtelte Hashes vorhanden sind? Warum kann ich das nicht verwenden, wenn verschachtelte Hashes vorhanden sind? Für mich entgeht die URL nur dem verschachtelten Hash. Es sollte kein Problem geben, diese in der http-Anfrage zu verwenden.
Sam
2
Ohne Schienen: require 'active_support/all'wird benötigt
Dorian
Zumindest mit Rails 5.2 werden to_queryNullwerte nicht richtig behandelt. { a: nil, b: '1'}.to_query == "a=&b=1", aber Rack und CGI werden beide a=als leere Zeichenfolge analysiert , nicht nil. Ich bin mir nicht sicher, ob andere Server unterstützt werden, aber bei Rails sollte die richtige Abfragezeichenfolge lauten a&b=1. Ich denke, es ist falsch, dass Rails keine
Abfragezeichenfolge
153

Wenn Sie Ruby 1.9.2 oder höher verwenden, können Sie verwenden, URI.encode_www_formwenn Sie keine Arrays benötigen.

ZB (aus den Ruby-Dokumenten in 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"

Sie werden feststellen, dass Array-Werte nicht mit Schlüsselnamen festgelegt werden, die []so enthalten, wie wir es alle in Abfragezeichenfolgen gewohnt sind. Die verwendete Spezifikation encode_www_formentspricht der HTML5-Definition von application/x-www-form-urlencodedDaten.

Bo Jeanes
quelle
8
+1, das ist bei weitem das Beste. Es hängt nicht von Quellen außerhalb von Ruby selbst ab.
Danyel
+1 funktioniert gut mit '{: a => "a" ,: b => {: c => "c" ,: d => true} ,: e => []}' Beispiel
Duke
1
Scheint nicht mit Ruby 2.0 zu funktionieren - der Hash {:c => "c", :d => true}scheint inspiziert zu sein, also als String durchgeschickt.
user208769
1
Es war ein Abschnitt des größeren Ausschnitts oben -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
Beachten Sie, dass dies für Array-Werte andere Ergebnisse hat als für beide Addressable::URIund ActiveSupports Object#to_query.
Matt Huggins
61

Sie müssen den aufgeblähten ActiveSupport nicht laden oder Ihren eigenen rollen, Sie können Rack::Utils.build_queryund verwenden Rack::Utils.build_nested_query. Hier ist ein Blog-Beitrag , der ein gutes Beispiel gibt:

require 'rack'

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

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

Es werden sogar Arrays verarbeitet:

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"]}

Oder das schwierigere verschachtelte Zeug:

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
quelle
Ihr verschachteltes Beispiel zeigt, dass es nicht richtig funktioniert - wenn Sie beginnen, :bbesteht es aus einem Array von zwei Hashes. Am Ende ist :bes eine Reihe von einem größeren Hash.
Ed Ruder
3
@EdRuder gibt es nicht richtig, weil es keinen akzeptierten Standard gibt. Was es zeigt, ist, dass es viel näher ist als jeder andere Versuch, gemessen an den anderen Antworten.
iain
1
Diese Methode ist seit Rails 2.3.8 veraltet: apidock.com/rails/Rack/Utils/build_query
davidgoli
8
@davidgoli Ähm, nicht im Rack, es ist nicht github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Wenn Sie es in Rails verwenden möchten, ist es sicherlich so einfach wie require 'rack'? Es muss vorhanden sein, wenn man bedenkt, dass alle wichtigen Ruby-Webframeworks jetzt auf Rack basieren.
iain
@EdRuder ActiveSupport führt to_queryauch die beiden Arrays zusammen (v4.2).
Kelvin
9

Von Merb stehlen:

# 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

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

Avdi
quelle
1
Leider funktioniert dies nicht, wenn wir ein verschachteltes Array innerhalb der Parameter haben (siehe Beispiel 2) ... :(
Julien Genestoux
2
Und macht keinen König der Flucht.
Ernest
9

Hier ist ein kurzer und süßer Liner, wenn Sie nur einfache ASCII-Schlüssel- / Wert-Abfragezeichenfolgen unterstützen müssen:

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
quelle
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
quelle
3
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

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

Hier ist ein anderer Weg. Für einfache Fragen.

Зелёный
quelle
2
Sie sollten jedoch wirklich sicherstellen, dass Sie Ihren Schlüsseln und Werten ordnungsgemäß URI-entkommen. Auch für einfache Fälle. Es wird dich beißen.
Jrochkind
2

Ich weiß, dass dies eine alte Frage ist, aber ich wollte nur diesen Code posten, da ich kein einfaches Juwel finden konnte, um genau diese Aufgabe für mich zu erledigen.

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

Hier als Juwel aufgerollt: https://github.com/simen/queryparams

svale
quelle
1
URI.escape != CGI.escapeund für die URL möchten Sie die erste.
Ernest
2
Eigentlich nicht, @Ernest. Wenn Sie beispielsweise eine andere URL als Parameter in Ihre URL einbetten (sagen wir, dies ist die Rückgabe-URL, zu der nach der Anmeldung umgeleitet werden soll), behält URI.escape das '?' und '&' der eingebetteten URL brechen die umgebende URL, während CGI.escape sie für später als% 3F und% 26 korrekt verstaut. 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

Der beste Ansatz ist die Verwendung von Hash.to_params, das mit Arrays einwandfrei funktioniert.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
fhidalgo
quelle
Ohne Schienen: require 'active_support/all'wird benötigt
Dorian
1

Wenn Sie sich im Kontext einer Faraday-Anfrage befinden, können Sie auch einfach den Parameter-Hash als zweites Argument übergeben. Faraday sorgt dafür, dass die richtige Parameter-URL daraus wird:

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

Ich benutze dieses Juwel gerne:

https://rubygems.org/gems/php_http_build_query

Beispielnutzung:

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
quelle
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
quelle