rack-router 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +2 -2
- data/Rakefile +8 -0
- data/config.ru +9 -0
- data/lib/rack-router.rb +1 -5
- data/lib/rack/route.rb +88 -0
- data/lib/rack/router.rb +81 -0
- data/rack-router.gemspec +1 -1
- data/test/route_test.rb +68 -0
- data/test/router_test.rb +26 -0
- metadata +9 -2
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Rack::Router
|
2
2
|
|
3
|
-
A router for rack apps
|
3
|
+
A simple router for rack apps
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -18,7 +18,7 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
|
21
|
+
See [config.ru](http://github.com/pjb3/rack-router/tree/master/config.ru)
|
22
22
|
|
23
23
|
## Contributing
|
24
24
|
|
data/Rakefile
CHANGED
data/config.ru
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'rack/router'
|
2
|
+
require 'rack/lobster'
|
3
|
+
|
4
|
+
router = Rack::Router.new do
|
5
|
+
get "/hello/:name" => proc{|env| [200, { "Content-Type" => "text/html" }, ["<h1>Hello, #{env['rack.route_params'][:name]}</h1>"] ] }
|
6
|
+
get "/lobster" => Rack::Lobster.new
|
7
|
+
end
|
8
|
+
|
9
|
+
run router
|
data/lib/rack-router.rb
CHANGED
data/lib/rack/route.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Rack
|
2
|
+
class Route
|
3
|
+
|
4
|
+
attr_accessor :pattern, :app, :constraints
|
5
|
+
|
6
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
7
|
+
DEFAULT_WILDCARD_NAME = :paths
|
8
|
+
WILDCARD_PATTERN = /\/\*(.*)/.freeze
|
9
|
+
NAMED_SEGMENTS_PATTERN = /\/:([^$\/]+)/.freeze
|
10
|
+
NAMED_SEGMENTS_REPLACEMENT_PATTERN = /\/:([^$\/]+)/.freeze
|
11
|
+
DOT = '.'.freeze
|
12
|
+
|
13
|
+
def initialize(pattern, app, constraints=nil)
|
14
|
+
if pattern.to_s.strip.empty?
|
15
|
+
raise ArgumentError.new("pattern cannot be blank")
|
16
|
+
end
|
17
|
+
|
18
|
+
unless app.respond_to?(:call)
|
19
|
+
raise ArgumentError.new("app must be callable")
|
20
|
+
end
|
21
|
+
|
22
|
+
@pattern = pattern
|
23
|
+
@app = app
|
24
|
+
@constraints = constraints
|
25
|
+
end
|
26
|
+
|
27
|
+
def regexp
|
28
|
+
@regexp ||= compile
|
29
|
+
end
|
30
|
+
|
31
|
+
def compile
|
32
|
+
src = if pattern_match = pattern.match(WILDCARD_PATTERN)
|
33
|
+
@wildcard_name = if pattern_match[1].to_s.strip.empty?
|
34
|
+
DEFAULT_WILDCARD_NAME
|
35
|
+
else
|
36
|
+
pattern_match[1].to_sym
|
37
|
+
end
|
38
|
+
pattern.gsub(WILDCARD_PATTERN,'(?:/(.*)|)')
|
39
|
+
elsif pattern_match = pattern.match(NAMED_SEGMENTS_PATTERN)
|
40
|
+
pattern.gsub(NAMED_SEGMENTS_REPLACEMENT_PATTERN, '/(?<\1>[^$/]+)')
|
41
|
+
else
|
42
|
+
pattern
|
43
|
+
end
|
44
|
+
Regexp.new("\\A#{src}\\Z")
|
45
|
+
end
|
46
|
+
|
47
|
+
def match(path)
|
48
|
+
if path.to_s.strip.empty?
|
49
|
+
raise ArgumentError.new("path is required")
|
50
|
+
end
|
51
|
+
|
52
|
+
if path_match = path.split(DOT).first.match(regexp)
|
53
|
+
params = if @wildcard_name
|
54
|
+
{ @wildcard_name => path_match[1].to_s.split('/') }
|
55
|
+
else
|
56
|
+
Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
|
57
|
+
end
|
58
|
+
|
59
|
+
if meets_constraints(params)
|
60
|
+
params
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def meets_constraints(params)
|
66
|
+
if constraints
|
67
|
+
constraints.each do |param, constraint|
|
68
|
+
unless params[param].to_s.match(constraint)
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def eql?(o)
|
77
|
+
o.is_a?(self.class) &&
|
78
|
+
o.pattern == pattern &&
|
79
|
+
o.app == app &&
|
80
|
+
o.constraints == constraints
|
81
|
+
end
|
82
|
+
alias == eql?
|
83
|
+
|
84
|
+
def hash
|
85
|
+
pattern.hash ^ app.hash ^ constraints.hash
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/rack/router.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rack/route'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
class Router
|
6
|
+
|
7
|
+
HEAD = 'HEAD'.freeze
|
8
|
+
GET = 'GET'.freeze
|
9
|
+
POST = 'POST'.freeze
|
10
|
+
PUT = 'PUT'.freeze
|
11
|
+
DELETE = 'DELETE'.freeze
|
12
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
13
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
14
|
+
ROUTE_PARAMS = 'rack.route_params'.freeze
|
15
|
+
DEFAULT_NOT_FOUND_BODY = '<h1>Not Found</h1>'.freeze
|
16
|
+
DEFAULT_NOT_FOUND_RESPONSE = [404,
|
17
|
+
{
|
18
|
+
"Content-Type" => "text/html",
|
19
|
+
"Content-Length" => DEFAULT_NOT_FOUND_BODY.length.to_s
|
20
|
+
}, [DEFAULT_NOT_FOUND_BODY]]
|
21
|
+
|
22
|
+
def initialize(&block)
|
23
|
+
@routes = {}
|
24
|
+
routes(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def routes(&block)
|
28
|
+
instance_eval(&block) if block
|
29
|
+
@routes
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(route_spec)
|
33
|
+
route(GET, route_spec)
|
34
|
+
end
|
35
|
+
|
36
|
+
def post(route_spec)
|
37
|
+
route(POST, route_spec)
|
38
|
+
end
|
39
|
+
|
40
|
+
def put(route_spec)
|
41
|
+
route(PUT, route_spec)
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete(route_spec)
|
45
|
+
route(DELETE, route_spec)
|
46
|
+
end
|
47
|
+
|
48
|
+
def route(method, route_spec)
|
49
|
+
route = Route.new(route_spec.first.first, route_spec.first.last)
|
50
|
+
@routes[method] ||= []
|
51
|
+
@routes[method] << route
|
52
|
+
route
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(env)
|
56
|
+
if app = match(env)
|
57
|
+
app.call(env)
|
58
|
+
else
|
59
|
+
not_found(env)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def match(env)
|
64
|
+
request_method = env[REQUEST_METHOD]
|
65
|
+
request_method = GET if request_method == HEAD
|
66
|
+
if method_routes = @routes[request_method]
|
67
|
+
method_routes.each do |route|
|
68
|
+
if params = route.match(env[PATH_INFO])
|
69
|
+
env[ROUTE_PARAMS] = params
|
70
|
+
return route.app
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def not_found(env)
|
77
|
+
DEFAULT_NOT_FOUND_RESPONSE
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
data/rack-router.gemspec
CHANGED
data/test/route_test.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "rack/route"
|
3
|
+
|
4
|
+
class RouteTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_match
|
7
|
+
match "/*" , "/" , :paths => []
|
8
|
+
match "/*" , "/foo" , :paths => %w[foo]
|
9
|
+
match "/*" , "/foo/bar/baz" , :paths => %w[foo bar baz]
|
10
|
+
match "/*stuff" , "/" , :stuff => []
|
11
|
+
match "/*stuff" , "/foo" , :stuff => %w[foo]
|
12
|
+
match "/*stuff" , "/foo/bar/baz" , :stuff => %w[foo bar baz]
|
13
|
+
match "/foo/*" , "/" , nil
|
14
|
+
match "/foo/*" , "/foo" , :paths => []
|
15
|
+
match "/foo/*" , "/foo/bar/baz" , :paths => %w[bar baz]
|
16
|
+
match "/foo/*stuff", "/" , nil
|
17
|
+
match "/foo/*stuff", "/foo" , :stuff => []
|
18
|
+
match "/foo/*stuff", "/foo/bar/baz" , :stuff => %w[bar baz]
|
19
|
+
match "/" , "/" , {}
|
20
|
+
match "/" , "/foo" , nil
|
21
|
+
match "/foo" , "/" , nil
|
22
|
+
match "/foo" , "/foo" , {}
|
23
|
+
match "/:id" , "/42" , { :id => "42" }
|
24
|
+
match "/:id" , "/" , nil
|
25
|
+
match "/posts/:id" , "/posts/42" , { :id => "42" }
|
26
|
+
match "/posts/:id" , "/posts" , nil
|
27
|
+
match "/:x/:y" , "/a/b" , { :x => "a" , :y => "b" }
|
28
|
+
match "/posts/:id" , "/posts/42.html", { :id => "42" }
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_match_with_constraints
|
32
|
+
r = route("/posts/:year/:month/:day/:slug",
|
33
|
+
:year => /\A\d{4}\Z/,
|
34
|
+
:month => /\A\d{1,2}\Z/,
|
35
|
+
:day => /\A\d{1,2}\Z/)
|
36
|
+
assert_equal({
|
37
|
+
:year => "2012",
|
38
|
+
:month => "9",
|
39
|
+
:day => "20",
|
40
|
+
:slug => "test"
|
41
|
+
}, r.match("/posts/2012/9/20/test"))
|
42
|
+
assert_equal(nil, r.match("/posts/2012/9/20"))
|
43
|
+
assert_equal(nil, r.match("/posts/2012/x/20/test"))
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_eql
|
47
|
+
app = lambda{|env| [200, {}, [""]] }
|
48
|
+
assert_equal(
|
49
|
+
Rack::Route.new("/", app),
|
50
|
+
Rack::Route.new("/", app))
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def route(path, constraints=nil)
|
55
|
+
Rack::Route.new(path, lambda{|env| [200, {}, [""]] }, constraints)
|
56
|
+
end
|
57
|
+
|
58
|
+
def match(pattern, path, params)
|
59
|
+
msg = "#{caller[0]} expected route #{pattern} to "
|
60
|
+
if params
|
61
|
+
msg << "match #{path} and return #{params.inspect}"
|
62
|
+
else
|
63
|
+
msg << "no match #{path}"
|
64
|
+
end
|
65
|
+
assert_equal(params, route(pattern).match(path), msg)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
data/test/router_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rack/router'
|
3
|
+
|
4
|
+
class RouterTest < Test::Unit::TestCase
|
5
|
+
def test_call
|
6
|
+
app1 = lambda{|env| [200, {}, [env["rack.route_params"][:id]]] }
|
7
|
+
app2 = lambda{|env| [200, {}, ["2"]] }
|
8
|
+
|
9
|
+
router = Rack::Router.new do
|
10
|
+
post "/stuff" => app2
|
11
|
+
put "/it" => app2
|
12
|
+
delete "/remove" => app2
|
13
|
+
get "/:id" => app1
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_equal({
|
17
|
+
"POST" => [Rack::Route.new("/stuff", app2)],
|
18
|
+
"PUT" => [Rack::Route.new("/it", app2)],
|
19
|
+
"DELETE" => [Rack::Route.new("/remove", app2)],
|
20
|
+
"GET" => [Rack::Route.new("/:id", app1)]
|
21
|
+
}, router.routes)
|
22
|
+
|
23
|
+
assert_equal ["42"], router.call("REQUEST_METHOD" => "GET", "PATH_INFO" => "/42").last
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-router
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -23,8 +23,13 @@ files:
|
|
23
23
|
- LICENSE
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
|
+
- config.ru
|
26
27
|
- lib/rack-router.rb
|
28
|
+
- lib/rack/route.rb
|
29
|
+
- lib/rack/router.rb
|
27
30
|
- rack-router.gemspec
|
31
|
+
- test/route_test.rb
|
32
|
+
- test/router_test.rb
|
28
33
|
homepage: https://github.com/pjb3/rack-router
|
29
34
|
licenses: []
|
30
35
|
post_install_message:
|
@@ -49,4 +54,6 @@ rubygems_version: 1.8.24
|
|
49
54
|
signing_key:
|
50
55
|
specification_version: 3
|
51
56
|
summary: A simple router for rack apps
|
52
|
-
test_files:
|
57
|
+
test_files:
|
58
|
+
- test/route_test.rb
|
59
|
+
- test/router_test.rb
|