faraday 0.7.4 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +276 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +40 -153
  5. data/Rakefile +4 -139
  6. data/examples/client_spec.rb +65 -0
  7. data/examples/client_test.rb +79 -0
  8. data/lib/faraday/adapter/em_http.rb +286 -0
  9. data/lib/faraday/adapter/em_http_ssl_patch.rb +62 -0
  10. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +69 -0
  11. data/lib/faraday/adapter/em_synchrony.rb +120 -36
  12. data/lib/faraday/adapter/excon.rb +108 -12
  13. data/lib/faraday/adapter/httpclient.rb +152 -0
  14. data/lib/faraday/adapter/net_http.rb +187 -43
  15. data/lib/faraday/adapter/net_http_persistent.rb +91 -0
  16. data/lib/faraday/adapter/patron.rb +106 -10
  17. data/lib/faraday/adapter/rack.rb +75 -0
  18. data/lib/faraday/adapter/test.rb +160 -61
  19. data/lib/faraday/adapter/typhoeus.rb +7 -46
  20. data/lib/faraday/adapter.rb +105 -33
  21. data/lib/faraday/adapter_registry.rb +30 -0
  22. data/lib/faraday/autoload.rb +95 -0
  23. data/lib/faraday/connection.rb +525 -157
  24. data/lib/faraday/dependency_loader.rb +37 -0
  25. data/lib/faraday/encoders/flat_params_encoder.rb +98 -0
  26. data/lib/faraday/encoders/nested_params_encoder.rb +171 -0
  27. data/lib/faraday/error.rb +122 -30
  28. data/lib/faraday/file_part.rb +128 -0
  29. data/lib/faraday/logging/formatter.rb +105 -0
  30. data/lib/faraday/middleware.rb +14 -22
  31. data/lib/faraday/middleware_registry.rb +129 -0
  32. data/lib/faraday/options/connection_options.rb +22 -0
  33. data/lib/faraday/options/env.rb +181 -0
  34. data/lib/faraday/options/proxy_options.rb +28 -0
  35. data/lib/faraday/options/request_options.rb +22 -0
  36. data/lib/faraday/options/ssl_options.rb +59 -0
  37. data/lib/faraday/options.rb +222 -0
  38. data/lib/faraday/param_part.rb +53 -0
  39. data/lib/faraday/parameters.rb +5 -0
  40. data/lib/faraday/rack_builder.rb +248 -0
  41. data/lib/faraday/request/authorization.rb +55 -0
  42. data/lib/faraday/request/basic_authentication.rb +20 -0
  43. data/lib/faraday/request/instrumentation.rb +54 -0
  44. data/lib/faraday/request/multipart.rb +84 -48
  45. data/lib/faraday/request/retry.rb +239 -0
  46. data/lib/faraday/request/token_authentication.rb +20 -0
  47. data/lib/faraday/request/url_encoded.rb +46 -27
  48. data/lib/faraday/request.rb +112 -50
  49. data/lib/faraday/response/logger.rb +24 -25
  50. data/lib/faraday/response/raise_error.rb +40 -11
  51. data/lib/faraday/response.rb +44 -35
  52. data/lib/faraday/utils/headers.rb +139 -0
  53. data/lib/faraday/utils/params_hash.rb +61 -0
  54. data/lib/faraday/utils.rb +72 -117
  55. data/lib/faraday.rb +142 -64
  56. data/spec/external_adapters/faraday_specs_setup.rb +14 -0
  57. data/spec/faraday/adapter/em_http_spec.rb +47 -0
  58. data/spec/faraday/adapter/em_synchrony_spec.rb +16 -0
  59. data/spec/faraday/adapter/excon_spec.rb +49 -0
  60. data/spec/faraday/adapter/httpclient_spec.rb +73 -0
  61. data/spec/faraday/adapter/net_http_persistent_spec.rb +57 -0
  62. data/spec/faraday/adapter/net_http_spec.rb +64 -0
  63. data/spec/faraday/adapter/patron_spec.rb +18 -0
  64. data/spec/faraday/adapter/rack_spec.rb +8 -0
  65. data/spec/faraday/adapter/typhoeus_spec.rb +7 -0
  66. data/spec/faraday/adapter_registry_spec.rb +28 -0
  67. data/spec/faraday/adapter_spec.rb +55 -0
  68. data/spec/faraday/composite_read_io_spec.rb +80 -0
  69. data/spec/faraday/connection_spec.rb +691 -0
  70. data/spec/faraday/error_spec.rb +45 -0
  71. data/spec/faraday/middleware_spec.rb +26 -0
  72. data/spec/faraday/options/env_spec.rb +70 -0
  73. data/spec/faraday/options/options_spec.rb +297 -0
  74. data/spec/faraday/options/proxy_options_spec.rb +37 -0
  75. data/spec/faraday/options/request_options_spec.rb +19 -0
  76. data/spec/faraday/params_encoders/flat_spec.rb +34 -0
  77. data/spec/faraday/params_encoders/nested_spec.rb +134 -0
  78. data/spec/faraday/rack_builder_spec.rb +196 -0
  79. data/spec/faraday/request/authorization_spec.rb +88 -0
  80. data/spec/faraday/request/instrumentation_spec.rb +76 -0
  81. data/spec/faraday/request/multipart_spec.rb +274 -0
  82. data/spec/faraday/request/retry_spec.rb +242 -0
  83. data/spec/faraday/request/url_encoded_spec.rb +83 -0
  84. data/spec/faraday/request_spec.rb +109 -0
  85. data/spec/faraday/response/logger_spec.rb +220 -0
  86. data/spec/faraday/response/middleware_spec.rb +68 -0
  87. data/spec/faraday/response/raise_error_spec.rb +106 -0
  88. data/spec/faraday/response_spec.rb +75 -0
  89. data/spec/faraday/utils/headers_spec.rb +82 -0
  90. data/spec/faraday/utils_spec.rb +56 -0
  91. data/spec/faraday_spec.rb +37 -0
  92. data/spec/spec_helper.rb +132 -0
  93. data/spec/support/disabling_stub.rb +14 -0
  94. data/spec/support/fake_safe_buffer.rb +15 -0
  95. data/spec/support/helper_methods.rb +133 -0
  96. data/spec/support/shared_examples/adapter.rb +104 -0
  97. data/spec/support/shared_examples/params_encoder.rb +18 -0
  98. data/spec/support/shared_examples/request_method.rb +234 -0
  99. data/spec/support/streaming_response_checker.rb +35 -0
  100. data/spec/support/webmock_rack_app.rb +68 -0
  101. metadata +126 -126
  102. data/Gemfile +0 -29
  103. data/config.ru +0 -6
  104. data/faraday.gemspec +0 -92
  105. data/lib/faraday/adapter/action_dispatch.rb +0 -29
  106. data/lib/faraday/builder.rb +0 -160
  107. data/lib/faraday/request/json.rb +0 -35
  108. data/lib/faraday/upload_io.rb +0 -23
  109. data/test/adapters/live_test.rb +0 -205
  110. data/test/adapters/logger_test.rb +0 -37
  111. data/test/adapters/net_http_test.rb +0 -33
  112. data/test/adapters/test_middleware_test.rb +0 -70
  113. data/test/connection_test.rb +0 -254
  114. data/test/env_test.rb +0 -158
  115. data/test/helper.rb +0 -41
  116. data/test/live_server.rb +0 -45
  117. data/test/middleware_stack_test.rb +0 -118
  118. data/test/request_middleware_test.rb +0 -116
  119. data/test/response_middleware_test.rb +0 -74
@@ -1,98 +1,107 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
 
3
5
  module Faraday
6
+ # Response represents an HTTP response from making an HTTP request.
4
7
  class Response
5
8
  # Used for simple response middleware.
6
9
  class Middleware < Faraday::Middleware
7
10
  def call(env)
8
- @app.call(env).on_complete do |env|
9
- on_complete(env)
11
+ @app.call(env).on_complete do |environment|
12
+ on_complete(environment)
10
13
  end
11
14
  end
12
15
 
13
16
  # Override this to modify the environment after the response has finished.
14
17
  # Calls the `parse` method if defined
18
+ # `parse` method can be defined as private, public and protected
15
19
  def on_complete(env)
16
- if respond_to? :parse
17
- env[:body] = parse(env[:body]) unless [204,304].index env[:status]
18
- end
20
+ return unless respond_to?(:parse, true) && env.parse_body?
21
+
22
+ env.body = parse(env.body)
19
23
  end
20
24
  end
21
25
 
22
26
  extend Forwardable
23
- extend AutoloadHelper
27
+ extend MiddlewareRegistry
24
28
 
25
- autoload_all 'faraday/response',
26
- :RaiseError => 'raise_error',
27
- :Logger => 'logger'
28
-
29
- register_lookup_modules \
30
- :raise_error => :RaiseError,
31
- :logger => :Logger
29
+ register_middleware File.expand_path('response', __dir__),
30
+ raise_error: [:RaiseError, 'raise_error'],
31
+ logger: [:Logger, 'logger']
32
32
 
33
33
  def initialize(env = nil)
34
- @env = env
34
+ @env = Env.from(env) if env
35
35
  @on_complete_callbacks = []
36
36
  end
37
37
 
38
38
  attr_reader :env
39
- alias_method :to_hash, :env
40
39
 
41
40
  def status
42
- finished? ? env[:status] : nil
41
+ finished? ? env.status : nil
42
+ end
43
+
44
+ def reason_phrase
45
+ finished? ? env.reason_phrase : nil
43
46
  end
44
47
 
45
48
  def headers
46
- finished? ? env[:response_headers] : {}
49
+ finished? ? env.response_headers : {}
47
50
  end
48
51
  def_delegator :headers, :[]
49
52
 
50
53
  def body
51
- finished? ? env[:body] : nil
54
+ finished? ? env.body : nil
52
55
  end
53
56
 
54
57
  def finished?
55
58
  !!env
56
59
  end
57
60
 
58
- def on_complete
59
- if not finished?
60
- @on_complete_callbacks << Proc.new
61
+ def on_complete(&block)
62
+ if !finished?
63
+ @on_complete_callbacks << block
61
64
  else
62
- yield env
65
+ yield(env)
63
66
  end
64
- return self
67
+ self
65
68
  end
66
69
 
67
70
  def finish(env)
68
- raise "response already finished" if finished?
69
- @env = env
70
- @on_complete_callbacks.each { |callback| callback.call(env) }
71
- return self
71
+ raise 'response already finished' if finished?
72
+
73
+ @env = env.is_a?(Env) ? env : Env.from(env)
74
+ @on_complete_callbacks.each { |callback| callback.call(@env) }
75
+ self
72
76
  end
73
77
 
74
78
  def success?
75
- (200..299).include?(status)
79
+ finished? && env.success?
80
+ end
81
+
82
+ def to_hash
83
+ {
84
+ status: env.status, body: env.body,
85
+ response_headers: env.response_headers
86
+ }
76
87
  end
77
88
 
78
89
  # because @on_complete_callbacks cannot be marshalled
79
90
  def marshal_dump
80
- !finished? ? nil : {
81
- :status => @env[:status], :body => @env[:body],
82
- :response_headers => @env[:response_headers]
83
- }
91
+ finished? ? to_hash : nil
84
92
  end
85
93
 
86
94
  def marshal_load(env)
87
- @env = env
95
+ @env = Env.from(env)
88
96
  end
89
97
 
90
98
  # Expand the env with more properties, without overriding existing ones.
91
99
  # Useful for applying request params after restoring a marshalled Response.
92
100
  def apply_request(request_env)
93
101
  raise "response didn't finish yet" unless finished?
94
- @env = request_env.merge @env
95
- return self
102
+
103
+ @env = Env.from(request_env).update(@env)
104
+ self
96
105
  end
97
106
  end
98
107
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Utils
5
+ # A case-insensitive Hash that preserves the original case of a header
6
+ # when set.
7
+ #
8
+ # Adapted from Rack::Utils::HeaderHash
9
+ class Headers < ::Hash
10
+ def self.from(value)
11
+ new(value)
12
+ end
13
+
14
+ def self.allocate
15
+ new_self = super
16
+ new_self.initialize_names
17
+ new_self
18
+ end
19
+
20
+ def initialize(hash = nil)
21
+ super()
22
+ @names = {}
23
+ update(hash || {})
24
+ end
25
+
26
+ def initialize_names
27
+ @names = {}
28
+ end
29
+
30
+ # on dup/clone, we need to duplicate @names hash
31
+ def initialize_copy(other)
32
+ super
33
+ @names = other.names.dup
34
+ end
35
+
36
+ # need to synchronize concurrent writes to the shared KeyMap
37
+ keymap_mutex = Mutex.new
38
+
39
+ # symbol -> string mapper + cache
40
+ KeyMap = Hash.new do |map, key|
41
+ value = if key.respond_to?(:to_str)
42
+ key
43
+ else
44
+ key.to_s.split('_') # user_agent: %w(user agent)
45
+ .each(&:capitalize!) # => %w(User Agent)
46
+ .join('-') # => "User-Agent"
47
+ end
48
+ keymap_mutex.synchronize { map[key] = value }
49
+ end
50
+ KeyMap[:etag] = 'ETag'
51
+
52
+ def [](key)
53
+ key = KeyMap[key]
54
+ super(key) || super(@names[key.downcase])
55
+ end
56
+
57
+ def []=(key, val)
58
+ key = KeyMap[key]
59
+ key = (@names[key.downcase] ||= key)
60
+ # join multiple values with a comma
61
+ val = val.to_ary.join(', ') if val.respond_to?(:to_ary)
62
+ super(key, val)
63
+ end
64
+
65
+ def fetch(key, *args, &block)
66
+ key = KeyMap[key]
67
+ key = @names.fetch(key.downcase, key)
68
+ super(key, *args, &block)
69
+ end
70
+
71
+ def delete(key)
72
+ key = KeyMap[key]
73
+ key = @names[key.downcase]
74
+ return unless key
75
+
76
+ @names.delete key.downcase
77
+ super(key)
78
+ end
79
+
80
+ def include?(key)
81
+ @names.include? key.downcase
82
+ end
83
+
84
+ alias has_key? include?
85
+ alias member? include?
86
+ alias key? include?
87
+
88
+ def merge!(other)
89
+ other.each { |k, v| self[k] = v }
90
+ self
91
+ end
92
+
93
+ alias update merge!
94
+
95
+ def merge(other)
96
+ hash = dup
97
+ hash.merge! other
98
+ end
99
+
100
+ def replace(other)
101
+ clear
102
+ @names.clear
103
+ update other
104
+ self
105
+ end
106
+
107
+ def to_hash
108
+ ::Hash.new.update(self)
109
+ end
110
+
111
+ def parse(header_string)
112
+ return unless header_string && !header_string.empty?
113
+
114
+ headers = header_string.split(/\r\n/)
115
+
116
+ # Find the last set of response headers.
117
+ start_index = headers.rindex { |x| x.match(%r{^HTTP/}) } || 0
118
+ last_response = headers.slice(start_index, headers.size)
119
+
120
+ last_response
121
+ .tap { |a| a.shift if a.first.start_with?('HTTP/') }
122
+ .map { |h| h.split(/:\s*/, 2) } # split key and value
123
+ .reject { |p| p[0].nil? } # ignore blank lines
124
+ .each { |key, value| add_parsed(key, value) }
125
+ end
126
+
127
+ protected
128
+
129
+ attr_reader :names
130
+
131
+ private
132
+
133
+ # Join multiple values with a comma.
134
+ def add_parsed(key, value)
135
+ self[key] ? self[key] << ', ' << value : self[key] = value
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Utils
5
+ # A hash with stringified keys.
6
+ class ParamsHash < Hash
7
+ def [](key)
8
+ super(convert_key(key))
9
+ end
10
+
11
+ def []=(key, value)
12
+ super(convert_key(key), value)
13
+ end
14
+
15
+ def delete(key)
16
+ super(convert_key(key))
17
+ end
18
+
19
+ def include?(key)
20
+ super(convert_key(key))
21
+ end
22
+
23
+ alias has_key? include?
24
+ alias member? include?
25
+ alias key? include?
26
+
27
+ def update(params)
28
+ params.each do |key, value|
29
+ self[key] = value
30
+ end
31
+ self
32
+ end
33
+ alias merge! update
34
+
35
+ def merge(params)
36
+ dup.update(params)
37
+ end
38
+
39
+ def replace(other)
40
+ clear
41
+ update(other)
42
+ end
43
+
44
+ def merge_query(query, encoder = nil)
45
+ return self unless query && !query.empty?
46
+
47
+ update((encoder || Utils.default_params_encoder).decode(query))
48
+ end
49
+
50
+ def to_query(encoder = nil)
51
+ (encoder || Utils.default_params_encoder).encode(self)
52
+ end
53
+
54
+ private
55
+
56
+ def convert_key(key)
57
+ key.to_s
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/faraday/utils.rb CHANGED
@@ -1,149 +1,106 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday/utils/headers'
4
+ require 'faraday/utils/params_hash'
2
5
 
3
6
  module Faraday
7
+ # Utils contains various static helper methods.
4
8
  module Utils
5
- include Rack::Utils
6
-
7
- extend Rack::Utils
8
- extend self
9
-
10
- class Headers < HeaderHash
11
- # symbol -> string mapper + cache
12
- KeyMap = Hash.new do |map, key|
13
- map[key] = if key.respond_to?(:to_str) then key
14
- else
15
- key.to_s.split('_'). # :user_agent => %w(user agent)
16
- each { |w| w.capitalize! }. # => %w(User Agent)
17
- join('-') # => "User-Agent"
18
- end
19
- end
20
- KeyMap[:etag] = "ETag"
21
-
22
- def [](k)
23
- super(KeyMap[k])
24
- end
9
+ module_function
25
10
 
26
- def []=(k, v)
27
- # join multiple values with a comma
28
- v = v.to_ary.join(', ') if v.respond_to? :to_ary
29
- super(KeyMap[k], v)
30
- end
31
-
32
- alias_method :update, :merge!
33
-
34
- def parse(header_string)
35
- return unless header_string && !header_string.empty?
36
- header_string.split(/\r\n/).
37
- tap { |a| a.shift if a.first.index('HTTP/') == 0 }. # drop the HTTP status line
38
- map { |h| h.split(/:\s+/, 2) }.reject { |(k, v)| k.nil? }. # split key and value, ignore blank lines
39
- each { |key, value|
40
- # join multiple values with a comma
41
- if self[key] then self[key] << ', ' << value
42
- else self[key] = value
43
- end
44
- }
45
- end
11
+ def build_query(params)
12
+ FlatParamsEncoder.encode(params)
46
13
  end
47
14
 
48
- # hash with stringified keys
49
- class ParamsHash < Hash
50
- def [](key)
51
- super(convert_key(key))
52
- end
53
-
54
- def []=(key, value)
55
- super(convert_key(key), value)
56
- end
15
+ def build_nested_query(params)
16
+ NestedParamsEncoder.encode(params)
17
+ end
57
18
 
58
- def delete(key)
59
- super(convert_key(key))
60
- end
19
+ def default_space_encoding
20
+ @default_space_encoding ||= '+'
21
+ end
61
22
 
62
- def include?(key)
63
- super(convert_key(key))
64
- end
23
+ class << self
24
+ attr_writer :default_space_encoding
25
+ end
65
26
 
66
- alias_method :has_key?, :include?
67
- alias_method :member?, :include?
68
- alias_method :key?, :include?
27
+ ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/.freeze
69
28
 
70
- def update(params)
71
- params.each do |key, value|
72
- self[key] = value
73
- end
74
- self
75
- end
76
- alias_method :merge!, :update
29
+ def escape(str)
30
+ str.to_s.gsub(ESCAPE_RE) do |match|
31
+ '%' + match.unpack('H2' * match.bytesize).join('%').upcase
32
+ end.gsub(' ', default_space_encoding)
33
+ end
77
34
 
78
- def merge(params)
79
- dup.update(params)
80
- end
35
+ def unescape(str)
36
+ CGI.unescape str.to_s
37
+ end
81
38
 
82
- def replace(other)
83
- clear
84
- update(other)
85
- end
39
+ DEFAULT_SEP = /[&;] */n.freeze
86
40
 
87
- def merge_query(query)
88
- if query && !query.empty?
89
- update Utils.parse_query(query)
90
- end
91
- self
92
- end
41
+ # Adapted from Rack
42
+ def parse_query(query)
43
+ FlatParamsEncoder.decode(query)
44
+ end
93
45
 
94
- def to_query
95
- Utils.build_query(self)
96
- end
46
+ def parse_nested_query(query)
47
+ NestedParamsEncoder.decode(query)
48
+ end
97
49
 
98
- private
50
+ def default_params_encoder
51
+ @default_params_encoder ||= NestedParamsEncoder
52
+ end
99
53
 
100
- def convert_key(key)
101
- key.to_s
102
- end
54
+ class << self
55
+ attr_writer :default_params_encoder
103
56
  end
104
57
 
105
- # Make Rack::Utils methods public.
106
- public :build_query, :parse_query
107
-
108
- # Override Rack's version since it doesn't handle non-String values
109
- def build_nested_query(value, prefix = nil)
110
- case value
111
- when Array
112
- value.map { |v| build_nested_query(v, "#{prefix}[]") }.join("&")
113
- when Hash
114
- value.map { |k, v|
115
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
116
- }.join("&")
117
- when NilClass
118
- prefix
58
+ # Normalize URI() behavior across Ruby versions
59
+ #
60
+ # url - A String or URI.
61
+ #
62
+ # Returns a parsed URI.
63
+ def URI(url) # rubocop:disable Naming/MethodName
64
+ if url.respond_to?(:host)
65
+ url
66
+ elsif url.respond_to?(:to_str)
67
+ default_uri_parser.call(url)
119
68
  else
120
- raise ArgumentError, "value must be a Hash" if prefix.nil?
121
- "#{prefix}=#{escape(value)}"
69
+ raise ArgumentError, 'bad argument (expected URI object or URI string)'
122
70
  end
123
71
  end
124
72
 
125
- # Be sure to URI escape '+' symbols to %2B. Otherwise, they get interpreted
126
- # as spaces.
127
- def escape(s)
128
- s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) do
129
- '%' << $1.unpack('H2'*bytesize($1)).join('%').tap { |c| c.upcase! }
73
+ def default_uri_parser
74
+ @default_uri_parser ||= begin
75
+ require 'uri'
76
+ Kernel.method(:URI)
130
77
  end
131
78
  end
132
79
 
133
- # Receives a URL and returns just the path with the query string sorted.
80
+ def default_uri_parser=(parser)
81
+ @default_uri_parser = if parser.respond_to?(:call) || parser.nil?
82
+ parser
83
+ else
84
+ parser.method(:parse)
85
+ end
86
+ end
87
+
88
+ # Receives a String or URI and returns just
89
+ # the path with the query string sorted.
134
90
  def normalize_path(url)
135
- (url.path != "" ? url.path : "/") +
136
- (url.query ? "?#{sort_query_params(url.query)}" : "")
91
+ url = URI(url)
92
+ (url.path.start_with?('/') ? url.path : '/' + url.path) +
93
+ (url.query ? "?#{sort_query_params(url.query)}" : '')
137
94
  end
138
95
 
139
96
  # Recursive hash update
140
97
  def deep_merge!(target, hash)
141
98
  hash.each do |key, value|
142
- if Hash === value and Hash === target[key]
143
- target[key] = deep_merge(target[key], value)
144
- else
145
- target[key] = value
146
- end
99
+ target[key] = if value.is_a?(Hash) && target[key].is_a?(Hash)
100
+ deep_merge(target[key], value)
101
+ else
102
+ value
103
+ end
147
104
  end
148
105
  target
149
106
  end
@@ -153,8 +110,6 @@ module Faraday
153
110
  deep_merge!(source.dup, hash)
154
111
  end
155
112
 
156
- protected
157
-
158
113
  def sort_query_params(query)
159
114
  query.split('&').sort.join('&')
160
115
  end