Ruby: Comment transformer un hachage en paramètres HTTP?


205

C'est assez facile avec un hachage simple comme

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

ce qui se traduirait par

"a=a&b=b"

Mais que faites-vous avec quelque chose de plus complexe comme

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

qui devrait se traduire par

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

Ou pire encore, (que faire) avec quelque chose comme:

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

Merci pour l'aide très appréciée!


Il semble que vous souhaitiez convertir JSON en paramètres HTTP ... peut-être avez-vous besoin d'un encodage différent?
CookieOfFortune

Hum, ce n'est en fait pas Json, mais un Ruby Hash ... je ne suis pas sûr de comprendre pourquoi l'encodage est important ici.
Julien Genestoux

La réponse des lmanners doit être encouragée. Il y a beaucoup de bonnes réponses roll-your-own ici (beaucoup avec des scores élevés) mais ActiveSupport a depuis ajouté un support standardisé pour cela, rendant la conversation théorique. Malheureusement, la réponse de lmanner est toujours enfouie dans la liste.
Noach Magedman

2
@Noach à mon avis, toute réponse qui dit de s'appuyer sur une bibliothèque qui corrige fortement les classes principales de singe devrait rester enterrée. La justification d'un grand nombre de ces correctifs est au mieux fragile (jetez un œil aux commentaires de Yehuda Katz dans cet article ), ceci étant un excellent exemple. YMMV, mais pour moi, quelque chose avec une méthode de classe ou qui n'ouvre pas Object et Hash, et où les auteurs ne diraient pas "ne vous heurtez pas!" serait beaucoup, beaucoup mieux.
iain

Réponses:


86

Mise à jour: cette fonctionnalité a été supprimée de la gemme.

Julien, votre réponse est bonne, et je l'ai empruntée sans vergogne, mais elle n'échappe pas correctement aux personnages réservés, et il y a quelques autres cas extrêmes où elle tombe en panne.

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

Le bijou est « adressable »

gem install addressable

1
THX! Quels sont les cas limites où ma solution se casse? donc je peux l'ajouter aux spécifications?
Julien Genestoux

2
Il ne gère pas les booléens, ce qui est clairement indésirable: {"a" => "a & b = b"}. To_params
Bob Aman

3
Pour info, malheureusement, ce comportement a été supprimé d'Adressable à partir du 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet

2
@oif_vet Pourriez-vous dire quel comportement a été supprimé? L'approche suggérée par Bob d'utiliser le joyau adressable pour résoudre le problème de l'affiche originale fonctionne pour moi à partir de addressable-2.3.2.
sheldonh

1
@sheldonh, non, @oif_vet est correct. J'ai supprimé ce comportement. Les structures profondément imbriquées ne sont plus prises en charge dans Addressable comme entrées pour le query_valuesmutateur.
Bob Aman

269

Pour les hachages de base non imbriqués, Rails / ActiveSupport l'a 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


1
Pourquoi dites-vous qu'il est cassé? la sortie que vous avez montrée est correcte, non?
Tokland

Je viens de l'essayer et vous semblez avoir raison. Peut-être que ma déclaration était à l'origine due à la façon dont une version antérieure de rails analysait la chaîne de requête (je semblais me souvenir qu'elle écrasait les valeurs «b» précédentes). A commencé "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" pour 127.0.0.1 au 2011-03-10 11:19:40 -0600 Traitement par SitesController # index as Paramètres HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Gabe Martin-Dempesy

qu'est-ce qui ne va pas s'il y a des hachages imbriqués? Pourquoi je ne peux pas l'utiliser lorsque des hachages sont imbriqués? Pour moi, il suffit que l'URL échappe au hachage imbriqué, cela ne devrait poser aucun problème lors de l'utilisation de la requête http.
Sam

2
Sans rails: require 'active_support/all'est nécessaire
Dorian

Au moins avec Rails 5.2 to_queryne gère pas correctement les valeurs nulles. { a: nil, b: '1'}.to_query == "a=&b=1", mais Rack et CGI analysent tous les deux a=comme une chaîne vide, non nil. Je ne suis pas sûr de la prise en charge d'autres serveurs, mais avec les rails, la chaîne de requête correcte devrait être a&b=1. Je pense qu'il est faux que Rails ne puisse pas produire une chaîne de requête correctement analysée par elle-même ...
jsmartt

154

Si vous utilisez Ruby 1.9.2 ou une version ultérieure, vous pouvez l'utiliser URI.encode_www_formsi vous n'avez pas besoin de tableaux.

Par exemple (à partir des documents 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"

Vous remarquerez que les valeurs de tableau ne sont pas définies avec des noms de clé contenant []comme nous sommes tous habitués dans les chaînes de requête. La spécification utilisée encode_www_formest conforme à la définition HTML5 des application/x-www-form-urlencodeddonnées.


8
+1, c'est de loin le meilleur. Cela ne dépend d'aucune source en dehors de Ruby lui-même.
Danyel

+1 fonctionne très bien avec '{: a => "a",: b => {: c => "c",: d => true},: e => []}' exemple
Duke

1
Ne semble pas fonctionner avec ruby ​​2.0 - le hachage {:c => "c", :d => true}semble être inspecté, donc envoyé via une chaîne.
user208769

1
C'était une section de l'extrait plus grand ci-dessus -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
Notez que cela a des résultats différents pour les valeurs de tableau que les deux Addressable::URIet ActiveSupport Object#to_query.
Matt Huggins

61

Pas besoin de charger l'ActiveSupport gonflé ou de rouler le vôtre, vous pouvez utiliser Rack::Utils.build_queryet Rack::Utils.build_nested_query. Voici un article de blog qui donne un bon exemple:

require 'rack'

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

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

Il gère même les tableaux:

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 les trucs imbriqués les plus difficiles:

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

Votre exemple imbriqué montre qu'il ne fonctionne pas correctement - lorsque vous démarrez, il :bs'agit d'un tableau de deux hachages. Vous vous retrouvez avec :bun tableau d'un hachage plus grand.
Ed Ruder

3
@EdRuder il n'y a pas correctement car il n'y a pas de norme acceptée. Ce que cela montre, c'est que c'est beaucoup plus proche que la tentative de quiconque, à en juger par les autres réponses.
iain

1
Cette méthode est déconseillée depuis Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli

8
@davidgoli Erm, pas dans Rack, ce n'est pas github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Si vous voulez l'utiliser dans Rails, c'est sûrement aussi simple que require 'rack'? Il doit être là, étant donné que tous les principaux frameworks Web Ruby sont désormais construits sur Rack.
iain

@EdRuder ActiveSupport to_queryfusionne également les 2 baies (v4.2).
Kelvin

9

Voler à 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

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


1
Malheureusement, cela ne fonctionne pas quand nous avons un tableau imbriqué à l'intérieur des paramètres (voir l'exemple # 2) ... :(
Julien Genestoux

2
Et ne fait aucun roi de s'échapper.
Ernest

9

Voici une ligne courte et douce si vous ne devez prendre en charge que des chaînes de requête de clé / valeur 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"

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

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

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

Voici une autre façon. Pour des requêtes simples.


2
vous devez vraiment vous assurer que vous échappez bien vos URI et vos clés. Même pour les cas simples. Ça va vous mordre.
jrochkind

2

Je sais que c'est une vieille question, mais je voulais juste publier ce morceau de code car je ne pouvais pas trouver un simple bijou pour faire juste cette tâche pour moi.

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

Enroulé comme un joyau ici: https://github.com/simen/queryparams


1
URI.escape != CGI.escapeet pour l'URL, vous voulez le premier.
Ernest

2
En fait non, @Ernest. Par exemple, en incorporant une autre URL en tant que paramètre à votre URL (disons que c'est l'URL de retour vers laquelle être redirigé après la connexion) URI.escape gardera le «?» et '&' de l'URL incorporée en place brisant l'URL environnante, tandis que CGI.escape les replacera correctement pour plus tard en tant que% 3F et% 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

La meilleure approche consiste à utiliser Hash.to_params, qui fonctionne correctement avec les tableaux.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"

Sans rails: require 'active_support/all'est nécessaire
Dorian

1

Si vous êtes dans le contexte d'une demande Faraday, vous pouvez également passer le hachage params comme deuxième argument et faraday se charge de faire de l'URL param appropriée une partie de celui-ci:

faraday_instance.get(url, params_hsh)

0

J'aime utiliser ce bijou:

https://rubygems.org/gems/php_http_build_query

Exemple d'utilisation:

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

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"
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.