rutter 0.1.2 → 0.2.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.
- 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
|