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
Хм, это на самом деле не Json, а Ruby Hash ... не уверен, что понимаю, почему здесь важна кодировка.
Julien Genestoux
Ответ от lmanners должен быть поддержан. Здесь есть много отличных ответов «сами себе» (многие с высокими баллами), но с тех пор ActiveSupport добавил стандартизированную поддержку для этого, что сделало разговор спорным. К сожалению, ответ Иманнера все еще скрыт в списке.
Ноах Магедман
2
@ Нет, на мой взгляд, любой ответ, в котором говорится, что нужно полагаться на библиотеку, в которой основные классы исправлений мартышки должны быть похоронены. Обоснование огромного количества этих патчей в лучшем случае шатко (взгляните на комментарии Иегуды Каца в этой статье ), что является отличным примером. YMMV, но для меня что-то с методом класса или которое не открывает Object и Hash, и где авторы не сказали бы «просто не конфликтуйте с нами!» было бы намного лучше.
13

Ответы:

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
К вашему сведению, это поведение было удалено из Addressable с 2.3 ( github.com/sporkmonger/addressable/commit/… )
oif_vet
2
@oif_vet Не могли бы вы сказать, какое поведение было удалено? Предложенный Бобом подход к использованию адресуемого драгоценного камня для решения проблемы оригинального плаката работает для меня как адресуемый-2.3.2.
Шелдон
1
@sheldonh, нет, @oif_vet правильно. Я убрал это поведение. Глубоко вложенные структуры больше не поддерживаются в Addressable как входные данные для 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
Я только что попробовал, и вы, кажется, правы. Возможно, мое утверждение изначально было связано с тем, как более ранняя версия rails анализировала строку запроса (я, кажется, вспомнил, что она перезаписывала предыдущие значения '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 # index как Параметры HTML: {"a" => "a", "b" => ["c", "d", "e"]}
Гейб Мартин-Демпси
что пойдет не так, если есть вложенные хэши? Почему я не могу использовать это, когда есть вложенные хэши? Для меня это просто URL-адрес избегает вложенного хэша, не должно быть проблем с использованием этого в запросе http.
Сэм
2
Без рельсов: require 'active_support/all'нужен
Дориан,
По крайней мере, в Rails 5.2 to_queryнеправильно обрабатываются значения nil. { a: nil, b: '1'}.to_query == "a=&b=1", но Rack и CGI оба разбираются a=как пустая строка, а не nil. Я не уверен насчет поддержки других серверов, но с рельсами правильная строка запроса должна быть a&b=1. Я думаю, что это неправильно, что Rails не может создать строку запроса, которая сама себя правильно анализирует ...
jsmartt
154

Если вы используете 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, это, безусловно, лучший. Это не зависит ни от каких источников, кроме самого Ruby.
Даниэль
+1 отлично работает с '{: a => "a",: b => {: c => "c",: d => true},: e => []}'
герцог
1
Кажется, не работает с ruby ​​2.0 - {:c => "c", :d => true}кажется, что хеш проверяется, поэтому передается в виде строки.
user208769 13.12.13
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"}]}
Iain
источник
Ваш вложенный пример демонстрирует, что он не работает должным образом - при запуске :bэто массив из двух хешей. В итоге :bполучается массив из одного большего хэша.
Эд Рудер
3
@EdRuder нет должным образом , потому что нет общепринятого стандарта. Что он показывает, так это то, что он намного ближе, чем кто-либо другой, судя по другим ответам.
января
1
Этот метод устарел с Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
davidgoli
8
@davidgoli Хм, не в Rack, это не github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140 . Если вы хотите использовать его в Rails, наверняка это так же просто, как require 'rack'? Это должно быть там, учитывая, что все основные веб-фреймворки Ruby теперь созданы поверх Rack.
января
@EdRuder ActiveSupport to_queryтакже объединяет 2 массива (v4.2).
Кельвин
9

Украсть у 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

См. Http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html.

AVDI
источник
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"
Hubro
источник
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

svale
источник
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"
fhidalgo
источник
Без рельсов: require 'active_support/all'нужен
Дориан,
1

Если вы находитесь в контексте запроса Фарадея, вы также можете просто передать хэш params в качестве второго аргумента, и Фарадей позаботится о том, чтобы сделать из него правильный URL-адрес param:

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"
mhorbul
источник