Skip to content

Commit

Permalink
Introduces a parameter encoder option to connections.
Browse files Browse the repository at this point in the history
Allows parameter encoding behavior to be overridden.
Fixes technoweenie/faraday#182.
Based on sqrrrl/faraday@dc8409d4d5.
  • Loading branch information
sporkmonger committed Oct 15, 2012
1 parent 263a35b commit ab2a855
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 56 deletions.
11 changes: 7 additions & 4 deletions lib/faraday/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
193 changes: 193 additions & 0 deletions lib/faraday/parameters.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/faraday/request/url_encoded.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 19 additions & 51 deletions lib/faraday/utils.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'cgi'

Faraday.require_libs 'parameters'

module Faraday
module Utils
extend self
Expand Down Expand Up @@ -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
Expand All @@ -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 .~-]+/
Expand All @@ -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
Expand All @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions test/connection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions test/request_middleware_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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â"})
Expand Down

0 comments on commit ab2a855

Please sign in to comment.