diff --git a/.gitignore b/.gitignore index a9a4404..47389ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ tmtags ## VIM *.swp +## RubyMine +/.idea/ + ## PROJECT::GENERAL coverage rdoc diff --git a/lib/signature.rb b/lib/signature.rb index 19d16da..12c547b 100644 --- a/lib/signature.rb +++ b/lib/signature.rb @@ -180,15 +180,11 @@ def string_to_sign def parameter_string param_hash = @query_hash.merge(@auth_hash || {}) - # Convert keys to lowercase strings - hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v } - # Exclude signature from signature generation! - hash.delete("auth_signature") + param_hash.delete(:auth_signature) + param_hash.delete("auth_signature") - hash.sort.map do |k, v| - QueryEncoder.encode_param_without_escaping(k, v) - end.join('&') + QueryEncoder.encode_params_without_escaping(param_hash) end def validate_version! diff --git a/lib/signature/query_encoder.rb b/lib/signature/query_encoder.rb index ad1802b..64552d6 100644 --- a/lib/signature/query_encoder.rb +++ b/lib/signature/query_encoder.rb @@ -2,26 +2,50 @@ module Signature # Query string encoding extracted with thanks from em-http-request module QueryEncoder class << self - # URL encodes query parameters: - # single k=v, or a URL encoded array, if v is an array of values - def encode_param(k, v) - if v.is_a?(Array) - v.map { |e| escape(k) + "[]=" + escape(e) }.join("&") - else - escape(k) + "=" + escape(v) - end + # URL encodes query parameters + def encode_params(hash) + collect_entries_for([], hash) { |v| escape(v) }.join('&') end - + # Like encode_param, but doesn't url escape keys or values - def encode_param_without_escaping(k, v) - if v.is_a?(Array) - v.map { |e| k + "[]=" + e }.join("&") + def encode_params_without_escaping(hash) + collect_entries_for([], hash).join('&') + end + + private + + def collect_entries_for(prefix, value, entries = [], &block) + case value + when Array + value.each { |v| collect_entries_for(prefix + [nil], v, entries) } + when Hash + value = with_normalized_keys value + value.keys.sort.each do |key| + collect_entries_for(prefix + [key], value[key], entries) + end else - "#{k}=#{v}" + entries << entry(prefix, value, &block) end + entries end - private + def entry(key, value) + query_key = key.inject('') do |memo, part| + if part.nil? + memo << "[]" + else + part = if block_given? then yield part else part end + if memo == '' + memo << part + else + memo << "[#{part}]" + end + end + memo + end + value = if block_given? then yield value else value end + "#{query_key}=#{value}" + end def escape(s) if defined?(EscapeUtils) @@ -42,6 +66,12 @@ def bytesize(string) string.size end end + + def with_normalized_keys(hash) + normalized = {} + hash.each { |key, value| normalized[key.to_s.downcase] = value } + normalized + end end end end diff --git a/spec/signature_spec.rb b/spec/signature_spec.rb index 3d972bf..ec15e79 100644 --- a/spec/signature_spec.rb +++ b/spec/signature_spec.rb @@ -67,6 +67,14 @@ @request.send(:string_to_sign).should == "POST\n/some/path\nthings[]=thing1&things[]=thing2" end + it "should generate correct string when query hash contains nested elements" do + @request.query_hash = { + "things" => [{ "thing_1" => "value1" }, { "thing_2" => "value2" }] + } + @request.send(:string_to_sign).should == + "POST\n/some/path\nthings[][thing_1]=value1&things[][thing_2]=value2" + end + # This may well change in auth version 2 it "should not escape keys or values in the query string" do @request.query_hash = {