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 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