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