webmachine 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +4 -0
  4. data/Gemfile +13 -11
  5. data/README.md +85 -89
  6. data/Rakefile +0 -1
  7. data/documentation/adapters.md +39 -0
  8. data/documentation/authentication-and-authorization.md +37 -0
  9. data/documentation/configurator.md +19 -0
  10. data/documentation/error-handling.md +86 -0
  11. data/documentation/examples.md +215 -0
  12. data/documentation/how-it-works.md +76 -0
  13. data/documentation/routes.md +97 -0
  14. data/documentation/validation.md +159 -0
  15. data/documentation/versioning-apis.md +74 -0
  16. data/documentation/visual-debugger.md +38 -0
  17. data/examples/application.rb +2 -2
  18. data/examples/debugger.rb +1 -1
  19. data/lib/webmachine.rb +3 -1
  20. data/lib/webmachine/adapter.rb +7 -13
  21. data/lib/webmachine/adapters.rb +1 -2
  22. data/lib/webmachine/adapters/httpkit.rb +74 -0
  23. data/lib/webmachine/adapters/lazy_request_body.rb +1 -2
  24. data/lib/webmachine/adapters/rack.rb +37 -21
  25. data/lib/webmachine/adapters/reel.rb +21 -23
  26. data/lib/webmachine/adapters/webrick.rb +16 -16
  27. data/lib/webmachine/application.rb +2 -2
  28. data/lib/webmachine/chunked_body.rb +3 -4
  29. data/lib/webmachine/constants.rb +75 -0
  30. data/lib/webmachine/decision/conneg.rb +12 -10
  31. data/lib/webmachine/decision/flow.rb +31 -21
  32. data/lib/webmachine/decision/fsm.rb +10 -18
  33. data/lib/webmachine/decision/helpers.rb +9 -37
  34. data/lib/webmachine/dispatcher.rb +13 -10
  35. data/lib/webmachine/dispatcher/route.rb +18 -8
  36. data/lib/webmachine/errors.rb +7 -1
  37. data/lib/webmachine/header_negotiation.rb +25 -0
  38. data/lib/webmachine/headers.rb +7 -2
  39. data/lib/webmachine/locale/en.yml +7 -5
  40. data/lib/webmachine/media_type.rb +10 -8
  41. data/lib/webmachine/request.rb +44 -15
  42. data/lib/webmachine/resource.rb +1 -1
  43. data/lib/webmachine/resource/callbacks.rb +6 -4
  44. data/lib/webmachine/spec/IO_response.body +1 -0
  45. data/lib/webmachine/spec/adapter_lint.rb +70 -36
  46. data/lib/webmachine/spec/test_resource.rb +10 -4
  47. data/lib/webmachine/streaming/fiber_encoder.rb +1 -5
  48. data/lib/webmachine/streaming/io_encoder.rb +6 -0
  49. data/lib/webmachine/trace.rb +1 -0
  50. data/lib/webmachine/trace/fsm.rb +20 -10
  51. data/lib/webmachine/trace/resource_proxy.rb +2 -0
  52. data/lib/webmachine/translation.rb +2 -1
  53. data/lib/webmachine/version.rb +3 -3
  54. data/memory_test.rb +37 -0
  55. data/spec/spec_helper.rb +9 -9
  56. data/spec/webmachine/adapter_spec.rb +14 -15
  57. data/spec/webmachine/adapters/httpkit_spec.rb +10 -0
  58. data/spec/webmachine/adapters/rack_spec.rb +6 -6
  59. data/spec/webmachine/adapters/reel_spec.rb +15 -11
  60. data/spec/webmachine/adapters/webrick_spec.rb +2 -2
  61. data/spec/webmachine/application_spec.rb +18 -17
  62. data/spec/webmachine/chunked_body_spec.rb +3 -3
  63. data/spec/webmachine/configuration_spec.rb +5 -5
  64. data/spec/webmachine/cookie_spec.rb +13 -13
  65. data/spec/webmachine/decision/conneg_spec.rb +48 -42
  66. data/spec/webmachine/decision/falsey_spec.rb +4 -4
  67. data/spec/webmachine/decision/flow_spec.rb +194 -144
  68. data/spec/webmachine/decision/fsm_spec.rb +17 -17
  69. data/spec/webmachine/decision/helpers_spec.rb +20 -20
  70. data/spec/webmachine/dispatcher/route_spec.rb +73 -27
  71. data/spec/webmachine/dispatcher_spec.rb +34 -24
  72. data/spec/webmachine/errors_spec.rb +1 -1
  73. data/spec/webmachine/etags_spec.rb +19 -19
  74. data/spec/webmachine/events_spec.rb +6 -6
  75. data/spec/webmachine/headers_spec.rb +14 -14
  76. data/spec/webmachine/media_type_spec.rb +36 -36
  77. data/spec/webmachine/request_spec.rb +33 -33
  78. data/spec/webmachine/resource/authentication_spec.rb +6 -6
  79. data/spec/webmachine/response_spec.rb +12 -12
  80. data/spec/webmachine/trace/fsm_spec.rb +8 -8
  81. data/spec/webmachine/trace/resource_proxy_spec.rb +9 -9
  82. data/spec/webmachine/trace/trace_store_spec.rb +5 -5
  83. data/spec/webmachine/trace_spec.rb +3 -3
  84. data/webmachine.gemspec +2 -6
  85. metadata +48 -206
  86. data/lib/webmachine/adapters/hatetepe.rb +0 -108
  87. data/lib/webmachine/adapters/mongrel.rb +0 -127
  88. data/lib/webmachine/dispatcher/not_found_resource.rb +0 -5
  89. data/lib/webmachine/fiber18.rb +0 -88
  90. data/spec/webmachine/adapters/hatetepe_spec.rb +0 -60
  91. data/spec/webmachine/adapters/mongrel_spec.rb +0 -16
@@ -0,0 +1,74 @@
1
+ # Versioning APIs
2
+
3
+ ## By URL
4
+
5
+ ```ruby
6
+
7
+ class MyResourceV1 < Webmachine::Resource
8
+
9
+ end
10
+
11
+ class MyResourceV2 < Webmachine::Resource
12
+
13
+ end
14
+
15
+ App = Webmachine::Application.new do |app|
16
+ app.routes do
17
+ add ["api", "v1", "myresource"], MyResourceV1
18
+ add ["api", "v2", "myresource"], MyResourceV2
19
+ end
20
+ end
21
+
22
+ ```
23
+
24
+ ## By Content-Type
25
+
26
+ Note: if no Accept header is specified, then the first content type in the list will be chosen.
27
+
28
+ ```ruby
29
+
30
+ class MyResource < Webmachine::Resource
31
+
32
+ def content_types_provided
33
+ [
34
+ ["application/myapp.v2+json", :to_json_v2],
35
+ ["application/myapp.v1+json", :to_json_v1]
36
+ ]
37
+ end
38
+
39
+ end
40
+
41
+ ```
42
+
43
+ ## By Header value
44
+
45
+ ```ruby
46
+
47
+ class MyResourceV1 < Webmachine::Resource
48
+
49
+ end
50
+
51
+ class MyResourceV2 < Webmachine::Resource
52
+
53
+ end
54
+
55
+ class VersionGuard
56
+
57
+ def initialize version
58
+ @version = version
59
+ end
60
+
61
+ def call(request)
62
+ request.headers['X-My-App-Version'] == @version
63
+ end
64
+
65
+ end
66
+
67
+ App = Webmachine::Application.new do |app|
68
+ app.routes do
69
+ add ["api", "myresource"], VersionGuard.new("1"), MyResourceV1
70
+ add ["api", "myresource"], VersionGuard.new("2"), MyResourceV2
71
+ end
72
+ end
73
+
74
+ ```
@@ -0,0 +1,38 @@
1
+ ### Visual debugger
2
+
3
+ In development, you can turn on tracing of the
4
+ decision graph for a resource by implementing the `#trace?` callback
5
+ so that it returns true:
6
+
7
+ ```ruby
8
+ class MyTracedResource < Webmachine::Resource
9
+ def trace?
10
+ true
11
+ end
12
+
13
+ # The rest of your callbacks...
14
+ end
15
+ ```
16
+
17
+ Then enable the visual debugger resource by adding a route to your
18
+ configuration:
19
+
20
+ ```ruby
21
+ Webmachine.application.routes do
22
+ # This can be any path as long as it ends with :*
23
+ add ['trace', :*], Webmachine::Trace::TraceResource
24
+ # The rest of your routes...
25
+ end
26
+ ```
27
+
28
+ Now when you visit your traced resource, a trace of the request
29
+ process will be recorded in memory. Open your browser to `/trace` to
30
+ list the recorded traces and inspect the result. The response from your
31
+ traced resource will also include the `X-Webmachine-Trace-Id` that you
32
+ can use to lookup the trace. It might look something like this:
33
+
34
+ ![preview calls at decision](http://seancribbs-skitch.s3.amazonaws.com/Webmachine_Trace_2156885920-20120625-100153.png)
35
+
36
+ Refer to
37
+ [examples/debugger.rb](/examples/debugger.rb)
38
+ for an example of how to enable the debugger.
@@ -25,10 +25,10 @@ MyApp = Webmachine::Application.new do |app|
25
25
  config.adapter = :WEBrick
26
26
  end
27
27
  # And add routes like this:
28
- app.add_route ['fizz', :buzz, '*'], RouteDebugResource
28
+ app.add_route ['fizz', :buzz, :*], RouteDebugResource
29
29
  # OR add routes this way:
30
30
  app.routes do
31
- add [:test, :foo, '*'], RouteDebugResource
31
+ add [:test, :foo, :*], RouteDebugResource
32
32
  end
33
33
  end
34
34
 
data/examples/debugger.rb CHANGED
@@ -31,7 +31,7 @@ end
31
31
 
32
32
  TraceExample = Webmachine::Application.new do |app|
33
33
  app.routes do
34
- add ['trace', '*'], Webmachine::Trace::TraceResource
34
+ add ['trace', :*], Webmachine::Trace::TraceResource
35
35
  add [], MyTracedResource
36
36
  end
37
37
  end
data/lib/webmachine.rb CHANGED
@@ -1,10 +1,12 @@
1
- require 'webmachine/configuration'
1
+ require 'webmachine/configuration'
2
+ require 'webmachine/constants'
2
3
  require 'webmachine/cookie'
3
4
  require 'webmachine/headers'
4
5
  require 'webmachine/request'
5
6
  require 'webmachine/response'
6
7
  require 'webmachine/etags'
7
8
  require 'webmachine/errors'
9
+ require 'webmachine/header_negotiation'
8
10
  require 'webmachine/decision'
9
11
  require 'webmachine/streaming'
10
12
  require 'webmachine/adapter'
@@ -5,23 +5,17 @@ module Webmachine
5
5
  # @abstract Subclass and override {#run} to implement a custom adapter.
6
6
  class Adapter
7
7
 
8
- # @return [Webmachine::Configuration] the application's configuration.
9
- attr_reader :configuration
8
+ # @return [Webmachine::Application] returns the application
9
+ attr_reader :application
10
10
 
11
- # @return [Webmachine::Dispatcher] the application's dispatcher.
12
- attr_reader :dispatcher
13
-
14
- # @param [Webmachine::Configuration] configuration the application's
15
- # configuration.
16
- # @param [Webmachine::Dispatcher] dispatcher the application's dispatcher.
17
- def initialize(configuration, dispatcher)
18
- @configuration = configuration
19
- @dispatcher = dispatcher
11
+ # @param [Webmachine::Application] application the application
12
+ def initialize(application)
13
+ @application = application
20
14
  end
21
15
 
22
16
  # Create a new adapter and run it.
23
- def self.run(configuration, dispatcher)
24
- new(configuration, dispatcher).run
17
+ def self.run(application)
18
+ new(application).run
25
19
  end
26
20
 
27
21
  # Start the adapter.
@@ -5,8 +5,7 @@ module Webmachine
5
5
  # Contains classes and modules that connect Webmachine to Ruby
6
6
  # application servers.
7
7
  module Adapters
8
- autoload :Mongrel, 'webmachine/adapters/mongrel'
9
8
  autoload :Reel, 'webmachine/adapters/reel'
10
- autoload :Hatetepe, 'webmachine/adapters/hatetepe'
9
+ autoload :HTTPkit, 'webmachine/adapters/httpkit'
11
10
  end
12
11
  end
@@ -0,0 +1,74 @@
1
+ require 'webmachine/adapter'
2
+ require 'webmachine/constants'
3
+ require 'webmachine/version'
4
+ require 'httpkit'
5
+ require 'webmachine/version'
6
+ require 'webmachine/response'
7
+ require 'webmachine/request'
8
+ require 'webmachine/headers'
9
+
10
+ module Webmachine
11
+ module Adapters
12
+ class HTTPkit < Adapter
13
+ VERSION_STRING = "#{Webmachine::SERVER_STRING} HTTPkit/#{::HTTPkit::VERSION}".freeze
14
+
15
+ def options
16
+ @options ||= {
17
+ :address => application.configuration.ip,
18
+ :port => application.configuration.port,
19
+ :handlers => [
20
+ ::HTTPkit::Server::TimeoutsHandler.new,
21
+ ::HTTPkit::Server::KeepAliveHandler.new,
22
+ self
23
+ ]
24
+ }
25
+ end
26
+
27
+ def run
28
+ ::HTTPkit.start do
29
+ ::HTTPkit::Server.start(options)
30
+ end
31
+ end
32
+
33
+ # Called by HTTPkit::Server for every request
34
+ def serve(request, served)
35
+ response = Webmachine::Response.new
36
+ application.dispatcher.dispatch(convert_request(request), response)
37
+
38
+ served.fulfill(convert_response(response))
39
+ end
40
+
41
+ private
42
+
43
+ # Converts HTTPkit::Request to Webmachine::Request
44
+ def convert_request(request)
45
+ Webmachine::Request.new(
46
+ request.http_method.to_s.upcase,
47
+ request.uri,
48
+ Webmachine::Headers[request.headers.dup],
49
+ request.body)
50
+ end
51
+
52
+ # Converts Webmachine::Response to HTTPkit::Response
53
+ def convert_response(response)
54
+ response.headers[SERVER] = VERSION_STRING
55
+
56
+
57
+ ::HTTPkit::Response.new(
58
+ response.code.to_i,
59
+ response.headers,
60
+ convert_body(response.body))
61
+ end
62
+
63
+ # HTTPkit::Body accepts strings and enumerables, i.e. Webmachine's
64
+ # Callable, Enumerable, IO, and Fiber encoders are supported.
65
+ def convert_body(body)
66
+ if body.respond_to?(:call)
67
+ [body.call]
68
+ else
69
+ body || ''
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,5 +1,4 @@
1
-
2
- module Webmachine
1
+ module Webmachine
3
2
  module Adapters
4
3
  # Wraps a request body so that it can be passed to
5
4
  # {Request} while still lazily evaluating the body.
@@ -1,17 +1,20 @@
1
+ require 'webmachine/adapter'
1
2
  require 'rack'
2
- require 'webmachine/version'
3
+ require 'webmachine/constants'
3
4
  require 'webmachine/headers'
4
5
  require 'webmachine/request'
5
6
  require 'webmachine/response'
6
- require 'webmachine/dispatcher'
7
+ require 'webmachine/version'
7
8
  require 'webmachine/chunked_body'
8
9
 
9
10
  module Webmachine
10
11
  module Adapters
11
12
  # A minimal "shim" adapter to allow Webmachine to interface with Rack. The
12
13
  # intention here is to allow Webmachine to run under Rack-compatible
13
- # web-servers, like unicorn and pow, and is not intended to allow Webmachine
14
- # to be "plugged in" to an existing Rack app as middleware.
14
+ # web-servers, like unicorn and pow.
15
+ # The adapter expects your Webmachine application to be mounted at the root path -
16
+ # it will NOT allow you to nest your Webmachine application at an arbitrary path
17
+ # eg. map "/api" { run MyWebmachineAPI }
15
18
  #
16
19
  # To use this adapter, create a config.ru file and populate it like so:
17
20
  #
@@ -26,7 +29,7 @@ module Webmachine
26
29
  # all "just work".
27
30
  #
28
31
  # And for development or testing your application can be run with Rack's
29
- # builtin Server identically to the Mongrel and WEBrick adapters with:
32
+ # builtin Server identically to the WEBrick adapter with:
30
33
  #
31
34
  # MyApplication.run
32
35
  #
@@ -34,22 +37,22 @@ module Webmachine
34
37
  # Used to override default Rack server options (useful in testing)
35
38
  DEFAULT_OPTIONS = {}
36
39
 
40
+ REQUEST_URI = 'REQUEST_URI'.freeze
41
+ VERSION_STRING = "#{Webmachine::SERVER_STRING} Rack/#{::Rack.version}".freeze
42
+ NEWLINE = "\n".freeze
43
+
37
44
  # Start the Rack adapter
38
45
  def run
39
46
  options = DEFAULT_OPTIONS.merge({
40
47
  :app => self,
41
- :Port => configuration.port,
42
- :Host => configuration.ip
43
- }).merge(configuration.adapter_options)
48
+ :Port => application.configuration.port,
49
+ :Host => application.configuration.ip
50
+ }).merge(application.configuration.adapter_options)
44
51
 
45
52
  @server = ::Rack::Server.new(options)
46
53
  @server.start
47
54
  end
48
55
 
49
- def shutdown
50
- @server.server.shutdown if @server
51
- end
52
-
53
56
  # Handles a Rack-based request.
54
57
  # @param [Hash] env the Rack environment
55
58
  def call(env)
@@ -57,26 +60,28 @@ module Webmachine
57
60
 
58
61
  rack_req = ::Rack::Request.new env
59
62
  request = Webmachine::Request.new(rack_req.request_method,
60
- URI.parse(rack_req.url),
63
+ env[REQUEST_URI],
61
64
  headers,
62
65
  RequestBody.new(rack_req))
63
66
 
64
67
  response = Webmachine::Response.new
65
- @dispatcher.dispatch(request, response)
68
+ application.dispatcher.dispatch(request, response)
66
69
 
67
- response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ")
70
+ response.headers[SERVER] = VERSION_STRING
68
71
 
69
72
  rack_status = response.code
70
- rack_headers = response.headers.flattened("\n")
73
+ rack_headers = response.headers.flattened(NEWLINE)
71
74
  rack_body = case response.body
72
75
  when String # Strings are enumerable in ruby 1.8
73
76
  [response.body]
74
77
  else
75
- if response.body.respond_to?(:call)
78
+ if (io_body = IO.try_convert(response.body))
79
+ io_body
80
+ elsif response.body.respond_to?(:call)
76
81
  Webmachine::ChunkedBody.new(Array(response.body.call))
77
82
  elsif response.body.respond_to?(:each)
78
83
  # This might be an IOEncoder with a Content-Length, which shouldn't be chunked.
79
- if response.headers["Transfer-Encoding"] == "chunked"
84
+ if response.headers[TRANSFER_ENCODING] == "chunked"
80
85
  Webmachine::ChunkedBody.new(response.body)
81
86
  else
82
87
  response.body
@@ -91,6 +96,8 @@ module Webmachine
91
96
  end
92
97
 
93
98
  class RackResponse
99
+ ONE_FIVE = '1.5'.freeze
100
+
94
101
  def initialize(body, status, headers)
95
102
  @body = body
96
103
  @status = status
@@ -98,8 +105,8 @@ module Webmachine
98
105
  end
99
106
 
100
107
  def finish
101
- @headers['Content-Type'] ||= 'text/html' if rack_release_enforcing_content_type
102
- @headers.delete('Content-Type') if response_without_body
108
+ @headers[CONTENT_TYPE] ||= TEXT_HTML if rack_release_enforcing_content_type
109
+ @headers.delete(CONTENT_TYPE) if response_without_body
103
110
  [@status, @headers, @body]
104
111
  end
105
112
 
@@ -110,7 +117,7 @@ module Webmachine
110
117
  end
111
118
 
112
119
  def rack_release_enforcing_content_type
113
- ::Rack.release < '1.5'
120
+ ::Rack.release < ONE_FIVE
114
121
  end
115
122
  end
116
123
 
@@ -123,6 +130,15 @@ module Webmachine
123
130
  @request = request
124
131
  end
125
132
 
133
+ # Rack Servers differ in the way you can access their request bodys.
134
+ # While some allow you to directly get a Ruby IO object others don't.
135
+ # You have to check the methods they expose, like #gets, #read, #each, #rewind and maybe others.
136
+ # See: https://github.com/rack/rack/blob/rack-1.5/lib/rack/lint.rb#L296
137
+ # @return [IO] the request body
138
+ def to_io
139
+ @request.body
140
+ end
141
+
126
142
  # Converts the body to a String so you can work with the entire
127
143
  # thing.
128
144
  # @return [String] the request body as a string
@@ -1,40 +1,41 @@
1
+ require 'webmachine/adapter'
2
+ require 'webmachine/constants'
3
+ require 'set'
1
4
  require 'reel'
2
- require 'webmachine/version'
3
5
  require 'webmachine/headers'
4
6
  require 'webmachine/request'
5
7
  require 'webmachine/response'
6
- require 'webmachine/dispatcher'
7
- require 'set'
8
8
 
9
9
  module Webmachine
10
10
  module Adapters
11
11
  class Reel < Adapter
12
12
  # Used to override default Reel server options (useful in testing)
13
13
  DEFAULT_OPTIONS = {}
14
-
14
+
15
15
  def run
16
16
  @options = DEFAULT_OPTIONS.merge({
17
- :port => configuration.port,
18
- :host => configuration.ip
19
- }).merge(configuration.adapter_options)
17
+ :port => application.configuration.port,
18
+ :host => application.configuration.ip
19
+ }).merge(application.configuration.adapter_options)
20
20
 
21
- if extra_verbs = configuration.adapter_options[:extra_verbs]
21
+ if extra_verbs = application.configuration.adapter_options[:extra_verbs]
22
22
  @extra_verbs = Set.new(extra_verbs.map(&:to_s).map(&:upcase))
23
23
  else
24
24
  @extra_verbs = Set.new
25
25
  end
26
26
 
27
- @server = ::Reel::Server.supervise(@options[:host], @options[:port], &method(:process))
27
+ if @options[:ssl]
28
+ unless @options[:ssl][:cert] && @options[:ssl][:key]
29
+ raise ArgumentError, 'Certificate or Private key missing for HTTPS Server'
30
+ end
31
+ @server = ::Reel::Server::HTTPS.supervise(@options[:host], @options[:port], @options[:ssl], &method(:process))
32
+ else
33
+ @server = ::Reel::Server::HTTP.supervise(@options[:host], @options[:port], &method(:process))
34
+ end
28
35
 
29
- # FIXME: this will no longer work on Ruby 2.0. We need Celluloid.trap
30
- trap("INT") { @server.terminate; exit 0 }
31
36
  Celluloid::Actor.join(@server)
32
37
  end
33
38
 
34
- def shutdown
35
- @server.terminate! if @server
36
- end
37
-
38
39
  def process(connection)
39
40
  connection.each_request do |request|
40
41
  # Users of the adapter can configure a custom WebSocket handler
@@ -54,7 +55,7 @@ module Webmachine
54
55
  # state machine. Do the "Railsy" thing and handle them like POSTs
55
56
  # with a magical parameter
56
57
  if @extra_verbs.include?(request.method)
57
- method = "POST"
58
+ method = POST_METHOD
58
59
  param = "_method=#{request.method}"
59
60
  uri = request_uri(request.url, request.headers, param)
60
61
  else
@@ -64,8 +65,9 @@ module Webmachine
64
65
 
65
66
  wm_headers = Webmachine::Headers[request.headers.dup]
66
67
  wm_request = Webmachine::Request.new(method, uri, wm_headers, request.body)
68
+
67
69
  wm_response = Webmachine::Response.new
68
- @dispatcher.dispatch(wm_request, wm_response)
70
+ application.dispatcher.dispatch(wm_request, wm_response)
69
71
 
70
72
  fixup_headers(wm_response)
71
73
  fixup_callable_encoder(wm_response)
@@ -77,13 +79,9 @@ module Webmachine
77
79
  end
78
80
 
79
81
  def request_uri(path, headers, extra_query_params = nil)
80
- host_parts = headers.fetch('Host').split(':')
81
82
  path_parts = path.split('?')
82
-
83
- uri_hash = {host: host_parts.first, path: path_parts.first}
84
-
85
- uri_hash[:port] = host_parts.last.to_i if host_parts.length == 2
86
- uri_hash[:query] = path_parts.last if path_parts.length == 2
83
+ uri_hash = {path: path_parts.first}
84
+ uri_hash[:query] = path_parts.last if path_parts.length == 2
87
85
 
88
86
  if extra_query_params
89
87
  if uri_hash[:query]