Browse files

Introduces a parameter encoder option to connections.

Allows parameter encoding behavior to be overridden.
Fixes technoweenie/faraday#182.
Based on sqrrrl/faraday@dc8409d.
  • Loading branch information...
1 parent 263a35b commit ab2a85565808e33ed31e981feb5f197c6a37dda6 @sporkmonger sporkmonger committed Oct 15, 2012
View
11 lib/faraday/connection.rb
@@ -3,7 +3,7 @@
require 'forwardable'
require 'uri'
-Faraday.require_libs 'builder', 'request', 'response', 'utils'
+Faraday.require_libs 'builder', 'request', 'response', 'utils', 'parameters'
module Faraday
# Public: Connection objects manage the default properties and the middleware
@@ -74,6 +74,9 @@ def initialize(url = nil, options = {})
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options[:request] || {}
+ unless @options[:params_encoder]
+ @options[:params_encoder] = Faraday::NestedParamsEncoder
+ end
@ssl = options[:ssl] || {}
@parallel_manager = nil
@@ -436,9 +439,9 @@ def build_request(method)
def build_url(url, extra_params = nil)
uri = build_exclusive_url(url)
- query_values = self.params.dup.merge_query(uri.query)
+ query_values = self.params.dup.merge_query(uri.query, options[:params_encoder])
query_values.update extra_params if extra_params
- uri.query = query_values.empty? ? nil : query_values.to_query
+ uri.query = query_values.empty? ? nil : query_values.to_query(options[:params_encoder])
uri
end
@@ -458,7 +461,7 @@ def build_exclusive_url(url, params = nil)
base.path = base.path + '/' # ensure trailing slash
end
uri = url ? base + url : base
- uri.query = params.to_query if params
+ uri.query = params.to_query(options[:params_encoder]) if params
uri.query = nil if uri.query and uri.query.empty?
uri
end
View
193 lib/faraday/parameters.rb
@@ -0,0 +1,193 @@
+module Faraday
+ module NestedParamsEncoder
+ ESCAPE_RE = /[^\w .~-]+/
+
+ def self.escape(s)
+ return s.to_s.gsub(ESCAPE_RE) {
+ '%' + $&.unpack('H2' * $&.bytesize).join('%').upcase
+ }.tr(' ', '+')
+ end
+
+ def self.unescape(s)
+ CGI.unescape(s.to_s)
+ end
+
+ def self.encode(params)
+ return nil if params == nil
+
+ if !params.is_a?(Array)
+ if !params.respond_to?(:to_hash)
+ raise TypeError,
+ "Can't convert #{params.class} into Hash."
+ end
+ params = params.to_hash
+ params = params.map do |key, value|
+ key = key.to_s if key.kind_of?(Symbol)
+ [key, value]
+ end
+ # Useful default for OAuth and caching.
+ # Only to be used for non-Array inputs. Arrays should preserve order.
+ params.sort!
+ end
+
+ # Helper lambda
+ to_query = lambda do |parent, value|
+ if value.is_a?(Hash)
+ value = value.map do |key, val|
+ key = escape(key)
+ [key, val]
+ end
+ value.sort!
+ buffer = ""
+ value.each do |key, val|
+ new_parent = "#{parent}%5B#{key}%5D"
+ buffer << "#{to_query.call(new_parent, val)}&"
+ end
+ return buffer.chop
+ elsif value.is_a?(Array)
+ buffer = ""
+ value.each_with_index do |val, i|
+ new_parent = "#{parent}%5B%5D"
+ buffer << "#{to_query.call(new_parent, val)}&"
+ end
+ return buffer.chop
+ else
+ encoded_value = escape(value)
+ return "#{parent}=#{encoded_value}"
+ end
+ end
+
+ # The params have form [['key1', 'value1'], ['key2', 'value2']].
+ buffer = ''
+ params.each do |parent, value|
+ encoded_parent = escape(parent)
+ buffer << "#{to_query.call(encoded_parent, value)}&"
+ end
+ return buffer.chop
+ end
+
+ def self.decode(query)
+ return nil if query == nil
+ # Recursive helper lambda
+ dehash = lambda do |hash|
+ hash.each do |(key, value)|
+ if value.kind_of?(Hash)
+ hash[key] = dehash.call(value)
+ end
+ end
+ # Numeric keys implies an array
+ if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
+ hash.sort.inject([]) do |accu, (_, value)|
+ accu << value; accu
+ end
+ else
+ hash
+ end
+ end
+
+ empty_accumulator = {}
+ return ((query.split('&').map do |pair|
+ pair.split('=', 2) if pair && !pair.empty?
+ end).compact.inject(empty_accumulator.dup) do |accu, (key, value)|
+ key = unescape(key)
+ if value.kind_of?(String)
+ value = unescape(value.gsub(/\+/, ' '))
+ end
+
+ array_notation = !!(key =~ /\[\]$/)
+ subkeys = key.split(/[\[\]]+/)
+ current_hash = accu
+ for i in 0...(subkeys.size - 1)
+ subkey = subkeys[i]
+ current_hash[subkey] = {} unless current_hash[subkey]
+ current_hash = current_hash[subkey]
+ end
+ if array_notation
+ current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
+ current_hash[subkeys.last] << value
+ else
+ current_hash[subkeys.last] = value
+ end
+ accu
+ end).inject(empty_accumulator.dup) do |accu, (key, value)|
+ accu[key] = value.kind_of?(Hash) ? dehash.call(value) : value
+ accu
+ end
+ end
+ end
+
+ module FlatParamsEncoder
+ ESCAPE_RE = /[^\w .~-]+/
+
+ def self.escape(s)
+ return s.to_s.gsub(ESCAPE_RE) {
+ '%' + $&.unpack('H2' * $&.bytesize).join('%').upcase
+ }.tr(' ', '+')
+ end
+
+ def self.unescape(s)
+ CGI.unescape(s.to_s)
+ end
+
+ def self.encode(params)
+ return nil if params == nil
+
+ if !params.is_a?(Array)
+ if !params.respond_to?(:to_hash)
+ raise TypeError,
+ "Can't convert #{params.class} into Hash."
+ end
+ params = params.to_hash
+ params = params.map do |key, value|
+ key = key.to_s if key.kind_of?(Symbol)
+ [key, value]
+ end
+ # Useful default for OAuth and caching.
+ # Only to be used for non-Array inputs. Arrays should preserve order.
+ params.sort!
+ end
+
+ # The params have form [['key1', 'value1'], ['key2', 'value2']].
+ buffer = ''
+ params.each do |key, value|
+ encoded_key = escape(key)
+ value = value.to_s if value == true || value == false
+ if value == nil
+ buffer << "#{encoded_key}&"
+ elsif value.kind_of?(Array)
+ value.each do |sub_value|
+ encoded_value = escape(sub_value)
+ buffer << "#{encoded_key}=#{encoded_value}&"
+ end
+ else
+ encoded_value = escape(value)
+ buffer << "#{encoded_key}=#{encoded_value}&"
+ end
+ end
+ return buffer.chop
+ end
+
+ def self.decode(query)
+ empty_accumulator = {}
+ return nil if query == nil
+ split_query = (query.split('&').map do |pair|
+ pair.split('=', 2) if pair && !pair.empty?
+ end).compact
+ return split_query.inject(empty_accumulator.dup) do |accu, pair|
+ pair[0] = unescape(pair[0])
+ pair[1] = true if pair[1].nil?
+ if pair[1].respond_to?(:to_str)
+ pair[1] = unescape(pair[1].to_str.gsub(/\+/, " "))
+ end
+ if accu[pair[0]].kind_of?(Array)
+ accu[pair[0]] << pair[1]
+ elsif accu[pair[0]]
+ accu[pair[0]] = [accu[pair[0]], pair[1]]
+ else
+ accu[pair[0]] = pair[1]
+ end
+ accu
+ end
+ end
+ end
+end
View
3 lib/faraday/request/url_encoded.rb
@@ -9,7 +9,8 @@ class << self
def call(env)
match_content_type(env) do |data|
- env[:body] = Faraday::Utils.build_nested_query data
+ params = Faraday::Utils::ParamsHash[data]
+ env[:body] = params.to_query(env[:request][:params_encoder])
end
@app.call env
end
View
70 lib/faraday/utils.rb
@@ -1,5 +1,7 @@
require 'cgi'
+Faraday.require_libs 'parameters'
+
module Faraday
module Utils
extend self
@@ -124,15 +126,15 @@ def replace(other)
update(other)
end
- def merge_query(query)
+ def merge_query(query, encoder=NestedParamsEncoder)
if query && !query.empty?
- update Utils.parse_nested_query(query)
+ update encoder.decode(query)
end
self
end
- def to_query
- Utils.build_nested_query(self)
+ def to_query(encoder=NestedParamsEncoder)
+ encoder.encode(self)
end
private
@@ -142,32 +144,12 @@ def convert_key(key)
end
end
- # Copied from Rack
def build_query(params)
- params.map { |k, v|
- if v.class == Array
- build_query(v.map { |x| [k, x] })
- else
- v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
- end
- }.join("&")
+ FlatParamsEncoder.encode(params)
end
- # Rack's version modified to handle non-String values
- def build_nested_query(value, prefix = nil)
- case value
- when Array
- value.map { |v| build_nested_query(v, "#{prefix}%5B%5D") }.join("&")
- when Hash
- value.map { |k, v|
- build_nested_query(v, prefix ? "#{prefix}%5B#{escape(k)}%5D" : escape(k))
- }.join("&")
- when NilClass
- prefix
- else
- raise ArgumentError, "value must be a Hash" if prefix.nil?
- "#{prefix}=#{escape(value)}"
- end
+ def build_nested_query(params)
+ NestedParamsEncoder.encode(params)
end
ESCAPE_RE = /[^\w .~-]+/
@@ -183,31 +165,12 @@ def unescape(s) CGI.unescape s.to_s end
DEFAULT_SEP = /[&;] */n
# Adapted from Rack
- def parse_query(qs)
- params = {}
-
- (qs || '').split(DEFAULT_SEP).each do |p|
- k, v = p.split('=', 2).map { |x| unescape(x) }
-
- if cur = params[k]
- if cur.class == Array then params[k] << v
- else params[k] = [cur, v]
- end
- else
- params[k] = v
- end
- end
- params
+ def parse_query(query)
+ FlatParamsEncoder.decode(query)
end
- def parse_nested_query(qs)
- params = {}
-
- (qs || '').split(DEFAULT_SEP).each do |p|
- k, v = p.split('=', 2).map { |s| unescape(s) }
- normalize_params(params, k, v)
- end
- params
+ def parse_nested_query(query)
+ NestedParamsEncoder.decode(query)
end
# Stolen from Rack
@@ -219,7 +182,12 @@ def normalize_params(params, name, v = nil)
return if k.empty?
if after == ""
- params[k] = v
+ if params[k]
+ params[k] = Array[params[k]] unless params[k].kind_of?(Array)
+ params[k] << v
+ else
+ params[k] = v
+ end
elsif after == "[]"
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
View
13 test/connection_test.rb
@@ -191,6 +191,19 @@ def test_build_url_bracketizes_nested_params_in_query
assert_equal "a%5Bb%5D=c", uri.query
end
+ def test_build_url_bracketizes_repeated_params_in_query
+ conn = Faraday::Connection.new
+ uri = conn.build_url("http://sushi.com/sake.html", 'a' => [1, 2])
+ assert_equal "a%5B%5D=1&a%5B%5D=2", uri.query
+ end
+
+ def test_build_url_without_braketizing_repeated_params_in_query
+ conn = Faraday::Connection.new
+ conn.options[:params_encoder] = Faraday::FlatParamsEncoder
+ uri = conn.build_url("http://sushi.com/sake.html", 'a' => [1, 2])
+ assert_equal "a=1&a=2", uri.query
+ end
+
def test_build_url_parses_url
conn = Faraday::Connection.new
uri = conn.build_url("http://sushi.com/sake.html")
View
10 test/request_middleware_test.rb
@@ -48,6 +48,16 @@ def test_url_encoded_nested
assert_equal expected, Faraday::Utils.parse_nested_query(response.body)
end
+ def test_url_encoded_non_nested
+ response = @conn.post('/echo', { :dimensions => ['date', 'location']}) do |req|
+ req.options[:params_encoder] = Faraday::FlatParamsEncoder
+ end
+ assert_equal 'application/x-www-form-urlencoded', response.headers['Content-Type']
+ expected = { 'dimensions' => ['date', 'location'] }
+ assert_equal expected, Faraday::Utils.parse_query(response.body)
+ assert_equal 'dimensions=date&dimensions=location', response.body
+ end
+
def test_url_encoded_unicode
err = capture_warnings {
response = @conn.post('/echo', {:str => "eé cç aã aâ"})

0 comments on commit ab2a855

Please sign in to comment.