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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7e0af90882ec8c36cf23e278e384a38e38aa3959
4
- data.tar.gz: '0387e7a032da114cb32b1635330dd878f207e960'
2
+ SHA256:
3
+ metadata.gz: 68f5fb5ae85793cb16d135928e3a26cf043a3eda09338f8c2a03117a16985b2d
4
+ data.tar.gz: c9766d83b10a8b6f103f26cc100ca753ecace464770fb739eccd4ecb2f2a876e
5
5
  SHA512:
6
- metadata.gz: 236b39818113ff5910c9be230bbb7ec42478c30b891a56ecbfe2083756b5c16299f44b9e603ab0c5d6cbae33643699d988a03f66668c44a2cd9cd401309154c9
7
- data.tar.gz: 8ffd27decede4f74d81a374296376692b47918946f9aab638f8369a1d539c088c803a9d3ad5551f6e942ffa0a94344147d251482c78f975b164760745d41e9e7
6
+ metadata.gz: 1b0bcaae9355facffd058bf84021a4d62d1365312c254fd4e2ead08b6d907c68791572f47dc3451d35fcda32fb777ff3c94c5785b270cc290187aef3ce3e3260
7
+ data.tar.gz: 10e3b47288658c8dd7068447339fa54981684e225542c4c05ac7440dc2fa3f2112c8461ac849b855e16c740e4357d0f7e673a49eabac946a762c21b028882d3a
data/.gitignore CHANGED
@@ -5,6 +5,8 @@
5
5
  /coverage/
6
6
  /pkg/
7
7
  /spec/reports/
8
+ /spec/examples.txt
8
9
  /tmp/
9
10
  .DS_Store
10
11
  /doc
12
+ .byebug_history
data/.rubocop.yml CHANGED
@@ -1,14 +1,21 @@
1
+ require:
2
+ - rubocop-performance
3
+
1
4
  AllCops:
2
- TargetRubyVersion: 2.4
5
+ TargetRubyVersion: 2.5
3
6
  DisplayCopNames: true
4
7
  Exclude:
5
- - bench/*
8
+ - "*.gemspec"
9
+ - "Gemfile"
10
+
11
+ Performance:
12
+ Enabled: true
6
13
 
7
14
  Style/StringLiterals:
8
15
  EnforcedStyle: double_quotes
9
16
 
10
- Lint/EndAlignment:
11
- EnforcedStyleAlignWith: variable
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
- - "CODECLIMATE_REPO_TOKEN=05a406e21e530e8210057604aa78eccebb988a8201c6b1e78187dff978bdee07 bundle exec codeclimate-test-reporter"
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.4.0
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
@@ -3,5 +3,8 @@
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
- gem "codeclimate-test-reporter"
6
+ unless ENV["CI"]
7
+ gem "byebug", platform: :mri
8
+ end
9
+
7
10
  gem "simplecov"
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Tobias Sandelius
3
+ Copyright (c) 2019 Tobias Sandelius
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- Basic usage
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
- ```ruby
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
- get "/", to: ->(env) {}
49
- post "/", to: ->(env) {}
50
- put "/", to: ->(env) {}
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
- require "rutter"
65
-
66
- Rutter.new do
67
- get "/books/:title", to: ->(env) {}
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
- get "/books/*title", to: ->(env) {}
90
- end
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
- # $ puma -e production -t 16:16
4
- # $ wrk -t 2 http://localhost:9292/
3
+ # puma -e production -t 16:16
5
4
 
6
- require "rack"
7
- require_relative "../lib/rutter"
5
+ require "bundler/setup"
6
+ require "rutter"
8
7
 
9
8
  router = Rutter.new do
10
- root to: lambda { |env|
11
- [200, { "Content-Type" => "text/html" }, ["Hello World"]]
12
- }
13
- end
9
+ # wrk -t 2 http://localhost:9292/
10
+ get "/", to: ->(_) { [200, {}, ["Hello World"]] }
14
11
 
15
- run router.freeze
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 Ramverk and Rack.
6
+ # HTTP router for Rack.
8
7
  module Rutter
9
- # Creates a new builder object.
8
+ # Factory method for creating a new builder object.
10
9
  #
11
- # @return [Rutter::Builder]
10
+ # @param base [String]
11
+ # Base URL, used for generating URLs.
12
12
  #
13
- # @see Rutter::Builder
14
- def self.new(**opts, &block)
15
- Builder.new(**opts, &block)
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
@@ -1,150 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
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 router redirect incoming requests to defined endpoints. Most often
10
- # it's to a controller and an action or a Rack endpoint.
13
+ # The builder map URL's to endpoints.
11
14
  #
12
- # @attr_reader flat_map [Array<Rutter::Route>]
13
- # Defined routes.
14
- # @attr_reader verb_map [Hash<String => Array>]
15
- # Routes group by verb.
16
- # @attr_reader named_map [Hash<Symbol => Rutter::Route>]
17
- # Named routes.
18
- #
19
- # @example basic usage
20
- # Rutter.new do
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, :verb_map, :named_map
25
+ attr_reader :flat_map
26
+ attr_reader :verb_map
27
+ attr_reader :named_map
32
28
 
33
- # Initializes the router.
29
+ # Initializes the builder.
34
30
  #
35
- # @param scheme [String]
36
- # URL scheme.
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
- # Block is run inside the created `Builder` context.
35
+ # Executes the block inside the created builder context.
46
36
  #
47
- # @return [self]
48
- def initialize(
49
- scheme: "http",
50
- host: "example.com",
51
- port: 80,
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 { |hash, key| hash[key] = [] }
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
- # Mount a Rack application at the specified path.
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
- # @return [void]
74
- def mount(app, at:)
75
- Route::VERBS.each { |verb| add verb, "#{at}*_", to: app }
76
- end
77
-
78
- # Convenient method to create a redirect endpoint.
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
- # @example with custom status
94
- # Rutter.new do
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::Route]
61
+ # @return [Rutter::Scope]
104
62
  #
105
- # @see Rutter::Builder#add
106
- def root(**opts)
107
- get "/", **opts.merge(as: :root)
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
- # Adds a new route to the collection.
68
+ # Creates a scoped collection of routes with the given name as namespace.
111
69
  #
112
- # @param method [String, Symbol]
113
- # Request method.
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
- # scope path: "animals", namespace: "Species", as: "animals" do
156
- # scope path: "mammals", namespace: "Mammals", as: "mammals" do
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
- # @example with subdomain
163
- # Rutter.new do
164
- # scope path: "v1", namespace: "Api::V1", subdomain: "api" do
165
- # get "/books", to: "Books#index"
166
- # end
167
- # end
168
- def scope(**opts, &block)
169
- Scope.new(self, **opts, &block)
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
- # Transforms a named route into a URL path.
103
+ # Generates a path from the given arguments.
173
104
  #
174
105
  # @param name [Symbol]
175
- # Name of the route.
176
- # @param params [Hash]
177
- # Route paremeters.
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 [ArgumentError]
182
- # if route not found.
183
- def path(name, params = {})
184
- route = named_map[name]
185
-
186
- raise ArgumentError, "no route called '#{name}'" unless route
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
- path = route.expand(params)
189
- path = path.gsub(%r{\A[\/]+|[\/]+\z}, "")
190
- "/#{path}"
139
+ route.expand(*args)
191
140
  end
192
141
 
193
- # Transforms a named route into a full URL with scheme and host.
142
+ # Generates a full URL from the given arguments.
194
143
  #
195
144
  # @param name [Symbol]
196
- # Name of the route.
197
- # @option params [String] :_scheme (configuration.scheme)
198
- # Override scheme.
199
- # @option params [String] :_host (configuration.host)
200
- # Override host.
201
- # @option params [String] :_subdomain (nil)
202
- # Set a specific subdomain for the URL.
203
- # @option params [Symbol] :_port (configuration.port)
204
- # Override port. The port will only be visible unless it's set to `80`
205
- # or `443`.
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 [ArgumentError]
210
- # If route not found.
211
- def url(name, params = {})
212
- scheme = params.delete(:_scheme) || @scheme
213
- host = params.delete(:_host) || @host
214
- port = (params.delete(:_port) || @port).to_i
215
- host = "#{host}:#{port}" unless [80, 443].include?(port)
216
- subdomain = params.delete(:_subdomain)
217
- host = "#{subdomain}.#{host}" if subdomain
218
- "#{scheme}://#{host}#{path(name, params)}"
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
- # Process the request and is compatible with the Rack protocol.
184
+ # Add a new, frozen, route to the map.
222
185
  #
223
- # @param env [Hash]
224
- # Rack environment hash.
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 [Array]
227
- # Serialized Rack response.
197
+ # @return [Rutter::Route]
228
198
  #
229
- # @see http://rack.github.io
199
+ # @raise [ArgumentError]
200
+ # If verb is unsupported.
230
201
  #
231
202
  # @private
232
- def call(env)
233
- if (route = match(env))
234
- env["rutter.action"] = route.endpoint[:action]
235
- env["rutter.params"] = route.params(env["PATH_INFO"])
236
- ctrl = route.endpoint[:controller]
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
- NOT_FOUND_RESPONSE
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
- # Freezes the router and its routes.
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
- named_map.freeze
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
- private
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
- # Matches the incoming request with the routes.
237
+ # Process the request and is compatible with the Rack protocol.
238
+ #
239
+ # @param env [Hash]
240
+ # Rack environment hash.
258
241
  #
259
- # @param env [Hash] Rack's environment hash.
242
+ # @return [Array]
243
+ # Serialized Rack response.
260
244
  #
261
- # @return [Rutter::Route, false]
245
+ # @see http://rack.github.io
262
246
  #
263
247
  # @private
264
- def match(env)
265
- path = (env["PATH_INFO"] || "/").downcase
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
- # @private
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
- if named_map.key?(name)
281
- raise "a route called '#{name}' has already been defined"
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
- named_map[name] = route
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