Ruby: Як перетворити хеш на параметри HTTP?


205

Це досить легко, як звичайний хеш

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

що перекладалося б на

"a=a&b=b"

Але що ви робите з чимось складнішим на кшталт

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

що має перекластись на

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

Або ще гірше, (що робити) з чимось на зразок:

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

Дякуємо за вдячну допомогу в цьому!


Здається, ви хочете перетворити JSON в параметри HTTP ... можливо, вам потрібно інше кодування?
CookieOfFortune

Хам, це насправді не Джсон, а Рубі Хеш ... не впевнений, я розумію, чому кодування тут має значення.
Жульєн Генесту

Відповідь lmanners слід просувати. Тут є багато чудових відповідей на власні відгуки (багато з високими показниками), але з тих пір ActiveSupport додає стандартизовану підтримку для цього, що робить дискусію суперечливою. На жаль, відповідь lmanner все ще заточений у списку.
Ноах Магедман

2
@Noach, на мою думку, будь-яка відповідь, яка говорить про те, щоб покластися на бібліотеку, що основні класи мавпових патчів, повинні залишатися похованими. Виправдання для величезної кількості цих патчів у кращому випадку хитке (подивіться коментарі Єгуди Кац у цій статті ), це є чудовим прикладом. YMMV, але для мене щось із класовим методом або тим, що не відкриває Object and Hash, і де автори не скажуть "просто не зіткнись з нами!" було б набагато, набагато краще.
Іен

Відповіді:


86

Оновлення: цю функціональність було видалено з дорогоцінного каміння.

Жюльєн, твоя самовідповідь хороша, і я безсоромно запозичив у неї, але це не належним чином уникне зарезервованих персонажів, і є кілька інших крайових випадків, коли вона руйнується.

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

Дорогоцінний камінь ' адресований '

gem install addressable

1
Дякую! Які кращі випадки, коли мій розчин порушується? тож я можу додати його до специфікацій?
Жульєн Генесту

2
Він не обробляє булеві, і це явно небажано: {"a" => "a & b = b"}. To_params
Боб Аман

3
FYI, на жаль, цю поведінку було видалено з адреси Address 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet

2
@oif_vet Ви можете сказати, яку поведінку було видалено? Запропонований Боб програмою використовувати адресний дорогоцінний камінь для вирішення проблеми оригінального плаката для мене працює як адреса-2.3.2.
sheldonh

1
@sheldonh, ні, @oif_vet правильний. Я зняв таку поведінку. Глибоко вкладені структури більше не підтримуються в Адресуванні як входи до query_valuesмутатора.
Боб Аман

269

Для основних, не вкладених хешів, 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


1
Чому ти кажеш, що це зламано? вихід, який ви показали, добре, чи не так?
tokland

Я просто спробував це, і ти, здається, маєш рацію. Можливо, моє твердження спочатку було пов’язане з тим, як попередня версія рейків проаналізувала рядок запиту (мені здалося, я пам'ятаю, що він переписував попередні значення "b"). Почав GET "/? A = a & b% 5B% 5D = c & b% 5B% 5D = d & b% 5B% 5D = e" за 127.0.0.1 в 2011-03-10 11:19:40 -0600 Обробка індексом SitesController # як Параметри HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Гейб Мартін-Демпесі

що піде не так, якщо є вкладені хеші? Чому я не можу використовувати це, коли є вкладені хеші? Для мене просто URL-адреса уникає вкладеного хешу, не повинно виникнути проблем із використанням цього в http-запиті.
Сем

2
Без рейок: require 'active_support/all'потрібен
Доріан

Принаймні, з Rails 5.2 to_queryнеправильно обробляє значення нуля. { a: nil, b: '1'}.to_query == "a=&b=1", але Rack і CGI обирають a=як порожній рядок, не так nil. Я не впевнений у підтримці інших серверів, але з рейками повинна бути правильна рядок запиту a&b=1. Я думаю, що це неправильно, що Rails не може створити рядок запиту, який правильно розбирається сам по собі ...
jsmartt

153

Якщо ви використовуєте Ruby 1.9.2 або новішої версії, ви можете використовувати, URI.encode_www_formякщо вам не потрібні масиви.

Напр. (З документів Ruby в 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"

Ви помітите, що значення масиву не задаються іменами ключів, що містять, []як ми всі звикли в рядках запитів. Специфікація, яка encode_www_formвикористовується, відповідає визначенню application/x-www-form-urlencodedданих HTML5 .


8
+1, це далеко не найкраще. Це не залежить від будь-яких джерел поза межами самого Рубі.
Даніель

+1 добре працює з прикладом '{: a => "a",: b => {: c => "c",: d => true},: e => []}' Приклад
герцог

1
Здається, не працює з ruby ​​2.0 - хеш, {:c => "c", :d => true}здається, перевіряється, тому надсилається через рядок.
користувач208769

1
Це був розділ більшого фрагмента вище -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
Зауважте, що це має різні результати для значень масиву, ніж обидва Addressable::URIта ActiveSupport Object#to_query.
Мет Хаггінс

61

Не потрібно завантажувати роздутий ActiveSupport або прокручувати свій власний, ви можете використовувати Rack::Utils.build_queryі Rack::Utils.build_nested_query. Ось допис у блозі, який дає хороший приклад:

require 'rack'

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

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

Він навіть обробляє масиви:

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

Або більш складні вкладені речі:

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

Ваш вкладений приклад демонструє, що він не працює належним чином - коли ви починаєте, :bце масив з двох хешів. Ви нарешті :bє масивом одного більшого хеша.
Ед Рудер

3
@EdRuder немає належним чином, оскільки немає прийнятого стандарту. Це показує, що це набагато ближче, ніж будь-хто інший, судячи з інших відповідей.
1313

1
Цей метод застарілий з моменту Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli

8
@davidgoli Erm, не в Rack це не github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Якщо ви хочете використовувати його в Rails, напевно це так просто, як require 'rack'? Він повинен бути там, враховуючи, що всі основні веб-рамки Ruby побудовані поверх Rack зараз.
1313

@EdRuder ActiveSupport to_queryтакож об'єднує 2 масиви (v4.2).
Келвін

9

Викрасти з Мерб:

# 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

Дивіться http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html


1
На жаль, це не спрацьовує, коли в параметрі є вкладений масив (див. Приклад №2) ... :(
Julien Genestoux

2
І не робить жоден король втечі.
Ернест

9

Ось короткий і солодкий один вкладиш, якщо вам потрібно лише підтримувати прості рядки запитів ключа / значення 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"

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"

Ось ще один спосіб. Для простих запитів.


2
ви дійсно повинні переконатися, що ви правильно URI-уникнути своїх ключів та значень. Навіть для простих випадків. Це вас вкусить.
jrochkind

2

Я знаю, що це давнє запитання, але я просто хотів опублікувати цей біт коду, оскільки я не міг знайти простий дорогоцінний камінь, щоб виконати саме це завдання для мене.

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

Згорнутий як дорогоцінний камінь тут: https://github.com/simen/queryparams


1
URI.escape != CGI.escapeа для URL-адреси ви хочете перший.
Ернест

2
Насправді ні, @Ernest. Якщо, наприклад, вбудований інший URL як параметр вашої URL-адреси (скажімо, це повернення URL-адреса, на який слід перенаправитись після входу), URI.escape буде зберігати "?" та "&" вбудованого URL-адреса на місце, що порушує навколишній URL-адрес, тоді як CGI.escape буде правильно їх відтягувати на потім як% 3F та% 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"
свале

2

Найкращим підходом є використання Hash.to_params, який є найкращим для роботи з масивами.

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

Без рейок: require 'active_support/all'потрібен
Доріан

1

Якщо ви знаходитесь в контексті запиту Faraday, ви також можете просто передати хеш парам, як другий аргумент, і faraday піклується про створення належної частини URL-адреси парами:

faraday_instance.get(url, params_hsh)

0

Мені подобається використовувати цей самоцвіт:

https://rubygems.org/gems/php_http_build_query

Використання зразка:

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"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.