rack-router 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
- Coming soon...
21
+ See [config.ru](http://github.com/pjb3/rack-router/tree/master/config.ru)
22
22
 
23
23
  ## Contributing
24
24
 
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs = ["lib"]
7
+ t.test_files = FileList["test/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
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
@@ -1,5 +1 @@
1
- module Rack
2
- module Router
3
- # Your code goes here...
4
- end
5
- end
1
+ require 'rack/router'
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
@@ -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
@@ -11,5 +11,5 @@ Gem::Specification.new do |gem|
11
11
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
12
12
  gem.name = "rack-router"
13
13
  gem.require_paths = ["lib"]
14
- gem.version = "0.0.1"
14
+ gem.version = "0.1.0"
15
15
  end
@@ -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
+
@@ -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.1
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