rutter 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rutter
4
+ # Supported request verbs.
5
+ #
6
+ # @return [Array]
7
+ VERBS = %w[GET POST PUT PATCH DELETE OPTIONS].freeze
8
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Rutter
4
4
  # Current version number.
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.0"
6
6
  end
data/rutter.gemspec CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path("../lib/rutter/version", __FILE__)
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.4.0"
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.add_dependency "rack", "~> 2.0"
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
- require "spec_helper"
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 "/books", to: ->(_env) { [200, {}, ["Books"]] }
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
- describe "with match" do
16
+ context "with match" do
17
17
  it "calls the matched endpoint" do
18
- get "/books"
19
- expect(last_response.status).to eq(200)
20
- expect(last_response.body).to eq("Books")
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
- describe "with no match" do
29
+ context "with no match" do
25
30
  it "returns 404" do
26
31
  get "/authors"
27
32
 
28
- expect(last_response.status).to eq(404)
29
- expect(last_response.body).to eq("Route Not Found")
30
- expect(last_response.headers["X-Cascade"]).to eq("pass")
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
- config.include Rack::Test::Methods
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
@@ -3,108 +3,120 @@
3
3
  module Rutter
4
4
  RSpec.describe Builder do
5
5
  let(:router) { Builder.new }
6
-
7
- describe "#add" do
8
- it "returns the created route object" do
9
- route = router.get "/books", to: ->(env) {}
10
- named_route = router.get "/named_books", to: ->(env) {}, as: :books
11
-
12
- expect(route).to be_a(Route)
13
- expect(named_route).to be_a(Route)
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
- it "adds a route to the collection" do
17
- router.get "/books", to: ->(env) {}
18
- router.post "/books", to: ->(env) {}
19
-
20
- expect(router.flat_map.size).to eq(2)
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 "stores the name of the route" do
24
- route = router.add "GET", "/books", as: :books, to: ->(env) {}
25
- expect(router.named_map[:books]).to eq(route)
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
- it "normalizes route names" do
29
- router.get "/books", as: :_bookie__books__, to: ->(env) {}
30
- expect(router.named_map.key?(:bookie_books)).to eq(true)
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 "raises an error if named route already been defined" do
34
- router.get "/books", as: :books, to: ->(env) {}
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 { router.get "/books", as: :books, to: ->(env) {} }
37
- .to raise_error("a route called 'books' has already been defined")
60
+ expect(router.url(:login, subdomain: "auth"))
61
+ .to eq("http://auth.rutter.org/login")
38
62
  end
39
- end
40
63
 
41
- describe "#root" do
42
- it "adds a root path with a root name" do
43
- route = router.root to: ->(env) {}
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 "verbs" do
51
- Route::VERBS.each do |verb|
52
- it "accept the #{verb} verb as method" do
53
- router.send verb.downcase, "/", to: ->() {}
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 "raises an error if route has not been defined" do
69
- expect { router.path(:user) }
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
- describe "#url" do
75
- it "generates a full URL" do
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 "allows host scheme and subdomain to be set" do
82
- router.get "/users/:id", to: "Users#show", as: :user
83
-
84
- url = router.url(
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(url).to eq("https://api.example.com/users/54")
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
- it "adds port if no 80 or 433" do
93
- router.get "/users/:id", to: "Users#show", as: :user
94
-
95
- expect(router.url(:user, id: 54, _port: 333))
96
- .to eq("http://example.com:333/users/54")
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 all objects" do
109
+ it "freezes the router and its maps" do
102
110
  router.freeze
103
111
 
104
- expect(router).to be_frozen
105
- expect(router.flat_map).to be_frozen
106
- expect(router.named_map).to be_frozen
107
- expect(router.verb_map).to be_frozen
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
@@ -2,80 +2,63 @@
2
2
 
3
3
  module Rutter
4
4
  RSpec.describe Route do
5
- it "raises an error if request method is invalid" do
6
- expect { Route.new("NONE", "/", ->(env) {}) }
7
- .to raise_error(ArgumentError, "invalid verb: 'NONE'")
8
- end
5
+ let(:endpoint) { ->(_) {} }
9
6
 
10
- it "normalizes path" do
11
- route = Route.new("GET", "//about////us/", ->(env) {})
12
- expect(route.path).to eq("/about/us")
7
+ it "freezes the object" do
8
+ expect(Route.new("/", endpoint))
9
+ .to be_frozen
13
10
  end
14
11
 
15
- describe "#expand" do
16
- it "expand dynamic routes" do
17
- route = Route.new("GET", "/books/:id(/:title)", -> {})
12
+ describe "#match?" do
13
+ it "returns true if path match" do
14
+ route = Route.new("/books", endpoint)
18
15
 
19
- expect(route.expand(id: 54, title: "eloquent", include: [:foo]))
20
- .to eq("/books/54/eloquent?include[]=foo")
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 "expand static routes" do
29
- route = Route.new("GET", "/books", -> {})
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
- describe "#match?" do
36
- it "matches non regexp" do
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 named parameters" do
42
- route = Route.new("GET", "/pages/:id(/:title)", -> {})
27
+ it "support using route constraints" do
28
+ route = Route.new("/books/:id", endpoint, id: /\d+/)
43
29
 
44
- expect(route.match?("/pages/54/eloquent-ruby")).to eq(true)
45
- expect(route.match?("/pages/54")).to eq(true)
46
- expect(route.match?("/pages/54.rb")).to eq(false)
47
- expect(route.match?("/pages")).to eq(false)
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
- it "support catch-all parameter" do
51
- route = Route.new("GET", "/pages/*slug", -> {})
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
- expect(route.match?("/pages/54/eloquent-ruby")).to eq(true)
54
- expect(route.match?("/pages/54/eloquent-ruby.rb")).to eq(true)
55
- expect(route.match?("/pages/54/eloquent-ruby/with/more/parts")).to eq(true)
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("GET", "/pages/:id(/:title)", -> {})
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