Ruby: ¿Cómo convertir un hash en parámetros HTTP?

205

Eso es bastante fácil con un simple hash como

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

lo que se traduciría en

"a=a&b=b"

¿Pero qué haces con algo más complejo como

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

que debería traducirse en

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

O peor aún, (qué hacer) con algo como:

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

¡Gracias por la apreciada ayuda con eso!

Julien Genestoux
fuente
Parece que desea convertir JSON en parámetros HTTP ... ¿tal vez necesita una codificación diferente?
CookieOfFortune
Hum, esto en realidad no es Json, sino un Ruby Hash ... no estoy seguro de entender por qué la codificación importa aquí.
Julien Genestoux
La respuesta de los moradores debe ser promovida. Aquí hay muchas respuestas geniales para tirar (muchas con puntajes altos), pero ActiveSupport ha agregado soporte estandarizado para esto, lo que hace que la conversación sea discutible. Desafortunadamente, la respuesta de lmanner todavía está enterrada en la lista.
Noach Magedman
2
@Noach, en mi opinión, cualquier respuesta que diga que se debe confiar en una biblioteca que en gran medida parchea a las clases básicas debería permanecer enterrada. La justificación para una gran cantidad de esos parches es inestable en el mejor de los casos (eche un vistazo a los comentarios de Yehuda Katz en este artículo ), siendo este un excelente ejemplo. YMMV, pero para mí, algo con un método de clase o que no abre Object and Hash, y donde los autores no dirían "¡simplemente no choques con nosotros!" Sería mucho, mucho mejor.
entre

Respuestas:

86

Actualización: esta funcionalidad se eliminó de la gema.

Julien, tu respuesta es buena, y me la tomé descaradamente, pero no escapa de los personajes reservados, y hay algunos otros casos extremos en los que se descompone.

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

La gema es ' direccionable '

gem install addressable
Bob Aman
fuente
1
¡Gracias! ¿Cuáles son los casos extremos donde se rompe mi solución? entonces puedo agregarlo a las especificaciones?
Julien Genestoux
2
No maneja booleanos, y esto es claramente indeseable: {"a" => "a & b = b"}. To_params
Bob Aman
3
Para su información, desafortunadamente este comportamiento se ha eliminado de Direccionable a partir de 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet
2
@oif_vet ¿Podría decir qué comportamiento se ha eliminado? La sugerencia sugerida por Bob de usar la gema direccionable para resolver el problema del póster original me funciona a partir de direccionable-2.3.2.
sheldonh
1
@sheldonh, no, @oif_vet es correcto. Eliminé este comportamiento. Las estructuras profundamente anidadas ya no son compatibles con Addressable como entradas para el query_valuesmutador.
Bob Aman el
269

Para hashes básicos no anidados, Rails / ActiveSupport tiene 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
fuente
1
¿Por qué dices que está roto? la salida que mostraste está bien, ¿no?
tokland
Lo intenté y parece que tienes razón. Tal vez mi declaración se debió originalmente a la forma en que una versión anterior de rails analizaba la cadena de consulta (parecía recordar que sobrescribía los valores 'b' anteriores). Comenzó GET "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" para 127.0.0.1 en 2011-03-10 11:19:40 -0600 Procesamiento por SitesController # index as Parámetros HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Gabe Martin-Dempesy
¿Qué sale mal si hay hashes anidados? ¿Por qué no puedo usar esto cuando hay hashes anidados? Para mí, solo la URL escapa al hash anidado, no debería haber ningún problema al usar esto en la solicitud http.
Sam
2
Sin rieles: require 'active_support/all'es necesario
Dorian
Al menos con Rails 5.2 to_queryno maneja los valores nulos correctamente. { a: nil, b: '1'}.to_query == "a=&b=1", pero Rack y CGI se analizan a=como una cadena vacía, no nil. No estoy seguro sobre el soporte para otros servidores, pero con los rieles, la cadena de consulta correcta debería ser a&b=1. Creo que está mal que Rails no pueda producir una cadena de consulta que se
analice
154

Si está utilizando Ruby 1.9.2 o posterior, puede usarlo URI.encode_www_formsi no necesita matrices.

Por ejemplo (de los documentos de Ruby en 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"

Notarás que los valores de la matriz no están configurados con nombres de claves que contienen []como todos a los que estamos acostumbrados en las cadenas de consulta. La especificación que encode_www_formutiliza está de acuerdo con la definición de application/x-www-form-urlencodeddatos HTML5 .

Bo Jeanes
fuente
8
+1, este es, de lejos, el mejor. No depende de ninguna fuente fuera de Ruby.
Danyel
+1 funciona bien con '{: a => "a",: b => {: c => "c",: d => true},: e => []}' ejemplo
Duke
1
No parece funcionar con ruby ​​2.0: el hash {:c => "c", :d => true}parece ser inspeccionado, por lo que se envía como una cadena.
user208769
1
Era una sección del fragmento más grande de arribaruby -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
Tenga en cuenta que esto tiene resultados diferentes para los valores de matriz que ambos Addressable::URIy ActiveSupport Object#to_query.
Matt Huggins
61

No es necesario cargar el ActiveSupport hinchado o rodar el suyo, puede usar Rack::Utils.build_queryy Rack::Utils.build_nested_query. Aquí hay una publicación de blog que da un buen ejemplo:

require 'rack'

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

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

Incluso maneja matrices:

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

O las cosas anidadas más difíciles:

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
fuente
Su ejemplo anidado demuestra que no funciona correctamente: cuando comienza, :bes una matriz de dos hashes. Terminas :bsiendo un conjunto de un hash más grande.
Ed Ruder
3
@EdRuder no existe correctamente porque no hay un estándar aceptado. Lo que sí muestra es que está mucho más cerca que el intento de cualquier otra persona, a juzgar por las otras respuestas.
iain
1
Este método está en desuso desde Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli
8
@davidgoli Erm, no en Rack no es github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Si quieres usarlo en Rails, seguramente es tan simple como require 'rack'? Debe estar allí, teniendo en cuenta que todos los principales marcos web de Ruby están construidos sobre Rack ahora.
entre
@EdRuder ActiveSupport's to_querytambién combina las 2 matrices (v4.2).
Kelvin
9

Robar de 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

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

Avdi
fuente
1
Desafortunadamente, esto no funciona cuando tenemos una matriz anidada dentro de los parámetros (ver ejemplo # 2) ... :(
Julien Genestoux
2
Y no hace ningún rey de escapar.
Ernest
9

Aquí hay un breve y dulce delineador si solo necesita admitir cadenas de consulta de clave / valor ASCII simples:

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

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

Aquí hay otra forma. Para consultas simples.

Зелёный
fuente
2
Sin embargo, realmente debe asegurarse de que está escapando correctamente de URI de sus claves y valores. Incluso para casos simples. Te morderá.
jrochkind
2

Sé que esta es una vieja pregunta, pero solo quería publicar este fragmento de código ya que no pude encontrar una gema simple para hacer esta tarea por mí.

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

Enrollado como gema aquí: https://github.com/simen/queryparams

svale
fuente
1
URI.escape != CGI.escapey para la URL quieres la primera.
Ernest
2
En realidad no, @Ernest. Cuando, por ejemplo, incrusta otra url como parámetro a su url (digamos que esta es la url de retorno a la que se redireccionará después del inicio de sesión) URI.escape mantendrá el '?' y '&' de la url incrustada en su lugar rompiendo la url circundante, mientras que CGI.escape los guardará correctamente para más tarde como% 3F y% 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

El mejor enfoque es usar Hash.to_params, que es el que funciona bien con las matrices.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
fhidalgo
fuente
Sin rieles: require 'active_support/all'es necesario
Dorian
1

Si se encuentra en el contexto de una solicitud de Faraday, también puede pasar el hash de params como el segundo argumento y faraday se encarga de hacer que la URL de param adecuada forme parte de él:

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

Me gusta usar esta gema:

https://rubygems.org/gems/php_http_build_query

Uso de la muestra:

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
Juan
fuente
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
fuente