Ruby: Jak zmienić skrót w parametry HTTP?

205

To jest całkiem proste z prostym hash

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

co przełożyłoby się na

"a=a&b=b"

Ale co robisz z czymś bardziej złożonym, takim jak

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

co powinno się przełożyć na

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

Lub jeszcze gorzej (co zrobić) z czymś takim jak:

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

Dzięki za bardzo docenianą pomoc!

Julien Genestoux
źródło
Wygląda na to, że chcesz przekonwertować JSON na parametry HTTP ... może potrzebujesz innego kodowania?
CookieOfFortune
Hum, to właściwie nie jest Json, ale Ruby Hash ... nie jestem pewien, czy rozumiem, dlaczego kodowanie ma tutaj znaczenie.
Julien Genestoux
Odpowiedź lmannerów powinna być promowana. Istnieje tutaj wiele świetnych odpowiedzi typu „zrób to sam” (wiele z wysokimi wynikami), ale ActiveSupport od tego czasu dodał do tego znormalizowane wsparcie, czyniąc dyskusję dyskusyjną. Niestety, odpowiedź Lmannera jest wciąż schowana na liście.
Noach Magedman,
2
@Noach, moim zdaniem, każda odpowiedź, która mówi, aby polegać na bibliotece, w której mocno małpowane łaty podstawowe klasy powinny pozostać pochowane. Uzasadnienie ogromnej liczby tych łat jest w najlepszym razie niepewne (spójrz na komentarze Yehudy Katza w tym artykule ), co jest doskonałym przykładem. YMMV, ale dla mnie coś z metodą klasową lub która nie otwiera Object i Hash, i gdzie autorzy nie powiedzieliby „po prostu nie koliduj z nami!” byłoby znacznie, znacznie lepiej.
iain

Odpowiedzi:

86

Aktualizacja: Ta funkcjonalność została usunięta z klejnotu.

Julien, twoja własna odpowiedź jest dobra, a ja bezwstydnie pożyczyłem od niej, ale nie ucieka właściwie zarezerwowanym postaciom, i jest kilka innych skrajnych przypadków, w których się psuje.

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

Klejnot jest „ adresowalny

gem install addressable
Bob Aman
źródło
1
Dzięki! Jakie są skrajne przypadki, w których moje rozwiązanie psuje się? więc mogę dodać to do specyfikacji?
Julien Genestoux
2
Nie obsługuje boolanów i jest to zdecydowanie niepożądane: {"a" => "a & b = b"}. To_params
Bob Aman
3
Do Twojej wiadomości, niestety to zachowanie zostało usunięte z Adresowalnych od 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet,
2
@oif_vet Czy możesz powiedzieć, jakie zachowanie zostało usunięte? Sugerowana przez Boba metoda użycia klejnotu adresowalnego do rozwiązania problemu z oryginalnym plakatem działa dla mnie od adresowalnego-2.3.2.
sheldonh
1
@ sheldonh, nie, @oif_vet jest poprawny. Usunąłem to zachowanie. Głęboko zagnieżdżone struktury nie są już obsługiwane w adresowalnych jako dane wejściowe do query_valuesmutatora.
Bob Aman
269

W przypadku podstawowych, nie zagnieżdżonych skrótów, Rails / ActiveSupport ma 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
źródło
1
Dlaczego mówisz, że jest zepsuty? wynik, który pokazałeś, jest w porządku, prawda?
tokland
Właśnie tego spróbowałem i wydaje się, że masz rację. Być może moje oświadczenie było pierwotnie spowodowane sposobem, w jaki wcześniejsza wersja szyn parsowała ciąg zapytania (wydawało mi się, że pamiętam, że zastępuje poprzednie wartości „b”). Rozpoczęto GET "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" dla 127.0.0.1 na 2011-03-10 11:19:40 -0600 Przetwarzanie przez SitesController # indeks jako Parametry HTML: {„a” ​​=> „a”, „b” => [„c”, „d”, „e”]}
Gabe Martin-Dempesy
co pójdzie nie tak, jeśli są zagnieżdżone skróty? Dlaczego nie mogę tego użyć, gdy są zagnieżdżone skróty? Dla mnie po prostu url ucieka z zagnieżdżonego skrótu, nie powinno być problemu z użyciem tego w żądaniu http.
Sam
2
Bez szyn: require 'active_support/all'jest potrzebny
Dorian,
Przynajmniej w Rails 5.2 to_querynie obsługuje poprawnie wartości zerowych. { a: nil, b: '1'}.to_query == "a=&b=1", ale zarówno Rack, jak i CGI parsują a=jako pusty ciąg, a nie nil. Nie jestem pewien co do obsługi innych serwerów, ale w przypadku szyn powinien być prawidłowy ciąg zapytania a&b=1. Myślę, że to źle, że Railsy nie mogą wygenerować ciągu zapytania, który sam jest poprawnie parsowany ...
jsmartt
153

Jeśli używasz Ruby 1.9.2 lub nowszego, możesz użyć URI.encode_www_form jeśli nie potrzebujesz tablic.

Np. (Z Ruby docs w 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"

Zauważysz, że wartości tablic nie są ustawione z nazwami kluczy zawierającymi, []jak wszyscy jesteśmy przyzwyczajeni w ciągach zapytań. Używana specyfikacja encode_www_formjest zgodna z definicją application/x-www-form-urlencodeddanych HTML5 .

Bo Jeanes
źródło
8
+1, to zdecydowanie najlepsze. Nie zależy od żadnych źródeł poza samym Ruby.
Danyel
+1 działa dobrze z przykładem „{: a =>" a ",: b => {: c =>" c ",: d => true},: e => []} 'przykład
Duke
1
{:c => "c", :d => true}Wygląda na to, że nie działa z Ruby 2.0 - skrót wydaje się być sprawdzany, więc jest przesyłany jako ciąg znaków.
user208769,
1
Była to część większego fragmentu powyżej -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&
użytkownik208769,
1
Zauważ, że ma to inne wyniki dla wartości tablic niż dla obu Addressable::URIi dla ActiveSupport Object#to_query.
Matt Huggins
61

Nie musisz ładować rozdętego ActiveSupport ani tworzyć własnych, możesz użyć Rack::Utils.build_queryi Rack::Utils.build_nested_query. Oto post na blogu, który stanowi dobry przykład:

require 'rack'

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

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

Obsługuje nawet tablice:

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

Lub trudniejsze zagnieżdżone rzeczy:

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
źródło
Twój zagnieżdżony przykład pokazuje, że nie działa poprawnie - kiedy zaczynasz, :bjest tablicą dwóch skrótów. W efekcie :bpowstaje tablica jednego większego skrótu.
Ed Ruder
3
@EdRuder nie ma poprawnie, ponieważ nie ma akceptowanego standardu. Pokazuje, że jest o wiele bliżej niż próba kogokolwiek innego, sądząc po innych odpowiedziach.
iain
1
Ta metoda jest przestarzała od czasu Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli
8
@davidgoli Erm, nie w Rack, to nie jest github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Jeśli chcesz go używać w Railsach, to z pewnością jest tak proste jak require 'rack'? Musi tam być, biorąc pod uwagę, że wszystkie główne frameworki Ruby są teraz zbudowane na Rack.
iain
@EdRuder ActiveSupport to_queryłączy także 2 tablice (v4.2).
Kelvin
9

Kradnij od Merba:

# 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

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

Avdi
źródło
1
Niestety, to deos nie działa, gdy mamy zagnieżdżoną macierz wewnątrz parametrów (patrz przykład # 2) ... :(
Julien Genestoux
2
I nie robi żadnego króla ucieczki.
Ernest,
9

Oto krótki i słodki jeden linijka, jeśli potrzebujesz obsługiwać tylko proste ciągi zapytań klucz / wartość ASCII:

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
źródło
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
źródło
3
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

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

Oto inny sposób. Dla prostych zapytań.

Зелёный
źródło
2
naprawdę powinieneś jednak upewnić się, że właściwie unikasz identyfikatora URI swoich kluczy i wartości. Nawet w prostych przypadkach. Ugryzie cię.
jrochkind
2

Wiem, że to stare pytanie, ale chciałem tylko napisać ten fragment kodu, ponieważ nie mogłem znaleźć prostego klejnotu, który mógłby wykonać to zadanie dla mnie.

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

Zwinięte jako klejnot tutaj: https://github.com/simen/queryparams

svale
źródło
1
URI.escape != CGI.escapei dla adresu URL chcesz pierwszy.
Ernest,
2
Właściwie nie, @Ernest. W przypadku np. Osadzenia innego adresu jako parametru do adresu URL (powiedzmy, że jest to adres zwrotny, na który należy przekierować po zalogowaniu), URI.escape zachowa „?” i „&” osadzonego adresu URL w miejscu, niszcząc otaczający adres URL, podczas gdy CGI.escape poprawnie je ukryje na później jako% 3F i% 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

Najlepszym rozwiązaniem jest użycie Hash.to_params, który działa dobrze z tablicami.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
fhidalgo
źródło
Bez szyn: require 'active_support/all'jest potrzebny
Dorian,
1

Jeśli jesteś w kontekście żądania Faradaya, możesz również przekazać skrót params jako drugi argument i faraday zajmuje się tworzeniem z niego odpowiedniego adresu URL param:

faraday_instance.get(url, params_hsh)
Yo Ludke
źródło
0

Lubię używać tego klejnotu:

https://rubygems.org/gems/php_http_build_query

Przykładowe użycie:

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
Jan
źródło
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
źródło