rutter 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +14 -4
- data/.travis.yml +12 -2
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +17 -102
- data/Rakefile +7 -1
- data/bench/config.ru +10 -9
- data/lib/rutter.rb +10 -7
- data/lib/rutter/builder.rb +180 -221
- data/lib/rutter/mount.rb +21 -0
- data/lib/rutter/naming.rb +95 -0
- data/lib/rutter/route.rb +69 -126
- data/lib/rutter/routes.rb +36 -9
- data/lib/rutter/scope.rb +39 -40
- data/lib/rutter/verbs.rb +8 -0
- data/lib/rutter/version.rb +1 -1
- data/rutter.gemspec +4 -3
- data/spec/integration/rack_spec.rb +21 -13
- data/spec/spec_helper.rb +6 -48
- data/spec/support/rack.rb +20 -0
- data/spec/unit/builder_spec.rb +86 -74
- data/spec/unit/namespace_spec.rb +26 -0
- data/spec/unit/route_spec.rb +33 -50
- data/spec/unit/routes_spec.rb +20 -11
- data/spec/unit/rutter_spec.rb +3 -2
- data/spec/unit/scope_spec.rb +49 -56
- metadata +25 -17
- data/bench/dynamic_routes +0 -20
- data/bench/expand +0 -19
- data/bench/helper.rb +0 -19
- data/bench/mount +0 -32
- data/bench/routes_helper +0 -24
- data/bench/static_routes +0 -20
- data/spec/integration/mount_spec.rb +0 -18
- data/spec/integration/params_spec.rb +0 -28
- data/spec/integration/redirect_spec.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 68f5fb5ae85793cb16d135928e3a26cf043a3eda09338f8c2a03117a16985b2d
|
4
|
+
data.tar.gz: c9766d83b10a8b6f103f26cc100ca753ecace464770fb739eccd4ecb2f2a876e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b0bcaae9355facffd058bf84021a4d62d1365312c254fd4e2ead08b6d907c68791572f47dc3451d35fcda32fb777ff3c94c5785b270cc290187aef3ce3e3260
|
7
|
+
data.tar.gz: 10e3b47288658c8dd7068447339fa54981684e225542c4c05ac7440dc2fa3f2112c8461ac849b855e16c740e4357d0f7e673a49eabac946a762c21b028882d3a
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,14 +1,21 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
|
1
4
|
AllCops:
|
2
|
-
TargetRubyVersion: 2.
|
5
|
+
TargetRubyVersion: 2.5
|
3
6
|
DisplayCopNames: true
|
4
7
|
Exclude:
|
5
|
-
-
|
8
|
+
- "*.gemspec"
|
9
|
+
- "Gemfile"
|
10
|
+
|
11
|
+
Performance:
|
12
|
+
Enabled: true
|
6
13
|
|
7
14
|
Style/StringLiterals:
|
8
15
|
EnforcedStyle: double_quotes
|
9
16
|
|
10
|
-
|
11
|
-
|
17
|
+
Layout/EmptyLineAfterGuardClause:
|
18
|
+
Enabled: false
|
12
19
|
|
13
20
|
Metrics/MethodLength:
|
14
21
|
Max: 15
|
@@ -23,3 +30,6 @@ Metrics/ModuleLength:
|
|
23
30
|
|
24
31
|
ClassAndModuleChildren:
|
25
32
|
Enabled: false
|
33
|
+
|
34
|
+
Naming/UncommunicativeMethodParamName:
|
35
|
+
MinNameLength: 2
|
data/.travis.yml
CHANGED
@@ -1,19 +1,29 @@
|
|
1
1
|
language: ruby
|
2
2
|
script: "bundle exec rake spec:coverage"
|
3
3
|
cache: bundler
|
4
|
+
env:
|
5
|
+
global:
|
6
|
+
- CC_TEST_REPORTER_ID=c1b17cd36cd4d025298f11b2a3ee678e4977c3969986f9ee497d6984a2c82c56
|
4
7
|
before_install:
|
5
8
|
- "gem update --system"
|
9
|
+
before_script:
|
10
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
11
|
+
- chmod +x ./cc-test-reporter
|
12
|
+
- ./cc-test-reporter before-build
|
6
13
|
after_script:
|
7
|
-
-
|
14
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
8
15
|
rvm:
|
16
|
+
- 2.6.0
|
9
17
|
- 2.5.0
|
10
|
-
- 2.
|
18
|
+
- jruby-9.2.0.0
|
11
19
|
- ruby-head
|
12
20
|
- jruby-head
|
21
|
+
- truffleruby
|
13
22
|
matrix:
|
14
23
|
allow_failures:
|
15
24
|
- rvm: jruby-head
|
16
25
|
- rvm: ruby-head
|
26
|
+
- rvm: truffleruby
|
17
27
|
branches:
|
18
28
|
only:
|
19
29
|
- master
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,8 +4,11 @@ HTTP router for Rack.
|
|
4
4
|
|
5
5
|
## Status
|
6
6
|
|
7
|
+
Under development, not ready for prime-time just yet.
|
8
|
+
|
7
9
|
[![Build Status](https://travis-ci.org/sandelius/rutter.svg?branch=master)](https://travis-ci.org/sandelius/rutter)
|
8
10
|
[![Test Coverage](https://codeclimate.com/github/sandelius/rutter/badges/coverage.svg)](https://codeclimate.com/github/sandelius/rutter/coverage)
|
11
|
+
[![Inline docs](http://inch-ci.org/github/sandelius/rutter.svg?branch=master)](http://inch-ci.org/github/sandelius/rutter)
|
9
12
|
|
10
13
|
## Installation
|
11
14
|
|
@@ -15,126 +18,38 @@ Add this line to your application's Gemfile:
|
|
15
18
|
gem "rutter"
|
16
19
|
```
|
17
20
|
|
18
|
-
And then execute:
|
19
|
-
|
20
|
-
$ bundle
|
21
|
-
|
22
|
-
Or install it yourself as:
|
23
|
-
|
24
|
-
$ gem install rutter
|
25
|
-
|
26
21
|
## Usage
|
27
22
|
|
28
|
-
|
23
|
+
The main purpose of a router is to map URL's to endpoints. An endpoint needs to
|
24
|
+
be either an object that responds to `call(env)` or a string that can be resolved
|
25
|
+
to one.
|
29
26
|
|
30
|
-
|
31
|
-
require "rutter"
|
32
|
-
|
33
|
-
router = Rutter.new do
|
34
|
-
get "/", to: ->(env) { [200, {}, ["Hello World"]] }
|
35
|
-
end
|
36
|
-
|
37
|
-
run router.freeze
|
38
|
-
```
|
39
|
-
|
40
|
-
### HTTP verbs
|
41
|
-
|
42
|
-
The router supports most of the verbs available.
|
27
|
+
Below are examples of both endpoint styles:
|
43
28
|
|
44
29
|
```ruby
|
45
|
-
require "rutter"
|
46
|
-
|
47
30
|
Rutter.new do
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
patch "/", to: ->(env) {}
|
52
|
-
delete "/", to: ->(env) {}
|
53
|
-
options "/", to: ->(env) {}
|
54
|
-
head "/", to: ->(env) {}
|
55
|
-
trace "/", to: ->(env) {}
|
56
|
-
end
|
31
|
+
# Endpoint that implements #call
|
32
|
+
get "/books", to: ->(env) { [200, {}, ["My Bookshelf"]] }
|
33
|
+
end.freeze
|
57
34
|
```
|
58
35
|
|
59
|
-
### Named parameters
|
60
|
-
|
61
|
-
In the example `:title` is a *named parameter*. The values are accessible via `env["rutter.params"]` and it contains a `Hash<String => String>`.
|
62
|
-
|
63
36
|
```ruby
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
37
|
+
class Books
|
38
|
+
def self.call(env)
|
39
|
+
[200, {}, ["My Bookshelf"]]
|
40
|
+
end
|
68
41
|
end
|
69
|
-
```
|
70
|
-
|
71
|
-
Named parameters only match a single path segment:
|
72
|
-
|
73
|
-
```
|
74
|
-
/books/eloquent-ruby match
|
75
|
-
/books/confident-ruby match
|
76
|
-
/books/confident-ruby.rb no match
|
77
|
-
/books/eloquent-ruby/reviews no match
|
78
|
-
/books/ no match
|
79
|
-
```
|
80
|
-
|
81
|
-
### Catch-All parameters
|
82
|
-
|
83
|
-
*catch-all* parameters have the form `*title`. Like the name suggests, they match everything, event new `/` segments. Therefore they must always be at the end of the pattern.
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
require "rutter"
|
87
42
|
|
88
43
|
Rutter.new do
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
```
|
94
|
-
/books/eloquent-ruby match
|
95
|
-
/books/confident-ruby match
|
96
|
-
/books/confident-ruby.rb match
|
97
|
-
/books/eloquent-ruby/reviews match
|
98
|
-
/books/ no match
|
99
|
-
```
|
100
|
-
|
101
|
-
### Optional segments
|
102
|
-
|
103
|
-
Support for optional segments have the form `(i-am-optional)`.
|
104
|
-
|
105
|
-
```ruby
|
106
|
-
require "rutter"
|
107
|
-
|
108
|
-
Rutter.new do
|
109
|
-
get "/books(/:title)", to: ->(env) {}
|
110
|
-
end
|
111
|
-
```
|
112
|
-
|
113
|
-
```
|
114
|
-
/books/eloquent-ruby match
|
115
|
-
/books/confident-ruby match
|
116
|
-
/books/confident-ruby.rb no match
|
117
|
-
/books/eloquent-ruby/reviews no match
|
118
|
-
/books match
|
119
|
-
```
|
120
|
-
|
121
|
-
### Redirects
|
122
|
-
|
123
|
-
Make legacy paths point to a new destination.
|
124
|
-
|
125
|
-
```ruby
|
126
|
-
require "rutter"
|
127
|
-
|
128
|
-
Rutter.new do
|
129
|
-
get "/legacy-path", to: redirect("/new_path")
|
130
|
-
end
|
44
|
+
# String that resolves to endpoint
|
45
|
+
get "/books", to: "books" # This will be resolved to <Books>
|
46
|
+
end.freeze
|
131
47
|
```
|
132
48
|
|
133
49
|
## Contributing
|
134
50
|
|
135
51
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sandelius/rutter. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
136
52
|
|
137
|
-
|
138
53
|
## License
|
139
54
|
|
140
55
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require "bundler/gem_tasks"
|
@@ -14,3 +13,10 @@ namespace :spec do
|
|
14
13
|
end
|
15
14
|
|
16
15
|
task default: :spec
|
16
|
+
|
17
|
+
task :clean do
|
18
|
+
FileUtils.rm_r ".yardoc" if Dir.exist?(".yardoc")
|
19
|
+
FileUtils.rm_r "doc" if Dir.exist?("doc")
|
20
|
+
FileUtils.rm_r "pkg" if Dir.exist?("pkg")
|
21
|
+
FileUtils.rm_r "coverage" if Dir.exist?("coverage")
|
22
|
+
end
|
data/bench/config.ru
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
# $ wrk -t 2 http://localhost:9292/
|
3
|
+
# puma -e production -t 16:16
|
5
4
|
|
6
|
-
require "
|
7
|
-
|
5
|
+
require "bundler/setup"
|
6
|
+
require "rutter"
|
8
7
|
|
9
8
|
router = Rutter.new do
|
10
|
-
|
11
|
-
|
12
|
-
}
|
13
|
-
end
|
9
|
+
# wrk -t 2 http://localhost:9292/
|
10
|
+
get "/", to: ->(_) { [200, {}, ["Hello World"]] }
|
14
11
|
|
15
|
-
|
12
|
+
# wrk -t 2 http://localhost:9292/ruby
|
13
|
+
get "/:lang", to: ->(env) { [200, {}, [env["rutter.params"]["lang"]]] }
|
14
|
+
end.freeze
|
15
|
+
|
16
|
+
run router
|
data/lib/rutter.rb
CHANGED
@@ -2,16 +2,19 @@
|
|
2
2
|
|
3
3
|
require_relative "rutter/version"
|
4
4
|
require_relative "rutter/builder"
|
5
|
-
require_relative "rutter/routes"
|
6
5
|
|
7
|
-
# HTTP router for
|
6
|
+
# HTTP router for Rack.
|
8
7
|
module Rutter
|
9
|
-
#
|
8
|
+
# Factory method for creating a new builder object.
|
10
9
|
#
|
11
|
-
# @
|
10
|
+
# @param base [String]
|
11
|
+
# Base URL, used for generating URLs.
|
12
12
|
#
|
13
|
-
# @
|
14
|
-
|
15
|
-
|
13
|
+
# @yield
|
14
|
+
# Executes the block inside the created builder context.
|
15
|
+
#
|
16
|
+
# @see Rutter::Builder#initialize
|
17
|
+
def self.new(base: "http://localhost:9292", &block)
|
18
|
+
Builder.new(base: base, &block)
|
16
19
|
end
|
17
20
|
end
|
data/lib/rutter/builder.rb
CHANGED
@@ -1,150 +1,75 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "uri"
|
4
4
|
|
5
|
+
require_relative "naming"
|
6
|
+
require_relative "verbs"
|
5
7
|
require_relative "route"
|
8
|
+
require_relative "mount"
|
6
9
|
require_relative "scope"
|
10
|
+
require_relative "routes"
|
7
11
|
|
8
12
|
module Rutter
|
9
|
-
# The
|
10
|
-
# it's to a controller and an action or a Rack endpoint.
|
13
|
+
# The builder map URL's to endpoints.
|
11
14
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# get "/", to: ->(env) {}
|
22
|
-
# post "/", to: ->(env) {}
|
23
|
-
# put "/", to: ->(env) {}
|
24
|
-
# patch "/", to: ->(env) {}
|
25
|
-
# delete "/", to: ->(env) {}
|
26
|
-
# options "/", to: ->(env) {}
|
27
|
-
# head "/", to: ->(env) {}
|
28
|
-
# trace "/", to: ->(env) {}
|
29
|
-
# end.freeze
|
15
|
+
# @!attribute [r] flat_map
|
16
|
+
# @return [Array]
|
17
|
+
# Defined routes in a flat map.
|
18
|
+
# @!attribute [r] verb_map
|
19
|
+
# @return [Hash]
|
20
|
+
# Defined routes grouped by verb.
|
21
|
+
# @!attribute [r] named_map
|
22
|
+
# @return [Hash]
|
23
|
+
# Defined routes grouped by route name.
|
30
24
|
class Builder
|
31
|
-
attr_reader :flat_map
|
25
|
+
attr_reader :flat_map
|
26
|
+
attr_reader :verb_map
|
27
|
+
attr_reader :named_map
|
32
28
|
|
33
|
-
# Initializes the
|
29
|
+
# Initializes the builder.
|
34
30
|
#
|
35
|
-
# @param
|
36
|
-
# URL
|
37
|
-
# @param host [String]
|
38
|
-
# URL host.
|
39
|
-
# @param port [Integer]
|
40
|
-
# URL port.
|
41
|
-
# @param controller_suffix [String]
|
42
|
-
# Suffix string for controllers (BooksController).
|
31
|
+
# @param base [String]
|
32
|
+
# Base URL, used for generating URLs.
|
43
33
|
#
|
44
34
|
# @yield
|
45
|
-
#
|
35
|
+
# Executes the block inside the created builder context.
|
46
36
|
#
|
47
|
-
# @return [
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
controller_suffix: nil,
|
53
|
-
&block
|
54
|
-
)
|
55
|
-
@scheme = scheme
|
56
|
-
@host = host
|
57
|
-
@port = port
|
58
|
-
@controller_suffix = controller_suffix
|
37
|
+
# @return [void]
|
38
|
+
#
|
39
|
+
# @private
|
40
|
+
def initialize(base: "http://localhost:9292", &block)
|
41
|
+
@uri = URI(base).freeze
|
59
42
|
@flat_map = []
|
60
|
-
@verb_map = Hash.new { |
|
43
|
+
@verb_map = Hash.new { |h, k| h[k] = [] }
|
61
44
|
@named_map = {}
|
62
45
|
|
63
46
|
instance_eval(&block) if block_given?
|
64
47
|
end
|
65
48
|
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# @param app [Class, Object]
|
69
|
-
# A class or an object that responds to `call`.
|
70
|
-
# @param at [String]
|
71
|
-
# Path prefix to match.
|
49
|
+
# Create a scoped set of routes.
|
72
50
|
#
|
73
|
-
# @
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# @param destination [String]
|
81
|
-
# The destination.
|
82
|
-
# @param status [Integer]
|
83
|
-
# Response status code.
|
84
|
-
#
|
85
|
-
# @return [Proc]
|
86
|
-
# Redirect endpoint.
|
87
|
-
#
|
88
|
-
# @example basic usage
|
89
|
-
# Rutter.new do
|
90
|
-
# get "/legacy-path", to: redirect("/new_path")
|
91
|
-
# end
|
51
|
+
# @param path [String]
|
52
|
+
# Scope path prefix.
|
53
|
+
# @param namespace [String, Symbol]
|
54
|
+
# Scope namespace.
|
55
|
+
# @param as [Symbol]
|
56
|
+
# Scope name prefix.
|
92
57
|
#
|
93
|
-
# @
|
94
|
-
#
|
95
|
-
# get "/legacy-path", to: redirect("/new_path", status: 301)
|
96
|
-
# end
|
97
|
-
def redirect(destination, status: 302)
|
98
|
-
->(_env) { [status, { "Location" => destination.to_s }, []] }
|
99
|
-
end
|
100
|
-
|
101
|
-
# Defines a root route (a GET route for '/').
|
58
|
+
# @yield
|
59
|
+
# Block is evaluated inside the created scope context.
|
102
60
|
#
|
103
|
-
# @return [Rutter::
|
61
|
+
# @return [Rutter::Scope]
|
104
62
|
#
|
105
|
-
# @see Rutter::
|
106
|
-
def
|
107
|
-
|
63
|
+
# @see Rutter::Scope
|
64
|
+
def scope(path: nil, namespace: nil, as: nil, &block)
|
65
|
+
Scope.new(self, path: path, namespace: namespace, as: as, &block)
|
108
66
|
end
|
109
67
|
|
110
|
-
#
|
68
|
+
# Creates a scoped collection of routes with the given name as namespace.
|
111
69
|
#
|
112
|
-
# @param
|
113
|
-
#
|
114
|
-
# @param path [String]
|
115
|
-
# Path template.
|
116
|
-
# @param as [String, Symbol]
|
117
|
-
# Route identifier (name).
|
118
|
-
# @param to [String, Proc]
|
119
|
-
# Route endpoint.
|
70
|
+
# @param name [Symbol, String]
|
71
|
+
# Scope namespace.
|
120
72
|
#
|
121
|
-
# @return [Rutter::Route]
|
122
|
-
def add(method, path, to:, as: nil)
|
123
|
-
route = Route.new(method, path, to, controller_suffix: @controller_suffix)
|
124
|
-
|
125
|
-
flat_map << route
|
126
|
-
verb_map[route.method] << route
|
127
|
-
|
128
|
-
return route unless as
|
129
|
-
|
130
|
-
add_named_route!(as, route)
|
131
|
-
end
|
132
|
-
|
133
|
-
# @see Rutter::Builder#add
|
134
|
-
Route::VERBS.each do |verb|
|
135
|
-
define_method verb.downcase do |path, to:, **opts|
|
136
|
-
add verb, path, to: to, **opts
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
# Starts a scoped collection of routes.
|
141
|
-
#
|
142
|
-
# @option opts [String] :path (nil)
|
143
|
-
# Path prefix
|
144
|
-
# @option opts [String] :namespace (nil)
|
145
|
-
# Namespace prefix
|
146
|
-
# @option opts [String, Symbol] :as (nil)
|
147
|
-
# Name prefix
|
148
73
|
# @yield
|
149
74
|
# Scope context.
|
150
75
|
#
|
@@ -152,155 +77,189 @@ module Rutter
|
|
152
77
|
#
|
153
78
|
# @example
|
154
79
|
# Rutter.new do
|
155
|
-
#
|
156
|
-
#
|
157
|
-
# get "/cats", to: "Cats#index", as: :cats
|
158
|
-
# end
|
80
|
+
# namespace :admin do
|
81
|
+
# get "/login", to: "sessions#new", as: :login
|
159
82
|
# end
|
160
83
|
# end
|
84
|
+
def namespace(name, &block)
|
85
|
+
scope path: name, namespace: name, as: name, &block
|
86
|
+
end
|
87
|
+
|
88
|
+
# Mount a Rack compatible at the given path prefix.
|
161
89
|
#
|
162
|
-
# @
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
168
|
-
def
|
169
|
-
|
90
|
+
# @param app [#call]
|
91
|
+
# Application to mount.
|
92
|
+
# @param at [String]
|
93
|
+
# Path prefix to match.
|
94
|
+
#
|
95
|
+
# @return [Rutter::Mount]
|
96
|
+
def mount(app, at:)
|
97
|
+
route = Mount.new(at, app)
|
98
|
+
@flat_map << route
|
99
|
+
VERBS.each { |verb| @verb_map[verb] << route }
|
100
|
+
route
|
170
101
|
end
|
171
102
|
|
172
|
-
#
|
103
|
+
# Generates a path from the given arguments.
|
173
104
|
#
|
174
105
|
# @param name [Symbol]
|
175
|
-
# Name of the route.
|
176
|
-
#
|
177
|
-
#
|
106
|
+
# Name of the route to generate path from.
|
107
|
+
#
|
108
|
+
# @overload path(name, key: value)
|
109
|
+
# @param key [String, Integer, Array]
|
110
|
+
# Key value.
|
111
|
+
# @overload path(name, key: value, key2: value2)
|
112
|
+
# @param key2 [String, Integer, Array]
|
113
|
+
# Key value.
|
178
114
|
#
|
179
115
|
# @return [String]
|
116
|
+
# Generated path.
|
180
117
|
#
|
181
|
-
# @raise [
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
118
|
+
# @raise [RuntimeError]
|
119
|
+
# If the route cannot be found.
|
120
|
+
#
|
121
|
+
# @see Rutter::Route#expand
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# router = Rutter.new(base: "http://rutter.org")
|
125
|
+
# router.get "/login", to: "sessions#new", as: :login
|
126
|
+
# router.get "/books/:id", to: "books#show", as: :book
|
127
|
+
#
|
128
|
+
# router.path(:login)
|
129
|
+
# # => "/login"
|
130
|
+
# router.path(:login, return_to: "/")
|
131
|
+
# # => "/login?return_to=/"
|
132
|
+
# router.path(:book, id: 82)
|
133
|
+
# # => "/books/82"
|
134
|
+
def path(name, *args)
|
135
|
+
unless (route = @named_map[name])
|
136
|
+
raise "No route called '#{name}' was found"
|
137
|
+
end
|
187
138
|
|
188
|
-
|
189
|
-
path = path.gsub(%r{\A[\/]+|[\/]+\z}, "")
|
190
|
-
"/#{path}"
|
139
|
+
route.expand(*args)
|
191
140
|
end
|
192
141
|
|
193
|
-
#
|
142
|
+
# Generates a full URL from the given arguments.
|
194
143
|
#
|
195
144
|
# @param name [Symbol]
|
196
|
-
# Name of the route.
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
# @
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
205
|
-
#
|
145
|
+
# Name of the route to generate URL from.
|
146
|
+
#
|
147
|
+
# @overload expand(name, subdomain: value)
|
148
|
+
# @param subdomain [String, Symbol]
|
149
|
+
# Subdomain to be added to the host.
|
150
|
+
# @overload expand(name, key: value)
|
151
|
+
# @param key [String, Integer, Array]
|
152
|
+
# Key value.
|
153
|
+
# @overload expand(name, key: value, key2: value2)
|
154
|
+
# @param key2 [String, Integer, Array]
|
155
|
+
# Key value.
|
206
156
|
#
|
207
157
|
# @return [String]
|
158
|
+
# Generated URL.
|
208
159
|
#
|
209
|
-
# @raise [
|
210
|
-
# If route
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
160
|
+
# @raise [RuntimeError]
|
161
|
+
# If the route cannot be found.
|
162
|
+
#
|
163
|
+
# @see Rutter::Builder#path
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# router = Rutter.new(base: "http://rutter.org")
|
167
|
+
# router.get "/login", to: "sessions#new", as: :login
|
168
|
+
# router.get "/books/:id", to: "books#show", as: :book
|
169
|
+
#
|
170
|
+
# router.url(:login)
|
171
|
+
# # => "http://rutter.org/login"
|
172
|
+
# router.url(:login, return_to: "/")
|
173
|
+
# # => "http://rutter.org/login?return_to=/"
|
174
|
+
# router.url(:book, id: 82)
|
175
|
+
# # => "http://rutter.org/books/82"
|
176
|
+
def url(name, **args)
|
177
|
+
host = @uri.scheme + "://"
|
178
|
+
host += "#{args.delete(:subdomain)}." if args.key?(:subdomain)
|
179
|
+
host += @uri.host
|
180
|
+
host += ":#{@uri.port}" if @uri.port != 80 && @uri.port != 443
|
181
|
+
host + path(name, args)
|
219
182
|
end
|
220
183
|
|
221
|
-
#
|
184
|
+
# Add a new, frozen, route to the map.
|
222
185
|
#
|
223
|
-
# @param
|
224
|
-
#
|
186
|
+
# @param verb [String]
|
187
|
+
# Request verb to match.
|
188
|
+
# @param path [String]
|
189
|
+
# Path template to match.
|
190
|
+
# @param to [#call]
|
191
|
+
# Rack endpoint.
|
192
|
+
# @param as [Symbol, String]
|
193
|
+
# Route name/identifier.
|
194
|
+
# @param constraints [Hash]
|
195
|
+
# Route segment constraints.
|
225
196
|
#
|
226
|
-
# @return [
|
227
|
-
# Serialized Rack response.
|
197
|
+
# @return [Rutter::Route]
|
228
198
|
#
|
229
|
-
# @
|
199
|
+
# @raise [ArgumentError]
|
200
|
+
# If verb is unsupported.
|
230
201
|
#
|
231
202
|
# @private
|
232
|
-
def
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
ctrl = Object.const_get(ctrl) if ctrl.is_a?(String)
|
238
|
-
return ctrl.call(env)
|
203
|
+
def add(verb, path, to:, as: nil, constraints: nil)
|
204
|
+
verb = verb.to_s.upcase
|
205
|
+
|
206
|
+
unless VERBS.include?(verb)
|
207
|
+
raise ArgumentError, "Unsupported verb '#{verb}'"
|
239
208
|
end
|
240
209
|
|
241
|
-
|
210
|
+
route = Route.new(path, to, constraints)
|
211
|
+
@flat_map << route
|
212
|
+
@verb_map[verb] << route
|
213
|
+
return route unless as
|
214
|
+
|
215
|
+
named_map[Naming.route_name(as)] = route
|
242
216
|
end
|
243
217
|
|
244
|
-
#
|
218
|
+
# Freeze the state of the router.
|
245
219
|
#
|
246
220
|
# @return [self]
|
247
221
|
def freeze
|
248
|
-
flat_map.freeze
|
249
|
-
verb_map.freeze
|
250
|
-
|
222
|
+
@flat_map.freeze
|
223
|
+
@verb_map.freeze
|
224
|
+
@verb_map.each_value(&:freeze)
|
225
|
+
@named_map.freeze
|
251
226
|
|
252
227
|
super
|
253
228
|
end
|
254
229
|
|
255
|
-
|
230
|
+
# @see #add
|
231
|
+
VERBS.each do |verb|
|
232
|
+
define_method verb.downcase do |path, to:, as: nil, constraints: nil|
|
233
|
+
add verb, path, to: to, as: as, constraints: constraints
|
234
|
+
end
|
235
|
+
end
|
256
236
|
|
257
|
-
#
|
237
|
+
# Process the request and is compatible with the Rack protocol.
|
238
|
+
#
|
239
|
+
# @param env [Hash]
|
240
|
+
# Rack environment hash.
|
258
241
|
#
|
259
|
-
# @
|
242
|
+
# @return [Array]
|
243
|
+
# Serialized Rack response.
|
260
244
|
#
|
261
|
-
# @
|
245
|
+
# @see http://rack.github.io
|
262
246
|
#
|
263
247
|
# @private
|
264
|
-
def
|
265
|
-
|
266
|
-
path = path.chomp("/") if path != "/" && path.end_with?("/")
|
267
|
-
|
268
|
-
routes = verb_map[env["REQUEST_METHOD"]]
|
269
|
-
routes.each do |route|
|
270
|
-
return route if route.match?(path)
|
271
|
-
end
|
272
|
-
|
273
|
-
false
|
274
|
-
end
|
248
|
+
def call(env)
|
249
|
+
request_method = env["REQUEST_METHOD"]
|
275
250
|
|
276
|
-
|
277
|
-
def add_named_route!(name, route)
|
278
|
-
name = normalize_route_name(name)
|
251
|
+
return NOT_FOUND_RESPONSE unless @verb_map.key?(request_method)
|
279
252
|
|
280
|
-
|
281
|
-
|
253
|
+
routes = @verb_map[request_method]
|
254
|
+
routes.each do |route|
|
255
|
+
next unless route.match?(env)
|
256
|
+
return route.call(env)
|
282
257
|
end
|
283
258
|
|
284
|
-
|
285
|
-
end
|
286
|
-
|
287
|
-
# @private
|
288
|
-
def normalize_route_name(name)
|
289
|
-
name.to_s
|
290
|
-
.tr("/", "_")
|
291
|
-
.gsub(/[_]{2,}/, "_")
|
292
|
-
.gsub(/\A_|_\z/, "")
|
293
|
-
.downcase
|
294
|
-
.to_sym
|
259
|
+
NOT_FOUND_RESPONSE
|
295
260
|
end
|
296
261
|
|
297
|
-
# Response returned when no route matched the request.
|
298
|
-
#
|
299
262
|
# @private
|
300
|
-
NOT_FOUND_RESPONSE = [
|
301
|
-
404,
|
302
|
-
{ "X-Cascade" => "pass" },
|
303
|
-
["Route Not Found"]
|
304
|
-
].freeze
|
263
|
+
NOT_FOUND_RESPONSE = [404, { "X-Cascade" => "pass" }, ["Not Found"]].freeze
|
305
264
|
end
|
306
265
|
end
|