Skip to content

Commit c17039f

Browse files
committed
Merge pull request #223 from madlep/rack_mapped
Support webmachine being mapped inside a parent rack app
2 parents e92f399 + b929920 commit c17039f

File tree

7 files changed

+201
-39
lines changed

7 files changed

+201
-39
lines changed

documentation/adapters.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ run on any webserver that provides a Rack interface. It also lets it run on
1010
In order to be compatible with popular deployment stacks,
1111
Webmachine has a [Rack](https://github.com/rack/rack) adapter (thanks to Jamis Buck).
1212

13-
Webmachine can be used with Rack middlware features such as Rack::Map and Rack::Cascade as long as:
14-
15-
1. The Webmachine app is mounted at the root directory.
16-
2. Any requests/responses that are handled by the Webmachine app are not modified by the middleware. The behaviours that are encapsulated in Webmachine assume that no modifications
13+
Webmachine can be used with Rack middlware features such as Rack::Map and Rack::Cascade as long as any requests/responses that are handled by the Webmachine app are **not** modified by the middleware. The behaviours that are encapsulated in Webmachine assume that no modifications
1714
are done to requests or response outside of Webmachine.
1815

1916
Keep in mind that Webmachine already supports many things that Rack middleware is used for with other HTTP frameworks (eg. etags, specifying supported/preferred Accept and Content-Types).
2017

18+
The base `Webmachine::Adapters::Rack` class assumes the Webmachine application
19+
is mounted at the route path `/` (i.e. not using `Rack::Builder#map` or Rails
20+
`ActionDispatch::Routing::Mapper::Base#mount`). In order to
21+
map to a subpath, use the `Webmachine::Adapters::RackMapped` adapter instead.
22+
2123
For an example of using Webmachine with Rack middleware, see the [Pact Broker][middleware-example].
2224

2325
See the [Rack Adapter API docs][rack-adapter-api-docs] for more information.

lib/webmachine/adapters/rack.rb

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ module Adapters
1212
# A minimal "shim" adapter to allow Webmachine to interface with Rack. The
1313
# intention here is to allow Webmachine to run under Rack-compatible
1414
# web-servers, like unicorn and pow.
15+
#
1516
# The adapter expects your Webmachine application to be mounted at the root path -
1617
# it will NOT allow you to nest your Webmachine application at an arbitrary path
1718
# eg. map "/api" { run MyWebmachineAPI }
19+
# To use map your Webmachine application at an arbitrary path, use the
20+
# `Webmachine::Adapters::RackMapped` subclass instead.
1821
#
1922
# To use this adapter, create a config.ru file and populate it like so:
2023
#
@@ -59,10 +62,7 @@ def call(env)
5962
headers = Webmachine::Headers.from_cgi(env)
6063

6164
rack_req = ::Rack::Request.new env
62-
request = Webmachine::Request.new(rack_req.request_method,
63-
rack_req.url,
64-
headers,
65-
RequestBody.new(rack_req))
65+
request = build_webmachine_request(rack_req, headers)
6666

6767
response = Webmachine::Response.new
6868
application.dispatcher.dispatch(request, response)
@@ -95,6 +95,26 @@ def call(env)
9595
rack_res.finish
9696
end
9797

98+
protected
99+
def routing_tokens(rack_req)
100+
nil # no-op for default, un-mapped rack adapter
101+
end
102+
103+
def base_uri(rack_req)
104+
nil # no-op for default, un-mapped rack adapter
105+
end
106+
107+
private
108+
def build_webmachine_request(rack_req, headers)
109+
Webmachine::Request.new(rack_req.request_method,
110+
rack_req.url,
111+
headers,
112+
RequestBody.new(rack_req),
113+
routing_tokens(rack_req),
114+
base_uri(rack_req)
115+
)
116+
end
117+
98118
class RackResponse
99119
ONE_FIVE = '1.5'.freeze
100120

@@ -167,5 +187,40 @@ def each
167187
end # class RequestBody
168188
end # class Rack
169189

190+
# Provides the same functionality as the parent Webmachine::Adapters::Rack
191+
# adapter, but allows the Webmachine application to be hosted at an
192+
# arbitrary path in a parent Rack application (as in Rack `map` or Rails
193+
# routing `mount`)
194+
#
195+
# This functionality is separated out from the parent class to preserve
196+
# backward compatibility in the behaviour of the parent Rack adpater.
197+
#
198+
# To use the adapter in a parent Rack application, map the Webmachine
199+
# application as follows in a rackup file or Rack::Builder:
200+
#
201+
# map '/foo' do
202+
# run SomeotherRackApp
203+
#
204+
# map '/bar' do
205+
# run MyWebmachineApp.adapter
206+
# end
207+
# end
208+
class RackMapped < Rack
209+
protected
210+
def routing_tokens(rack_req)
211+
routing_match = rack_req.path_info.match(Webmachine::Request::ROUTING_PATH_MATCH)
212+
routing_path = routing_match ? routing_match[1] : ""
213+
routing_path.split(SLASH)
214+
end
215+
216+
def base_uri(rack_req)
217+
# rack SCRIPT_NAME env var doesn't end with "/". This causes weird
218+
# behavour when URI.join concatenates URI components in
219+
# Webmachine::Decision::Flow#n11
220+
script_name = rack_req.script_name + SLASH
221+
URI.join(rack_req.base_url, script_name)
222+
end
223+
end
224+
170225
end # module Adapters
171226
end # module Webmachine

lib/webmachine/dispatcher/route.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,21 @@ def initialize(path_spec, *args)
7979
raise ArgumentError, t('not_resource_class', :class => resource.name) unless resource < Resource
8080
end
8181

82-
PATH_MATCH = /^\/(.*)/.freeze
83-
8482
# Determines whether the given request matches this route and
8583
# should be dispatched to the {#resource}.
8684
# @param [Reqeust] request the request object
8785
def match?(request)
88-
tokens = request.uri.path.match(PATH_MATCH)[1].split(SLASH)
86+
tokens = request.routing_tokens
8987
bind(tokens, {}) && guards.all? { |guard| guard.call(request) }
9088
end
9189

9290
# Decorates the request with information about the dispatch
9391
# route, including path bindings.
9492
# @param [Request] request the request object
9593
def apply(request)
96-
request.disp_path = request.uri.path.match(PATH_MATCH)[1]
94+
request.disp_path = request.routing_tokens.join(SLASH)
9795
request.path_info = @bindings.dup
98-
tokens = request.disp_path.split(SLASH)
96+
tokens = request.routing_tokens
9997
depth, trailing = bind(tokens, request.path_info)
10098
request.path_tokens = trailing || []
10199
end

lib/webmachine/request.rb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ module Webmachine
88
# should be instantiated by {Adapters} when a request is received
99
class Request
1010
HTTP_HEADERS_MATCH = /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i.freeze
11+
ROUTING_PATH_MATCH = /^\/(.*)/.freeze
1112

1213
extend Forwardable
1314

14-
attr_reader :method, :uri, :headers, :body
15+
attr_reader :method, :uri, :headers, :body, :routing_tokens, :base_uri
1516
attr_accessor :disp_path, :path_info, :path_tokens
1617

1718
# @param [String] method the HTTP request method
@@ -20,9 +21,14 @@ class Request
2021
# @param [Headers] headers the HTTP request headers
2122
# @param [String,#to_s,#each,nil] body the entity included in the
2223
# request, if present
23-
def initialize(method, uri, headers, body)
24+
def initialize(method, uri, headers, body, routing_tokens=nil, base_uri=nil)
2425
@method, @headers, @body = method, headers, body
2526
@uri = build_uri(uri, headers)
27+
@routing_tokens = routing_tokens || @uri.path.match(ROUTING_PATH_MATCH)[1].split(SLASH)
28+
@base_uri = base_uri || @uri.dup.tap do |u|
29+
u.path = SLASH
30+
u.query = nil
31+
end
2632
end
2733

2834
def_delegators :headers, :[]
@@ -53,16 +59,6 @@ def has_body?
5359
!(body.nil? || body.empty?)
5460
end
5561

56-
# The root URI for the request, ignoring path and query. This is
57-
# useful for calculating relative paths to resources.
58-
# @return [URI]
59-
def base_uri
60-
@base_uri ||= uri.dup.tap do |u|
61-
u.path = SLASH
62-
u.query = nil
63-
end
64-
end
65-
6662
# Returns a hash of query parameters (they come after the ? in the
6763
# URI). Note that this does NOT work in the same way as Rails,
6864
# i.e. it does not support nested arrays and hashes.

spec/webmachine/adapters/rack_spec.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
end
1515
end
1616

17+
describe Webmachine::Adapters::RackMapped do
18+
it_should_behave_like :adapter_lint do
19+
it "should set Server header" do
20+
response = client.request(Net::HTTP::Get.new("/test"))
21+
expect(response["Server"]).to match(/Webmachine/)
22+
expect(response["Server"]).to match(/Rack/)
23+
end
24+
end
25+
end
26+
1727
describe Webmachine::Adapters::Rack::RackResponse do
1828
context "on Rack < 1.5 release" do
1929
before { allow(Rack).to receive_messages(:release => "1.4") }
@@ -55,3 +65,59 @@
5565
end
5666
end
5767
end
68+
69+
describe Webmachine::Adapters::RackMapped do
70+
class CreateResource < Webmachine::Resource
71+
def allowed_methods
72+
["POST"]
73+
end
74+
75+
def content_types_accepted
76+
[["application/json", :from_json]]
77+
end
78+
79+
def content_types_provided
80+
[["application/json", :to_json]]
81+
end
82+
83+
def post_is_create?
84+
true
85+
end
86+
87+
def create_path
88+
"created_path_here/123"
89+
end
90+
91+
def from_json
92+
response.body = %{ {"foo": "bar"} }
93+
end
94+
end
95+
96+
let(:app) do
97+
Rack::Builder.new do
98+
map '/some/route' do
99+
run(Webmachine::Application.new do |app|
100+
app.add_route(["test"], Test::Resource)
101+
app.add_route(["create_test"], CreateResource)
102+
app.configure do | config |
103+
config.adapter = :RackMapped
104+
end
105+
end.adapter)
106+
end
107+
end
108+
end
109+
110+
context "using Rack::Test" do
111+
include Rack::Test::Methods
112+
113+
it "provides the full request URI" do
114+
rack_response = get "some/route/test", nil, {"HTTP_ACCEPT" => "test/response.request_uri"}
115+
expect(rack_response.body).to eq "http://example.org/some/route/test"
116+
end
117+
118+
it "provides LOCATION header using custom base_uri when creating from POST request" do
119+
rack_response = post "/some/route/create_test", %{{"foo": "bar"}}, {"HTTP_ACCEPT" => "application/json", "CONTENT_TYPE" => "application/json"}
120+
expect(rack_response.headers["Location"]).to eq("http://example.org/some/route/created_path_here/123")
121+
end
122+
end
123+
end

spec/webmachine/dispatcher/route_spec.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ def warn(*msgs); end # silence warnings for tests
77
describe Webmachine::Dispatcher::Route do
88
let(:method) { "GET" }
99
let(:uri) { URI.parse("http://localhost:8080/") }
10-
let(:request){ Webmachine::Request.new(method, uri, Webmachine::Headers.new, "") }
10+
let(:routing_tokens) { nil }
11+
let(:request){ Webmachine::Request.new(method, uri, Webmachine::Headers.new, "", routing_tokens) }
1112
let(:resource){ Class.new(Webmachine::Resource) }
1213

1314
describe '#apply' do
@@ -16,9 +17,7 @@ def warn(*msgs); end # silence warnings for tests
1617
}
1718

1819
describe 'a path_info fragment' do
19-
before do
20-
uri.path = '/hello/planet%20earth%20++'
21-
end
20+
let(:uri) { URI.parse("http://localhost:8080/hello/planet%20earth%20++") }
2221

2322
it 'should decode the value' do
2423
route.apply(request)
@@ -30,8 +29,10 @@ def warn(*msgs); end # silence warnings for tests
3029
matcher :match_route do |*expected|
3130
route = Webmachine::Dispatcher::Route.new(expected[0], Class.new(Webmachine::Resource), expected[1] || {})
3231
match do |actual|
33-
request.uri.path = actual if String === actual
34-
route.match?(request)
32+
uri = URI.parse("http://localhost:8080")
33+
uri.path = actual
34+
req = Webmachine::Request.new("GET", uri, Webmachine::Headers.new, "", routing_tokens)
35+
route.match?(req)
3536
end
3637

3738
failure_message do |_|
@@ -124,6 +125,18 @@ def call(request)
124125
end
125126
end
126127
end
128+
129+
context "with a request with explicitly specified routing tokens" do
130+
subject { "/some/route/foo/bar" }
131+
let(:routing_tokens) { ["foo", "bar"] }
132+
it { is_expected.to match_route(["foo", "bar"]) }
133+
it { is_expected.to match_route(["foo", :id]) }
134+
it { is_expected.to match_route ['*'] }
135+
it { is_expected.to match_route [:*] }
136+
it { is_expected.not_to match_route(["some", "route", "foo", "bar"]) }
137+
it { is_expected.not_to match_route %w{foo} }
138+
it { is_expected.not_to match_route [:id] }
139+
end
127140
end
128141

129142
context "applying bindings" do
@@ -170,7 +183,8 @@ def call(request)
170183

171184
context "on a deep path" do
172185
subject { described_class.new(%w{foo bar baz}, resource) }
173-
before { request.uri.path = "/foo/bar/baz"; subject.apply(request) }
186+
let(:uri) { URI.parse("http://localhost:8080/foo/bar/baz") }
187+
before { subject.apply(request) }
174188

175189
it "should assign the dispatched path as the path past the initial slash" do
176190
expect(request.disp_path).to eq("foo/bar/baz")

spec/webmachine/request_spec.rb

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
describe Webmachine::Request do
44
subject { request }
55

6-
let(:uri) { URI.parse("http://localhost:8080/some/resource") }
7-
let(:http_method) { "GET" }
8-
let(:headers) { Webmachine::Headers.new }
9-
let(:body) { "" }
10-
let(:request) { Webmachine::Request.new(http_method, uri, headers, body) }
6+
let(:uri) { URI.parse("http://localhost:8080/some/resource") }
7+
let(:http_method) { "GET" }
8+
let(:headers) { Webmachine::Headers.new }
9+
let(:body) { "" }
10+
let(:routing_tokens) { nil }
11+
let(:base_uri) { nil }
12+
let(:request) { Webmachine::Request.new(http_method, uri, headers, body, routing_tokens, base_uri) }
1113

1214
it "should provide access to the headers via brackets" do
1315
subject.headers['Accept'] = "*/*"
@@ -30,8 +32,17 @@
3032
expect(subject.content_md5).to be_nil
3133
end
3234

33-
it "should calculate a base URI" do
34-
expect(subject.base_uri).to eq(URI.parse("http://localhost:8080/"))
35+
context "base_uri" do
36+
it "should calculate a base URI" do
37+
expect(subject.base_uri).to eq(URI.parse("http://localhost:8080/"))
38+
end
39+
40+
context "when base_uri has been explicitly set" do
41+
let(:base_uri) { URI.parse("http://localhost:8080/some_base_uri/here") }
42+
it "should use the provided base_uri" do
43+
expect(subject.base_uri).to eq(URI.parse("http://localhost:8080/some_base_uri/here"))
44+
end
45+
end
3546
end
3647

3748
it "should provide a hash of query parameters" do
@@ -239,4 +250,24 @@ def body; block_given? ? yield(@body) : @body; end
239250
end
240251
end
241252

253+
describe '#routing_tokens' do
254+
subject { request.routing_tokens }
255+
256+
context "haven't been explicitly set" do
257+
let(:routing_tokens) { nil }
258+
it "extracts the routing tokens from the path portion of the uri" do
259+
expect(subject).to eq(["some", "resource"])
260+
end
261+
end
262+
263+
context "have been explicitly set" do
264+
let(:routing_tokens) { ["foo", "bar"] }
265+
266+
it "uses the specified routing_tokens" do
267+
expect(subject).to eq(["foo", "bar"])
268+
end
269+
end
270+
271+
end
272+
242273
end

0 commit comments

Comments
 (0)