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