Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fix for #337 with refactoring and more tests

  • Loading branch information...
commit 963abd8fbb2880f8eb27294b297de372cc7fc767 1 parent bf5ea1d
@simonoff simonoff authored
View
5 .gitignore
@@ -13,6 +13,9 @@ tmtags
## VIM
.*.sw[a-z]
+## RubyMine and related
+.idea
+
## PROJECT::GENERAL
coverage
rdoc
@@ -27,4 +30,4 @@ tmp/*
*.rbc
*.rbx
.ruby-gemset
-.ruby-version
+.ruby-version
View
359 lib/webmock/util/query_mapper.rb
@@ -1,142 +1,219 @@
module WebMock::Util
class QueryMapper
- #This class is based on Addressable::URI pre 2.3.0
-
- ##
- # Converts the query component to a Hash value.
- #
- # @option [Symbol] notation
- # May be one of <code>:flat</code>, <code>:dot</code>, or
- # <code>:subscript</code>. The <code>:dot</code> notation is not
- # supported for assignment. Default value is <code>:subscript</code>.
- #
- # @return [Hash, Array] The query string parsed as a Hash or Array object.
- #
- # @example
- # WebMock::Util::QueryMapper.query_to_values("?one=1&two=2&three=3")
- # #=> {"one" => "1", "two" => "2", "three" => "3"}
- # WebMock::Util::QueryMapper("?one[two][three]=four").query_values
- # #=> {"one" => {"two" => {"three" => "four"}}}
- # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
- # :notation => :dot
- # )
- # #=> {"one" => {"two" => {"three" => "four"}}}
- # WebMock::Util::QueryMapper.query_to_values("?one[two][three]=four",
- # :notation => :flat
- # )
- # #=> {"one[two][three]" => "four"}
- # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
- # :notation => :flat
- # )
- # #=> {"one.two.three" => "four"}
- # WebMock::Util::QueryMapper(
- # "?one[two][three][]=four&one[two][three][]=five"
- # )
- # #=> {"one" => {"two" => {"three" => ["four", "five"]}}}
- # WebMock::Util::QueryMapper.query_to_values(
- # "?one=two&one=three").query_values(:notation => :flat_array)
- # #=> [['one', 'two'], ['one', 'three']]
- def self.query_to_values(query, options={})
- query.force_encoding('utf-8') if query.respond_to?(:force_encoding)
- defaults = {:notation => :subscript}
- options = defaults.merge(options)
- if ![:flat, :dot, :subscript, :flat_array].include?(options[:notation])
- raise ArgumentError,
- "Invalid notation. Must be one of: " +
- "[:flat, :dot, :subscript, :flat_array]."
+ class << self
+ #This class is based on Addressable::URI pre 2.3.0
+
+ ##
+ # Converts the query component to a Hash value.
+ #
+ # @option [Symbol] notation
+ # May be one of <code>:flat</code>, <code>:dot</code>, or
+ # <code>:subscript</code>. The <code>:dot</code> notation is not
+ # supported for assignment. Default value is <code>:subscript</code>.
+ #
+ # @return [Hash, Array] The query string parsed as a Hash or Array object.
+ #
+ # @example
+ # WebMock::Util::QueryMapper.query_to_values("?one=1&two=2&three=3")
+ # #=> {"one" => "1", "two" => "2", "three" => "3"}
+ # WebMock::Util::QueryMapper("?one[two][three]=four").query_values
+ # #=> {"one" => {"two" => {"three" => "four"}}}
+ # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
+ # :notation => :dot
+ # )
+ # #=> {"one" => {"two" => {"three" => "four"}}}
+ # WebMock::Util::QueryMapper.query_to_values("?one[two][three]=four",
+ # :notation => :flat
+ # )
+ # #=> {"one[two][three]" => "four"}
+ # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
+ # :notation => :flat
+ # )
+ # #=> {"one.two.three" => "four"}
+ # WebMock::Util::QueryMapper(
+ # "?one[two][three][]=four&one[two][three][]=five"
+ # )
+ # #=> {"one" => {"two" => {"three" => ["four", "five"]}}}
+ # WebMock::Util::QueryMapper.query_to_values(
+ # "?one=two&one=three").query_values(:notation => :flat_array)
+ # #=> [['one', 'two'], ['one', 'three']]
+ def query_to_values(query, options={})
+ return nil if query.nil?
+ query.force_encoding('utf-8') if query.respond_to?(:force_encoding)
+
+ options = {:notation => :subscript}.merge(options)
+
+ unless %w(flat dot subscript flat_array).include?(options[:notation].to_s)
+ raise ArgumentError,
+ 'Invalid notation. Must be one of: ' +
+ '[:flat, :dot, :subscript, :flat_array].'
+ end
+
+ empty_accumulator = :flat_array == options[:notation] ? [] : {}
+
+ query_array = collect_query_parts(query)
+
+ query_hash = collect_query_hash(query_array, empty_accumulator, options)
+
+ normalize_query_hash(query_hash, empty_accumulator, options)
end
- dehash = lambda do |hash|
- hash.each do |(key, value)|
- if value.kind_of?(Hash)
- hash[key] = dehash.call(value)
+
+ def normalize_query_hash(query_hash, empty_accumulator, options)
+ query_hash.inject(empty_accumulator.dup) do |accumulator, (key, value)|
+ if options[:notation] == :flat_array
+ accumulator << [key, value]
+ else
+ accumulator[key] = value.kind_of?(Hash) ? dehash(value) : value
end
+ accumulator
end
- if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
- hash.sort.inject([]) do |accu, (_, value)|
- accu << value; accu
- end
- else
- hash
+ end
+
+ def collect_query_parts(query)
+ query_parts = query.split('&').map do |pair|
+ pair.split('=', 2) if pair && !pair.empty?
end
+ query_parts.compact
end
- return nil if query == nil
- empty_accumulator = :flat_array == options[:notation] ? [] : {}
- return ((query.split("&").map do |pair|
- pair.split("=", 2) if pair && !pair.empty?
- end).compact.inject(empty_accumulator.dup) do |accumulator, (key, value)|
- value = true if value.nil?
- key = Addressable::URI.unencode_component(key)
- key = key.dup.force_encoding(Encoding::ASCII_8BIT) if key.respond_to?(:force_encoding)
- if value != true
- value = Addressable::URI.unencode_component(value.gsub(/\+/, " "))
- end
- if options[:notation] == :flat
- if accumulator[key]
- raise ArgumentError, "Key was repeated: #{key.inspect}"
- end
- accumulator[key] = value
- elsif options[:notation] == :flat_array
- accumulator << [key, value]
- else
- if options[:notation] == :dot
- array_value = false
- subkeys = key.split(".")
- elsif options[:notation] == :subscript
- array_value = !!(key =~ /\[\]$/)
- subkeys = key.split(/[\[\]]+/)
- end
- current_hash = accumulator
- 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_value
- if current_hash[subkeys.last] && !current_hash[subkeys.last].is_a?(Array)
- current_hash[subkeys.last] = [current_hash[subkeys.last]]
- end
- current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
- current_hash[subkeys.last] << value
+
+ def collect_query_hash(query_array, empty_accumulator, options)
+ query_array.compact.inject(empty_accumulator.dup) do |accumulator, (key, value)|
+ value = if value.nil?
+ true
else
- current_hash[subkeys.last] = value
+ ::Addressable::URI.unencode_component(value.gsub(/\+/, ' '))
end
- end
- accumulator
- end).inject(empty_accumulator.dup) do |accumulator, (key, value)|
- if options[:notation] == :flat_array
- accumulator << [key, value]
+ key = Addressable::URI.unencode_component(key)
+ key = key.dup.force_encoding(Encoding::ASCII_8BIT) if key.respond_to?(:force_encoding)
+ self.__send__("fill_accumulator_for_#{options[:notation]}", accumulator, key, value)
+ accumulator
+ end
+ end
+
+ def fill_accumulator_for_flat(accumulator, key, value)
+ if accumulator[key]
+ raise ArgumentError, "Key was repeated: #{key.inspect}"
+ end
+ accumulator[key] = value
+ end
+
+ def fill_accumulator_for_flat_array(accumulator, key, value)
+ accumulator << [key, value]
+ end
+
+ def fill_accumulator_for_dot(accumulator, key, value)
+ array_value = false
+ subkeys = key.split(".")
+ current_hash = accumulator
+ subkeys[0..-2].each do |subkey|
+ current_hash[subkey] = {} unless current_hash[subkey]
+ current_hash = current_hash[subkey]
+ end
+ if array_value
+ if current_hash[subkeys.last] && !current_hash[subkeys.last].is_a?(Array)
+ current_hash[subkeys.last] = [current_hash[subkeys.last]]
+ end
+ current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
+ current_hash[subkeys.last] << value
else
- accumulator[key] = value.kind_of?(Hash) ? dehash.call(value) : value
+ current_hash[subkeys.last] = value
end
- accumulator
end
- end
- ##
- # Sets the query component for this URI from a Hash object.
- # This method produces a query string using the :subscript notation.
- # An empty Hash will result in a nil query.
- #
- # @param [Hash, #to_hash, Array] new_query_values The new query values.
- def self.values_to_query(new_query_values)
+ def fill_accumulator_for_subscript(accumulator, key, value)
+ current_node = accumulator
+ subkeys = key.split(/(?=\[\w)/)
+ subkeys[0..-2].each do |subkey|
+ node = subkey =~ /\[\]\z/ ? [] : {}
+ subkey = subkey.gsub(/[\[\]]/, '')
+ if current_node.is_a? Array
+ container = current_node.find { |n| n.is_a?(Hash) && n.has_key?(subkey) }
+ if container
+ current_node = container[subkey]
+ else
+ current_node << {subkey => node}
+ current_node = node
+ end
+ else
+ current_node[subkey] = node unless current_node[subkey]
+ current_node = current_node[subkey]
+ end
+ end
+ last_key = subkeys.last
+ array_value = !!(last_key =~ /\[\]$/)
+ last_key = last_key.gsub(/[\[\]]/, '')
+ if current_node.is_a? Array
+ container = current_node.find { |n| n.is_a?(Hash) && n.has_key?(last_key) }
+ if container
+ if array_value
+ container[last_key] << value
+ else
+ container[last_key] = value
+ end
+ else
+ if array_value
+ current_node << {last_key => [value]}
+ else
+ current_node << {last_key => value}
+ end
+ end
+ else
+ if array_value
+ current_node[last_key] = [] unless current_node[last_key]
+ current_node[last_key] << value
+ else
+ current_node[last_key] = value
+ end
+ end
+ end
+
+ ##
+ # Sets the query component for this URI from a Hash object.
+ # This method produces a query string using the :subscript notation.
+ # An empty Hash will result in a nil query.
+ #
+ # @param [Hash, #to_hash, Array] new_query_values The new query values.
+ def values_to_query(new_query_values)
+ return if new_query_values.nil?
+
+ unless new_query_values.is_a?(Array)
+ unless new_query_values.respond_to?(:to_hash)
+ raise TypeError,
+ "Can't convert #{new_query_values.class} into Hash."
+ end
+ new_query_values = new_query_values.to_hash
+ new_query_values = new_query_values.map do |key, value|
+ key = key.to_s if key.is_a?(::Symbol) || key.nil?
+ [key.to_s, value]
+ end
+ # Useful default for OAuth and caching.
+ # Only to be used for non-Array inputs. Arrays should preserve order.
+ new_query_values.sort!
+ end
- if new_query_values == nil
- return nil
+ buffer = ''
+ new_query_values.each do |parent, value|
+ encoded_parent = ::Addressable::URI.encode_component(
+ parent.dup, ::Addressable::URI::CharacterClasses::UNRESERVED
+ )
+ buffer << "#{to_query(encoded_parent, value)}&"
+ end
+ buffer.chop
end
- if !new_query_values.is_a?(Array)
- if !new_query_values.respond_to?(:to_hash)
- raise TypeError,
- "Can't convert #{new_query_values.class} into Hash."
+ def dehash(hash)
+ hash.each do |(key, value)|
+ if value.is_a?(::Hash)
+ hash[key] = self.dehash(value)
+ end
end
- new_query_values = new_query_values.to_hash
- new_query_values = new_query_values.map do |key, value|
- key = key.to_s if key.kind_of?(Symbol) || key.nil?
- [key.to_s, value]
+ if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
+ hash.sort.inject([]) do |accu, (_, value)|
+ accu << value; accu
+ end
+ else
+ hash
end
- # Useful default for OAuth and caching.
- # Only to be used for non-Array inputs. Arrays should preserve order.
- new_query_values.sort!
end
##
@@ -148,47 +225,41 @@ def self.values_to_query(new_query_values)
# @param [Array, Hash, Symbol, #to_str] value
#
# @return [String] a properly escaped and ordered URL query.
- to_query = lambda do |parent, value|
- if value.is_a?(Hash)
+
+ # new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
+ def to_query(parent, value)
+ case value
+ when ::Hash
value = value.map do |key, val|
[
- Addressable::URI.encode_component(key.dup, Addressable::URI::CharacterClasses::UNRESERVED),
+ ::Addressable::URI.encode_component(key.dup, ::Addressable::URI::CharacterClasses::UNRESERVED),
val
]
end
value.sort!
- buffer = ""
+ buffer = ''
value.each do |key, val|
new_parent = "#{parent}[#{key}]"
- buffer << "#{to_query.call(new_parent, val)}&"
+ buffer << "#{to_query(new_parent, val)}&"
end
- return buffer.chop
- elsif value.is_a?(Array)
- buffer = ""
+ buffer.chop
+ when ::Array
+ buffer = ''
value.each_with_index do |val, i|
new_parent = "#{parent}[#{i}]"
- buffer << "#{to_query.call(new_parent, val)}&"
+ buffer << "#{to_query(new_parent, val)}&"
end
- return buffer.chop
- elsif value == true
- return parent
+ buffer.chop
+ when TrueClass
+ parent
else
encoded_value = Addressable::URI.encode_component(
value.to_s.dup, Addressable::URI::CharacterClasses::UNRESERVED
)
- return "#{parent}=#{encoded_value}"
+ "#{parent}=#{encoded_value}"
end
end
-
- # new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
- buffer = ""
- new_query_values.each do |parent, value|
- encoded_parent = Addressable::URI.encode_component(
- parent.dup, Addressable::URI::CharacterClasses::UNRESERVED
- )
- buffer << "#{to_query.call(encoded_parent, value)}&"
- end
- return buffer.chop
end
+
end
end
View
45 spec/unit/util/query_mapper_spec.rb
@@ -1,30 +1,41 @@
require 'spec_helper'
describe WebMock::Util::QueryMapper do
- let(:query_mapper) { described_class }
+ it "should parse hash queries" do
+ # {"one" => {"two" => {"three" => ["four", "five"]}}}
+ query = "one%5Btwo%5D%5Bthree%5D%5B%5D=four&one%5Btwo%5D%5Bthree%5D%5B%5D=five"
+ hsh = WebMock::Util::QueryMapper.query_to_values(query)
+ hsh["one"]["two"]["three"].should == ["four", "five"]
+ end
- it "converts query to values" do
- query = "key=value&other_key=other_value"
- values = { 'key' => 'value', 'other_key' => 'other_value' }
- expect(query_mapper.query_to_values query).to eq values
+ it "should parse one nil value queries" do
+ # {'a' => nil, 'b' => 'c'}
+ query = "a=&b=c"
+ hsh = WebMock::Util::QueryMapper.query_to_values(query)
+ hsh['a'].should be_empty
+ hsh['b'].should == 'c'
end
- it 'converts values to a query string' do
- query = "key=value&other_key=other_value"
- values = [['key','value'],['other_key','other_value']]
- expect(query_mapper.values_to_query values).to eq query
+ it "should parse array queries" do
+ # {"one" => ["foo", "bar"]}
+ query = "one%5B%5D=foo&one%5B%5D=bar"
+ hsh = WebMock::Util::QueryMapper.query_to_values(query)
+ hsh["one"].should == ["foo", "bar"]
end
- it 'converts values with missing keys to a query string' do
- query = "=value"
- values = { '' => 'value' }
- expect(query_mapper.values_to_query values).to eq query
+ it "should parse string queries" do
+ # {"one" => "two", "three" => "four"}
+ query = "one=two&three=four"
+ hsh = WebMock::Util::QueryMapper.query_to_values(query)
+ hsh.should == {"one" => "two", "three" => "four"}
end
- it 'converts values with nil keys to a query string' do
- query = "=value"
- values = { nil => 'value' }
- expect(query_mapper.values_to_query values).to eq query
+ it "should parse nested queries" do
+ # [{"b"=>[{"c"=>[{"d"=>["1", {"e"=>"2"}]}]}]}]
+ query = "a%5B%5D%5Bb%5D%5B%5D%5Bc%5D%5B%5D%5Bd%5D%5B%5D=1&a%5B%5D%5Bb%5D%5B%5D%5Bc%5D%5B%5D%5Bd%5D%5B%5D%5Be%5D=2"
+ hsh = WebMock::Util::QueryMapper.query_to_values(query)
+ hsh["a"][0]["b"][0]["c"][0]["d"][0].should == "1"
+ hsh["a"][0]["b"][0]["c"][0]["d"][1]["e"].should == "2"
end
end

0 comments on commit 963abd8

Please sign in to comment.
Something went wrong with that request. Please try again.