Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support for named parameters in URLMap (replaces #343) #372

Closed
wants to merge 3 commits into from

4 participants

@ta
ta commented

Added support for sinatra-like named parameters in URLMap, which can be useful when working with scoped resources.

Note: Moved from master to a topic branch and replaces pull request #343 - a lesson learned

@gioele

Named groups are not available in Ruby 1.8.x

Fix committed in 5258bc7

@raggi
Owner

Might I ask why you wouldn't just use Sinatra for this?

@rkh
Owner

Or any other Rack router that's out there. This opens up a ton of issues otherwise.

@raggi
Owner

Thank you for your patch. I'm afraid that we're not accepting this one on the grounds that we'd like to keep URLMap very simple, and keep our edge cases low. You might consider creating a simple gem that implements this instead, or using Sinatra.

Thanks again!

@raggi raggi closed this
@ta
ta commented

You are most welcome! Sinatra wasn't a good fit for my project at hand.

@rkh
Owner

There are a ton of other routers out there.

@ta
ta commented

@rkh: Thank you!

@ta
ta commented

@raggi: Just wanted to explain my use case. With my "named parameters" patch to URLMap I can re-use my apps in different scopes like this:

class Articles < Sinatra::Base
  get "/:id/?" do; ...; end
  get "/?" do; ...; end.
end

class Comments < Sinatra::Base
  # Handle scope based on rack.url_params
  get "/:id/?" do; ...; end
  get "/?" do; ...; end
end

map "/" do
  map "/articles" do
    map "/:article_id/comments" do
      run Comments.new
    end
    run Articles.new
  end
  map "/comments" do
    run Comments.new
  end
end

I guess the "normal" way is to write something like this code below (which might not seem too bad, but if I were to add some classes like Tags etc., it would quickly become a mess with lots of code repetition across the apps).

class Articles < Sinatra::Base
  get "/:id/comments/:id/?" do; ...; end
  get "/:id/comments/?" do; ...; end
  get "/:id/?" do; ...; end
  get "/?" do; ...; end.
end

class Comments < Sinatra::Base
  get "/:id/?" do; ...; end
  get "/?" do; ...; end
end

map "/" do
  map "/articles" do
    run Articles.new
  end
  map "/comments" do
    run Comments.new
  end
end

I can't find any other router/middleware/gem (as @rkh suggests) that can handle my use case. If you have some pointers on how (if possible) to transform my patch into some middleware/gem, I would really appreciate it! Thanks.

@raggi
Owner

Seems like the kind of thing that Rails is really good at....

@rkh
Owner
class Comments < Sinatra::Base
  # ...
end

class Articles < Sinatra::Base
  # ...
  get '/:id/comments/*' do
    Comments.call(env)
  end
end

map('/articles') { run Articles }
map('/comments') { run Comments }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 86 additions and 7 deletions.
  1. +15 −7 lib/rack/urlmap.rb
  2. +71 −0 test/spec_urlmap.rb
View
22 lib/rack/urlmap.rb
@@ -20,6 +20,7 @@ def initialize(map = {})
def remap(map)
@mapping = map.map { |location, app|
+ keys = []
if location =~ %r{\Ahttps?://(.*?)(/.*)}
host, location = $1, $2
else
@@ -31,11 +32,17 @@ def remap(map)
end
location = location.chomp('/')
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
- [host, location, match, app]
- }.sort_by do |(host, location, _, _)|
- [host ? -host.size : NEGATIVE_INFINITY, -location.size]
+ pattern = "^#{Regexp.quote(location).gsub('/', '/+')}(.*)"
+ pattern = pattern.gsub(/((:\w+))/) do |match|
+ keys << $2[1..-1]
+ "([^/?#]+)"
+ end
+ pattern = Regexp.new(pattern, nil, 'n')
+
+ [host, location, pattern, app, keys]
+ }.sort_by do |(host, location, _, _, _)|
+ [host ? -host.size : NEGATIVE_INFINITY, -location.gsub(/:\w+\/?/, "").size]
end
end
@@ -46,20 +53,21 @@ def call(env)
sName = env['SERVER_NAME']
sPort = env['SERVER_PORT']
- @mapping.each do |host, location, match, app|
+ @mapping.each do |host, location, pattern, app, keys|
unless hHost == host \
|| sName == host \
|| (!host && (hHost == sName || hHost == sName+':'+sPort))
next
end
- next unless m = match.match(path.to_s)
+ next unless m = pattern.match(path.to_s)
- rest = m[1]
+ rest = m.values_at(-1).first
next unless !rest || rest.empty? || rest[0] == ?/
env['SCRIPT_NAME'] = (script_name + location)
env['PATH_INFO'] = rest
+ env['rack.url_params'] = Hash[*keys.collect!{|x| x.to_sym}.zip(m.values_at(1..-2)).flatten]
return app.call(env)
end
View
71 test/spec_urlmap.rb
@@ -210,4 +210,75 @@
res["X-PathInfo"].should.equal "/http://example.org/bar"
res["X-ScriptName"].should.equal ""
end
+
+ should "handle named parameters in URL" do
+ app = lambda { |env|
+ [200, {
+ "X-URLParams" => env["rack.url_params"],
+ "Content-Type" => "text/plain"
+ }, [""]]
+ }
+ map = Rack::URLMap.new({
+ "/foo" => app,
+ "/foo/:bar" => app,
+ "/foo/:bar/baz" => app,
+ "/foo/:bar/baz/:qux" => app
+ })
+
+ res = Rack::MockRequest.new(map).get("/foo")
+ res.should.be.ok
+ res["X-URLParams"].should.equal({})
+
+ res = Rack::MockRequest.new(map).get("/foo/2")
+ res.should.be.ok
+ res["X-URLParams"].should.equal({:bar=>"2"})
+
+ res = Rack::MockRequest.new(map).get("/foo/2/baz")
+ res.should.be.ok
+ res["X-URLParams"].should.equal({:bar=>"2"})
+
+ res = Rack::MockRequest.new(map).get("/foo/2/baz/four")
+ res.should.be.ok
+ res["X-URLParams"].should.equal({:bar=>"2",:qux=>"four"})
+ end
+
+ should "prioritize named parameters in URL correctly" do
+ app1 = lambda { |env|
+ [200, {
+ "Content-Type" => "text/plain"
+ }, ["app1"]]
+ }
+ app2 = lambda { |env|
+ [200, {
+ "Content-Type" => "text/plain"
+ }, ["app2"]]
+ }
+
+ map1 = Rack::URLMap.new({
+ "/foo/baaaaar" => app1,
+ "/foo/:short" => app2
+ })
+
+ map2 = Rack::URLMap.new({
+ "/foo/baaaaar" => app1,
+ "/foo/:looooooooong" => app2
+ })
+
+ res = Rack::MockRequest.new(map1).get("/foo/baaaaar")
+ res.should.be.ok
+ res.body.should.equal 'app1' # <= as expected
+
+ res = Rack::MockRequest.new(map1).get("/foo/not-bar")
+ res.should.be.ok
+ res.body.should.equal 'app2' # <= as expected
+
+ res = Rack::MockRequest.new(map2).get("/foo/baaaaar")
+ res.should.be.ok
+ res.body.should.equal 'app1' # <= fails!
+
+ res = Rack::MockRequest.new(map2).get("/foo/not-bar")
+ res.should.be.ok
+ res.body.should.equal 'app2' # <= as expected
+
+ end
end
Something went wrong with that request. Please try again.