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
data/lib/rutter/verbs.rb
ADDED
data/lib/rutter/version.rb
CHANGED
data/rutter.gemspec
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require File.expand_path("
|
3
|
+
require File.expand_path("lib/rutter/version", __dir__)
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "rutter"
|
7
7
|
spec.version = Rutter::VERSION
|
8
8
|
spec.summary = "HTTP router for Rack."
|
9
9
|
|
10
|
-
spec.required_ruby_version = ">= 2.
|
10
|
+
spec.required_ruby_version = ">= 2.5.0"
|
11
11
|
spec.required_rubygems_version = ">= 2.5.0"
|
12
12
|
|
13
13
|
spec.license = "MIT"
|
@@ -20,7 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^(spec)/})
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.
|
23
|
+
spec.add_runtime_dependency "mustermann", "~> 1.0"
|
24
|
+
spec.add_runtime_dependency "rack", "~> 2.0"
|
24
25
|
|
25
26
|
spec.add_development_dependency "bundler"
|
26
27
|
spec.add_development_dependency "rake"
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
RSpec.describe "Rack integration" do
|
6
|
-
let(:router) do
|
3
|
+
RSpec.describe "Rack compatible", type: :request do
|
4
|
+
let :router do
|
7
5
|
Rutter.new do
|
8
|
-
get "/
|
6
|
+
get "/say/:message", to: (lambda do |env|
|
7
|
+
[200, {}, ["I say, #{env['rutter.params']['message']}"]]
|
8
|
+
end)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -13,21 +13,29 @@ RSpec.describe "Rack integration" do
|
|
13
13
|
router.freeze
|
14
14
|
end
|
15
15
|
|
16
|
-
|
16
|
+
context "with match" do
|
17
17
|
it "calls the matched endpoint" do
|
18
|
-
get "/
|
19
|
-
|
20
|
-
expect(last_response.
|
18
|
+
get "/say/hello-world"
|
19
|
+
|
20
|
+
expect(last_response.status)
|
21
|
+
.to eq(200)
|
22
|
+
expect(last_response.body)
|
23
|
+
.to eq("I say, hello-world")
|
24
|
+
expect(last_response.headers["Content-Length"])
|
25
|
+
.to eq("18")
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
24
|
-
|
29
|
+
context "with no match" do
|
25
30
|
it "returns 404" do
|
26
31
|
get "/authors"
|
27
32
|
|
28
|
-
expect(last_response.status)
|
29
|
-
|
30
|
-
expect(last_response.
|
33
|
+
expect(last_response.status)
|
34
|
+
.to eq(404)
|
35
|
+
expect(last_response.body)
|
36
|
+
.to eq("Not Found")
|
37
|
+
expect(last_response.headers["X-Cascade"])
|
38
|
+
.to eq("pass")
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
4
|
|
5
|
+
unless ENV["CI"]
|
6
|
+
require "byebug"
|
7
|
+
end
|
8
|
+
|
5
9
|
if ENV["COVERAGE"] == "true"
|
6
10
|
require "simplecov"
|
7
|
-
require "codeclimate-test-reporter"
|
8
11
|
|
9
12
|
SimpleCov.start do
|
10
13
|
command_name "spec"
|
@@ -15,27 +18,12 @@ end
|
|
15
18
|
# Require support (helper) modules
|
16
19
|
Dir["./spec/support/**/*.rb"].each { |f| require f }
|
17
20
|
|
18
|
-
# Require library
|
19
|
-
require "rack/test"
|
20
21
|
require "rutter"
|
21
22
|
|
22
|
-
# This file was generated by the `rspec --init` command. Conventionally, all
|
23
|
-
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
24
|
-
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
25
|
-
# this file to always be loaded, without a need to explicitly require it in any
|
26
|
-
# files.
|
27
|
-
#
|
28
|
-
# Given that it is always loaded, you are encouraged to keep this file as
|
29
|
-
# light-weight as possible. Requiring heavyweight dependencies from this file
|
30
|
-
# will add to the boot time of your test suite on EVERY test run, even for an
|
31
|
-
# individual file that may not need all of that loaded. Instead, consider making
|
32
|
-
# a separate helper file that requires the additional dependencies and performs
|
33
|
-
# the additional setup, and require it from the spec files that actually need
|
34
|
-
# it.
|
35
|
-
#
|
36
23
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
37
24
|
RSpec.configure do |config|
|
38
|
-
|
25
|
+
# Disable monkey-pachting core ruby.
|
26
|
+
config.disable_monkey_patching!
|
39
27
|
|
40
28
|
# rspec-expectations config goes here. You can use an alternate
|
41
29
|
# assertion/expectation library such as wrong or the stdlib/minitest
|
@@ -66,34 +54,4 @@ RSpec.configure do |config|
|
|
66
54
|
# inherited by the metadata hash of host groups and examples, rather than
|
67
55
|
# triggering implicit auto-inclusion in groups with matching metadata.
|
68
56
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
69
|
-
|
70
|
-
# This allows you to limit a spec run to individual examples or groups
|
71
|
-
# you care about by tagging them with `:focus` metadata. When nothing
|
72
|
-
# is tagged with `:focus`, all examples get run. RSpec also provides
|
73
|
-
# aliases for `it`, `describe`, and `context` that include `:focus`
|
74
|
-
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
75
|
-
config.filter_run_when_matching :focus
|
76
|
-
|
77
|
-
# Limits the available syntax to the non-monkey patched syntax that is
|
78
|
-
# recommended. For more details, see:
|
79
|
-
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
80
|
-
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
81
|
-
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
82
|
-
config.disable_monkey_patching!
|
83
|
-
|
84
|
-
# This setting enables warnings. It's recommended, but in some cases may
|
85
|
-
# be too noisy due to issues in dependencies.
|
86
|
-
config.warnings = true
|
87
|
-
|
88
|
-
# Run specs in random order to surface order dependencies. If you find an
|
89
|
-
# order dependency and want to debug it, you can fix the order by providing
|
90
|
-
# the seed, which is printed after each run.
|
91
|
-
# --seed 1234
|
92
|
-
config.order = :random
|
93
|
-
|
94
|
-
# Seed global randomization in this process using the `--seed` CLI option.
|
95
|
-
# Setting this allows you to use `--seed` to deterministically reproduce
|
96
|
-
# test failures related to randomization by passing the same `--seed` value
|
97
|
-
# as the one that triggered the failure.
|
98
|
-
Kernel.srand config.seed
|
99
57
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/test"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module Support
|
7
|
+
module Rack
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def env_for(uri = "", opts = {})
|
11
|
+
::Rack::MockRequest.env_for(uri, opts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.include RSpec::Support::Rack
|
19
|
+
config.include Rack::Test::Methods, type: :request
|
20
|
+
end
|
data/spec/unit/builder_spec.rb
CHANGED
@@ -3,108 +3,120 @@
|
|
3
3
|
module Rutter
|
4
4
|
RSpec.describe Builder do
|
5
5
|
let(:router) { Builder.new }
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
expect(route
|
13
|
-
|
6
|
+
let(:endpoint) { ->(_) {} }
|
7
|
+
|
8
|
+
describe "#mount" do
|
9
|
+
it "matches path prefixes" do
|
10
|
+
route = router.mount endpoint, at: "/admin"
|
11
|
+
|
12
|
+
expect(route.match?(env_for("/")))
|
13
|
+
.to be_nil
|
14
|
+
expect(route.match?(env_for("/books")))
|
15
|
+
.to be_nil
|
16
|
+
expect(route.match?(env_for("/admin")))
|
17
|
+
.to eq("/admin")
|
18
|
+
expect(route.match?(env_for("/admin/books")))
|
19
|
+
.to eq("/admin")
|
14
20
|
end
|
21
|
+
end
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
router.
|
19
|
-
|
20
|
-
|
23
|
+
describe "#path" do
|
24
|
+
it "generates a path for named routes" do
|
25
|
+
router.get "/login", to: "sessions#new", as: :login
|
26
|
+
router.get "/books/:id", to: "books#show", as: :book
|
27
|
+
|
28
|
+
expect(router.path(:login))
|
29
|
+
.to eq("/login")
|
30
|
+
expect(router.path(:login, return_to: "/"))
|
31
|
+
.to eq("/login?return_to=%2F")
|
32
|
+
expect(router.path(:book, id: 82))
|
33
|
+
.to eq("/books/82")
|
21
34
|
end
|
22
35
|
|
23
|
-
it "
|
24
|
-
|
25
|
-
|
36
|
+
it "raises an error if route not founf" do
|
37
|
+
expect { router.path(:book) }
|
38
|
+
.to raise_error(RuntimeError, "No route called 'book' was found")
|
26
39
|
end
|
40
|
+
end
|
27
41
|
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
describe "#url" do
|
43
|
+
it "generates a full URL for named routes" do
|
44
|
+
router = Rutter.new(base: "http://rutter.org")
|
45
|
+
router.get "/login", to: "sessions#new", as: :login
|
46
|
+
router.get "/books/:id", to: "books#show", as: :book
|
47
|
+
|
48
|
+
expect(router.url(:login))
|
49
|
+
.to eq("http://rutter.org/login")
|
50
|
+
expect(router.url(:login, return_to: "/"))
|
51
|
+
.to eq("http://rutter.org/login?return_to=%2F")
|
52
|
+
expect(router.url(:book, id: 82))
|
53
|
+
.to eq("http://rutter.org/books/82")
|
31
54
|
end
|
32
55
|
|
33
|
-
it "
|
34
|
-
router.
|
56
|
+
it "supports adding a subdomain" do
|
57
|
+
router = Rutter.new(base: "http://rutter.org")
|
58
|
+
router.get "/login", to: "sessions#new", as: :login
|
35
59
|
|
36
|
-
expect
|
37
|
-
.to
|
60
|
+
expect(router.url(:login, subdomain: "auth"))
|
61
|
+
.to eq("http://auth.rutter.org/login")
|
38
62
|
end
|
39
|
-
end
|
40
63
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
expect(route.path).to eq("/")
|
46
|
-
expect(router.named_map[:root]).to eq(route)
|
64
|
+
it "raises an error if route not founf" do
|
65
|
+
expect { router.url(:book) }
|
66
|
+
.to raise_error(RuntimeError, "No route called 'book' was found")
|
47
67
|
end
|
48
68
|
end
|
49
69
|
|
50
|
-
describe "
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
expect(router.flat_map[0].method).to eq(verb)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
describe "#path" do
|
60
|
-
it "generates a URL path from the arguments" do
|
61
|
-
router.get "/users/:id(/:username)", to: "Users#show", as: :user
|
62
|
-
|
63
|
-
expect(router.path(:user, id: 45, username: "sandelius"))
|
64
|
-
.to eq("/users/45/sandelius")
|
65
|
-
expect(router.path(:user, id: 45)).to eq("/users/45")
|
70
|
+
describe "#add" do
|
71
|
+
it "raises an error if the verb is unsupported" do
|
72
|
+
expect { router.add("unknown", "/", to: endpoint) }
|
73
|
+
.to raise_error(ArgumentError, "Unsupported verb 'UNKNOWN'")
|
66
74
|
end
|
67
75
|
|
68
|
-
it "
|
69
|
-
|
70
|
-
.to raise_error("no route called 'user'")
|
71
|
-
end
|
72
|
-
end
|
76
|
+
it "normalize route names" do
|
77
|
+
route = router.get "/", to: endpoint, as: "_wierd/__name__"
|
73
78
|
|
74
|
-
|
75
|
-
|
76
|
-
router.get "/users/:id", to: "Users#show", as: :user
|
77
|
-
|
78
|
-
expect(router.url(:user, id: 54)).to eq("http://example.com/users/54")
|
79
|
+
expect(router.named_map[:wierd_name])
|
80
|
+
.to eq(route)
|
79
81
|
end
|
80
82
|
|
81
|
-
it "
|
82
|
-
router.get "/
|
83
|
-
|
84
|
-
|
85
|
-
:user,
|
86
|
-
id: 54, _host: "example.com", _scheme: "https", _subdomain: "api"
|
87
|
-
)
|
83
|
+
it "support using route constraints" do
|
84
|
+
route = router.get "/books/:id",
|
85
|
+
to: endpoint,
|
86
|
+
constraints: { id: /\d+/ }
|
88
87
|
|
89
|
-
expect(
|
88
|
+
expect(route.match?(env_for("/books/82")))
|
89
|
+
.to be(true)
|
90
|
+
expect(route.match?(env_for("/books/pickaxe")))
|
91
|
+
.to be(false)
|
90
92
|
end
|
93
|
+
end
|
91
94
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
95
|
+
describe "verbs" do
|
96
|
+
VERBS.each do |verb|
|
97
|
+
describe "##{verb.downcase}" do
|
98
|
+
it "recognize #{verb} verb" do
|
99
|
+
route = router.public_send verb.downcase, "/", to: endpoint
|
100
|
+
|
101
|
+
expect(router.verb_map[verb])
|
102
|
+
.to eq([route])
|
103
|
+
end
|
104
|
+
end
|
97
105
|
end
|
98
106
|
end
|
99
107
|
|
100
108
|
describe "#freeze" do
|
101
|
-
it "freezes
|
109
|
+
it "freezes the router and its maps" do
|
102
110
|
router.freeze
|
103
111
|
|
104
|
-
expect(router)
|
105
|
-
|
106
|
-
expect(router.
|
107
|
-
|
112
|
+
expect(router)
|
113
|
+
.to be_frozen
|
114
|
+
expect(router.flat_map)
|
115
|
+
.to be_frozen
|
116
|
+
expect(router.verb_map)
|
117
|
+
.to be_frozen
|
118
|
+
expect(router.named_map)
|
119
|
+
.to be_frozen
|
108
120
|
end
|
109
121
|
end
|
110
122
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rutter
|
4
|
+
RSpec.describe Builder do
|
5
|
+
let(:router) { Rutter.new }
|
6
|
+
|
7
|
+
describe "namespace" do
|
8
|
+
it "support nested namespacees" do
|
9
|
+
router.namespace :species do
|
10
|
+
namespace :mammals do
|
11
|
+
get "/cats", to: "cats#index", as: :cats
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
route = router.flat_map.first
|
16
|
+
|
17
|
+
expect(route.path)
|
18
|
+
.to eq("/species/mammals/cats")
|
19
|
+
expect(route.endpoint)
|
20
|
+
.to eq(controller: "Species::Mammals::Cats", action: "index")
|
21
|
+
expect(router.named_map[:species_mammals_cats])
|
22
|
+
.to eq(route)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/unit/route_spec.rb
CHANGED
@@ -2,80 +2,63 @@
|
|
2
2
|
|
3
3
|
module Rutter
|
4
4
|
RSpec.describe Route do
|
5
|
-
|
6
|
-
expect { Route.new("NONE", "/", ->(env) {}) }
|
7
|
-
.to raise_error(ArgumentError, "invalid verb: 'NONE'")
|
8
|
-
end
|
5
|
+
let(:endpoint) { ->(_) {} }
|
9
6
|
|
10
|
-
it "
|
11
|
-
|
12
|
-
|
7
|
+
it "freezes the object" do
|
8
|
+
expect(Route.new("/", endpoint))
|
9
|
+
.to be_frozen
|
13
10
|
end
|
14
11
|
|
15
|
-
describe "#
|
16
|
-
it "
|
17
|
-
route = Route.new("
|
12
|
+
describe "#match?" do
|
13
|
+
it "returns true if path match" do
|
14
|
+
route = Route.new("/books", endpoint)
|
18
15
|
|
19
|
-
expect(route.
|
20
|
-
.to
|
21
|
-
expect(route.expand(id: 54, include: [:foo]))
|
22
|
-
.to eq("/books/54?include[]=foo")
|
23
|
-
expect(route.expand(id: 54, title: "eloquent"))
|
24
|
-
.to eq("/books/54/eloquent")
|
25
|
-
expect(route.expand(id: 54)).to eq("/books/54")
|
16
|
+
expect(route.match?(env_for("/books")))
|
17
|
+
.to be(true)
|
26
18
|
end
|
27
19
|
|
28
|
-
it "
|
29
|
-
route = Route.new("
|
30
|
-
expect(route.expand(include: [:foo])).to eq("/books?include[]=foo")
|
31
|
-
expect(route.expand).to eq("/books")
|
32
|
-
end
|
33
|
-
end
|
20
|
+
it "returns false if path does not match" do
|
21
|
+
route = Route.new("/books", endpoint)
|
34
22
|
|
35
|
-
|
36
|
-
|
37
|
-
expect(Route.new("GET", "/books", -> {}).match?("/books"))
|
38
|
-
.to eq(true)
|
23
|
+
expect(route.match?(env_for("/")))
|
24
|
+
.to be(false)
|
39
25
|
end
|
40
26
|
|
41
|
-
it "support
|
42
|
-
route = Route.new("
|
27
|
+
it "support using route constraints" do
|
28
|
+
route = Route.new("/books/:id", endpoint, id: /\d+/)
|
43
29
|
|
44
|
-
expect(route.match?("/
|
45
|
-
|
46
|
-
expect(route.match?("/
|
47
|
-
|
30
|
+
expect(route.match?(env_for("/books/82")))
|
31
|
+
.to be(true)
|
32
|
+
expect(route.match?(env_for("/books/pickaxe")))
|
33
|
+
.to be(false)
|
48
34
|
end
|
35
|
+
end
|
49
36
|
|
50
|
-
|
51
|
-
|
37
|
+
describe "#expand" do
|
38
|
+
let(:route) do
|
39
|
+
Route.new("/books/:book_id/reviews/:id", endpoint, {})
|
40
|
+
end
|
41
|
+
|
42
|
+
it "generates path from given args" do
|
43
|
+
expect(route.expand(book_id: 1, id: 2))
|
44
|
+
.to eq("/books/1/reviews/2")
|
45
|
+
end
|
52
46
|
|
53
|
-
|
54
|
-
expect
|
55
|
-
|
56
|
-
expect(route.match?("/pages")).to eq(false)
|
47
|
+
it "raises an error if a required argument is missing" do
|
48
|
+
expect { route.expand(book_id: 1) }
|
49
|
+
.to raise_error(ArgumentError, /cannot expand with keys/)
|
57
50
|
end
|
58
51
|
end
|
59
52
|
|
60
53
|
describe "#params" do
|
61
54
|
it "extract params" do
|
62
|
-
route = Route.new("
|
55
|
+
route = Route.new("/pages/:id(/:title)?", endpoint)
|
63
56
|
|
64
57
|
expect(route.params("/pages/54/eloquent-ruby"))
|
65
58
|
.to eq("id" => "54", "title" => "eloquent-ruby")
|
66
59
|
expect(route.params("/pages/54"))
|
67
60
|
.to eq("id" => "54", "title" => nil)
|
68
61
|
end
|
69
|
-
|
70
|
-
it "returns nil if no match" do
|
71
|
-
route = Route.new("GET", "/books/:id", -> {})
|
72
|
-
expect(route.params("/posts/54")).to be_nil
|
73
|
-
end
|
74
|
-
|
75
|
-
it "returns empty array if no params present" do
|
76
|
-
route = Route.new("GET", "/books", -> {})
|
77
|
-
expect(route.params("/books")).to eq({})
|
78
|
-
end
|
79
62
|
end
|
80
63
|
end
|
81
64
|
end
|