Ruby: Como transformar um hash em parâmetros HTTP?

205

Isso é bem fácil com um hash simples como

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

o que se traduziria em

"a=a&b=b"

Mas o que você faz com algo mais complexo como

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

que deve se traduzir em

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

Ou pior ainda, (o que fazer) com algo como:

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

Obrigado pela ajuda muito apreciada com isso!

Julien Genestoux
fonte
Parece que você deseja converter JSON em parâmetros HTTP ... talvez você precise de uma codificação diferente?
CookieOfFortune 28/04/09
Hum, na verdade não é Json, mas um Ruby Hash ... não sei se entendo por que a codificação é importante aqui.
Julien Genestoux
A resposta dos membros deve ser promovida. Existem muitas respostas ótimas aqui (muitas com altas pontuações), mas o ActiveSupport desde então adicionou suporte padronizado a isso, tornando a conversa discutível. Infelizmente, a resposta de lmanner ainda está escondida na lista.
Noach Magedman
2
@ Approach, na minha opinião, qualquer resposta que diga que depende de uma biblioteca em que as principais classes de patches de macacos deve permanecer enterrada. A justificativa para um grande número desses patches é instável, na melhor das hipóteses (dê uma olhada nos comentários de Yehuda Katz neste artigo ), sendo este um excelente exemplo. YMMV, mas para mim, algo com um método de classe ou que não abre Object e Hash, e onde os autores não diriam "simplesmente não colidam conosco!" seria muito, muito melhor.
Iain 16/05

Respostas:

86

Atualizar: essa funcionalidade foi removida da gema.

Julien, sua resposta é boa, e eu tenho vergonhosamente a emprestado, mas ela não escapa de maneira adequada aos caracteres reservados, e há outros casos extremos em que ocorre.

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

A gema é ' endereçável '

gem install addressable
Bob Aman
fonte
1
THX! Quais são os casos extremos onde minha solução é interrompida? para que eu possa adicioná-lo às especificações?
Julien Genestoux 05/05
2
Ele não lida com booleans, e este é claramente indesejável:. { "A" => "A & B = b"} to_params
Bob Aman
3
FYI, infelizmente este comportamento foi removido endereçável a partir de 2,3 ( github.com/sporkmonger/addressable/commit/... )
oif_vet
2
@oif_vet Você poderia dizer qual comportamento foi removido? A sugestão sugerida por Bob de usar a gema endereçável para resolver o problema do pôster original funciona para mim a partir do endereçável-2.3.2.
sheldonh
1
@sheldonh, não, @oif_vet está correto. Eu removi esse comportamento. Estruturas profundamente aninhadas não são mais suportadas em Endereçável como entradas para o query_valuesmutador.
Bob Aman
269

Para hashes básicos e não aninhados, o Rails / ActiveSupport possui 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
fonte
1
Por que você diz que está quebrado? a saída que você mostrou está ok, não está?
tokland
Eu apenas tentei e você parece estar certo. Talvez minha declaração tenha sido originalmente devida à maneira como uma versão anterior do Rails analisou a string de consulta (eu me lembrei dela substituindo os valores 'b' anteriores). GET iniciado "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" para 127.0.0.1 em 10-03-2011 11:19:40 -0600 Processando por SitesController # index as Parâmetros HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Gabe Martin-Dempesy 30 /
o que dá errado se houver hashes aninhados? Por que não posso usar isso quando há hashes aninhados? Para mim, apenas o URL escapa ao hash aninhado, não deve haver nenhum problema em usar isso na solicitação http.
Sam
2
Sem Rails: require 'active_support/all'é necessário
Dorian
Pelo menos com o Rails 5.2 to_querynão lida com valores nulos adequadamente. { a: nil, b: '1'}.to_query == "a=&b=1", mas Rack e CGI analisam a=como uma sequência vazia, não nil. Não tenho certeza sobre o suporte a outros servidores, mas com os trilhos, a string de consulta correta deve ser a&b=1. Eu acho errado que o Rails não possa produzir uma string de consulta que seja corretamente analisada por si mesma ...
jsmartt 18/06
154

Se você estiver usando Ruby 1.9.2 ou posterior, poderá usar URI.encode_www_form lo se não precisar de matrizes.

Por exemplo (dos documentos Ruby em 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"

Você notará que os valores da matriz não são definidos com nomes de chaves que contêm []como todos nos acostumamos nas cadeias de consulta. A especificação encode_www_formusada está de acordo com a definição de application/x-www-form-urlencodeddados HTML5 .

Bo Jeanes
fonte
8
+1, este é de longe o melhor. Não depende de nenhuma fonte fora do próprio Ruby.
21413 Danyel
+1 funciona bem com o exemplo '{: a => "a",: b => {: c => "c",: d => true},: e => []}'
Duke,
1
Parece não funcionar com o ruby ​​2.0 - o hash {:c => "c", :d => true}parece ser inspecionado, então enviado como uma string.
user208769
1
Era uma seção do snippet maior acima -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
Observe que isso tem resultados diferentes para valores de matriz que os do Addressable::URIActiveSupport Object#to_query.
Matt Huggins
61

Não há necessidade de carregar o ActiveSupport inchado ou rolar o seu próprio, você pode usar Rack::Utils.build_querye Rack::Utils.build_nested_query. Aqui está uma postagem de blog que dá um bom exemplo:

require 'rack'

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

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

Ele até lida com matrizes:

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

Ou as coisas aninhadas mais difíceis:

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
fonte
Seu exemplo aninhado demonstra que ele não funciona corretamente - quando você inicia, :bé uma matriz de dois hashes. Você acaba :bsendo uma matriz de um hash maior.
precisa
3
@ EdRuder não existe corretamente porque não há padrão aceito. O que mostra é que é muito mais próximo do que a tentativa de qualquer outra pessoa, a julgar pelas outras respostas.
21313
1
Este método está obsoleto desde o Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli
8
@davidgoli Erm, não no Rack , não é github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Se você quiser usá-lo no Rails, certamente é tão simples quanto require 'rack'? Ele deve estar lá, considerando que todas as principais estruturas da Web Ruby são construídas sobre o Rack agora.
iain
O @EdRuder ActiveSupport to_querytambém mescla as duas matrizes (v4.2).
Kelvin
9

Roubar do 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

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

Avdi
fonte
1
Infelizmente, isso não funciona quando temos uma matriz aninhada dentro dos parâmetros (veja o exemplo 2) ... :(
Julien Genestoux
2
E não faz nenhum rei de escapar.
Ernest
9

Aqui está um liner curto e agradável se você precisar apenas suportar cadeias de consulta de chave / 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
fonte
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
fonte
3
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

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

Aqui está outra maneira. Para consultas simples.

Зелёный
fonte
2
você realmente deve ter certeza de que está usando o URI corretamente, escapando de suas chaves e valores. Mesmo para casos simples. Vai te morder.
Jrochkind 5/05
2

Sei que essa é uma pergunta antiga, mas eu só queria postar esse código, pois não consegui encontrar uma solução simples para executar essa tarefa por mim.

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

Enrolado como gema aqui: https://github.com/simen/queryparams

svale
fonte
1
URI.escape != CGI.escapee para o URL que você deseja o primeiro.
Ernest
2
Na verdade não, @ Ernest. Quando, por exemplo, incorporar outro URL como parâmetro ao seu URL (digamos que este seja o URL de retorno a ser redirecionado após o login), o URI.escape manterá o '?' e '&' do URL incorporado no local, quebrando o URL circundante, enquanto CGI.escape os guardará corretamente para mais tarde como% 3F e% 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 23/05
2

A melhor abordagem é usar Hash.to_params, que funciona bem com matrizes.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
fhidalgo
fonte
Sem Rails: require 'active_support/all'é necessário
Dorian
1

Se você estiver no contexto de uma solicitação do Faraday, também poderá passar o hash params como o segundo argumento e o faraday se encarrega de fazer parte do URL param apropriado:

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

Eu gosto de usar esta gema:

https://rubygems.org/gems/php_http_build_query

Uso da amostra:

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