rutter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +24 -0
- data/.travis.yml +22 -0
- data/.yardopts +2 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/Rakefile +18 -0
- data/bench/config.ru +15 -0
- data/bench/dynamic_routes +20 -0
- data/bench/expand +19 -0
- data/bench/helper.rb +19 -0
- data/bench/mount +32 -0
- data/bench/routes_helper +24 -0
- data/bench/static_routes +20 -0
- data/lib/rutter/builder.rb +306 -0
- data/lib/rutter/route.rb +168 -0
- data/lib/rutter/routes.rb +35 -0
- data/lib/rutter/scope.rb +68 -0
- data/lib/rutter/version.rb +6 -0
- data/lib/rutter.rb +17 -0
- data/rutter.gemspec +30 -0
- data/spec/integration/mount_spec.rb +20 -0
- data/spec/integration/params_spec.rb +28 -0
- data/spec/integration/rack_spec.rb +32 -0
- data/spec/integration/redirect_spec.rb +33 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/assertions.rb +7 -0
- data/spec/unit/builder_spec.rb +116 -0
- data/spec/unit/route_spec.rb +88 -0
- data/spec/unit/routes_spec.rb +29 -0
- data/spec/unit/rutter_spec.rb +15 -0
- data/spec/unit/scope_spec.rb +76 -0
- metadata +171 -0
@@ -0,0 +1,306 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
require_relative "route"
|
6
|
+
require_relative "scope"
|
7
|
+
|
8
|
+
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.
|
11
|
+
#
|
12
|
+
# @attr_reader [Array<Rutter::Route>] flat_map
|
13
|
+
# Defined routes.
|
14
|
+
# @attr_reader [Hash<String => Array>] verb_map
|
15
|
+
# Routes group by verb.
|
16
|
+
# @attr_reader [Hash<Symbol => Rutter::Route>] named_map
|
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
|
30
|
+
class Builder
|
31
|
+
attr_reader :flat_map, :verb_map, :named_map
|
32
|
+
|
33
|
+
# Initializes the router.
|
34
|
+
#
|
35
|
+
# @param [String] scheme
|
36
|
+
# URL scheme.
|
37
|
+
# @param [String] host
|
38
|
+
# URL host.
|
39
|
+
# @param [Integer] port
|
40
|
+
# URL port.
|
41
|
+
# @param [String] controller_suffix
|
42
|
+
# Suffix string for controllers (BooksController).
|
43
|
+
#
|
44
|
+
# @yield
|
45
|
+
# Block is run inside the created `Builder` context.
|
46
|
+
#
|
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
|
59
|
+
@flat_map = []
|
60
|
+
@verb_map = Hash.new { |hash, key| hash[key] = [] }
|
61
|
+
@named_map = {}
|
62
|
+
|
63
|
+
instance_eval(&block) if block_given?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Mount a Rack application at the specified path.
|
67
|
+
#
|
68
|
+
# @param [Class, Object] app
|
69
|
+
# A class or an object that responds to `call`.
|
70
|
+
# @param [String] at
|
71
|
+
# Path prefix to match.
|
72
|
+
#
|
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 [String] destination
|
81
|
+
# The destination.
|
82
|
+
# @param [Integer] status
|
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
|
92
|
+
#
|
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 '/').
|
102
|
+
#
|
103
|
+
# @return [Rutter::Route]
|
104
|
+
#
|
105
|
+
# @see Rutter::Builder#add
|
106
|
+
def root(**opts)
|
107
|
+
get "/", **opts.merge(as: :root)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Adds a new route to the collection.
|
111
|
+
#
|
112
|
+
# @param [String, Symbol] method
|
113
|
+
# Request method.
|
114
|
+
# @param [String] path
|
115
|
+
# Path template.
|
116
|
+
# @param [String, Symbol] as
|
117
|
+
# Route identifier (name).
|
118
|
+
# @param [String, Proc] to
|
119
|
+
# Route endpoint.
|
120
|
+
#
|
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
|
+
# @yield
|
149
|
+
# Scope context.
|
150
|
+
#
|
151
|
+
# @return [Rutter::Scope]
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# 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
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
#
|
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)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Transforms a named route into a URL path.
|
173
|
+
#
|
174
|
+
# @param [Symbol] name
|
175
|
+
# Name of the route.
|
176
|
+
# @param [Hash] params
|
177
|
+
# Route paremeters.
|
178
|
+
#
|
179
|
+
# @return [String]
|
180
|
+
#
|
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
|
187
|
+
|
188
|
+
path = route.expand(params)
|
189
|
+
path = path.gsub(%r{\A[\/]+|[\/]+\z}, "")
|
190
|
+
"/#{path}"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Transforms a named route into a full URL with scheme and host.
|
194
|
+
#
|
195
|
+
# @param [Symbol] name
|
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`.
|
206
|
+
#
|
207
|
+
# @return [String]
|
208
|
+
#
|
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)}"
|
219
|
+
end
|
220
|
+
|
221
|
+
# Process the request and is compatible with the Rack protocol.
|
222
|
+
#
|
223
|
+
# @param [Hash] env
|
224
|
+
# Rack environment hash.
|
225
|
+
#
|
226
|
+
# @return [Array]
|
227
|
+
# Serialized Rack response.
|
228
|
+
#
|
229
|
+
# @see http://rack.github.io
|
230
|
+
#
|
231
|
+
# @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)
|
239
|
+
end
|
240
|
+
|
241
|
+
NOT_FOUND_RESPONSE
|
242
|
+
end
|
243
|
+
|
244
|
+
# Freezes the router and its routes.
|
245
|
+
#
|
246
|
+
# @return [self]
|
247
|
+
def freeze
|
248
|
+
flat_map.freeze
|
249
|
+
verb_map.freeze
|
250
|
+
named_map.freeze
|
251
|
+
|
252
|
+
super
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
# Matches the incoming request with the routes.
|
258
|
+
#
|
259
|
+
# @param env [Hash] Rack's environment hash.
|
260
|
+
#
|
261
|
+
# @return [Rutter::Route, false]
|
262
|
+
#
|
263
|
+
# @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
|
275
|
+
|
276
|
+
# @private
|
277
|
+
def add_named_route!(name, route)
|
278
|
+
name = normalize_route_name(name)
|
279
|
+
|
280
|
+
if named_map.key?(name)
|
281
|
+
raise "a route called '#{name}' has already been defined"
|
282
|
+
end
|
283
|
+
|
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
|
295
|
+
end
|
296
|
+
|
297
|
+
# Response returned when no route matched the request.
|
298
|
+
#
|
299
|
+
# @private
|
300
|
+
NOT_FOUND_RESPONSE = [
|
301
|
+
404,
|
302
|
+
{ "X-Cascade" => "pass" },
|
303
|
+
["Route Not Found"]
|
304
|
+
].freeze
|
305
|
+
end
|
306
|
+
end
|
data/lib/rutter/route.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module Rutter
|
6
|
+
# Represents a single route.
|
7
|
+
#
|
8
|
+
# @attr_reader [String] method
|
9
|
+
# Request method to match.
|
10
|
+
# @attr_reader [String] path
|
11
|
+
# Path to match.
|
12
|
+
# @attr_reader [Hash] endpoint
|
13
|
+
# Route endpoint.
|
14
|
+
class Route
|
15
|
+
# Valid request verbs.
|
16
|
+
VERBS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
|
17
|
+
|
18
|
+
attr_reader :method, :path, :endpoint
|
19
|
+
|
20
|
+
# Initializes the route.
|
21
|
+
#
|
22
|
+
# @param [Symbol, String] method
|
23
|
+
# Requets method to match.
|
24
|
+
# @param [String] path
|
25
|
+
# Path template.
|
26
|
+
# @param [String, Proc, Hash] endpoint
|
27
|
+
# Route endpoint.
|
28
|
+
# @param [String, false] controller_suffix
|
29
|
+
# Suffix for string controllers.
|
30
|
+
#
|
31
|
+
# @return [self]
|
32
|
+
#
|
33
|
+
# @raise [ArgumentError]
|
34
|
+
# If request method is unknown.
|
35
|
+
#
|
36
|
+
# @private
|
37
|
+
def initialize(method, path, endpoint, controller_suffix: nil)
|
38
|
+
@method = method.to_s.upcase
|
39
|
+
|
40
|
+
unless VERBS.include?(method)
|
41
|
+
raise ArgumentError, "invalid verb: '#{method}'"
|
42
|
+
end
|
43
|
+
|
44
|
+
@controller_suffix = controller_suffix || ""
|
45
|
+
@path = normalize_path(path)
|
46
|
+
@dynamic = false
|
47
|
+
@pattern = path_to_pattern(@path).freeze
|
48
|
+
@endpoint = build_endpoint_hash(endpoint).freeze
|
49
|
+
|
50
|
+
freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
# Matches the given path to the route pattern.
|
54
|
+
#
|
55
|
+
# @param [String] path_info
|
56
|
+
# Requested path to match against.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def match?(path_info)
|
60
|
+
@dynamic ? path_info.match?(@pattern) : path_info == @path
|
61
|
+
end
|
62
|
+
|
63
|
+
# Extract params from the given path.
|
64
|
+
#
|
65
|
+
# NOTE: If the path does not match, `nil` is returned.
|
66
|
+
#
|
67
|
+
# @param [String] path
|
68
|
+
# Requested path to extract params from.
|
69
|
+
#
|
70
|
+
# @return [nil, Hash<String => String>]
|
71
|
+
def params(path)
|
72
|
+
return unless (result = path.match(@pattern))
|
73
|
+
values = result.captures.map { |v| CGI.unescape(v) if v }
|
74
|
+
Hash[result.names.zip(values)]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Expands the pattern with the given arguments.
|
78
|
+
#
|
79
|
+
# @param [Hash] params
|
80
|
+
# Expand parameters.
|
81
|
+
#
|
82
|
+
# @return [String] Expanded string.
|
83
|
+
#
|
84
|
+
# @example basic example
|
85
|
+
# route = Route.new("GET", "/books/:id/:title", -> {})
|
86
|
+
#
|
87
|
+
# route.expand(id: 54, title: "eloquent-ruby")
|
88
|
+
# # => "/books/54/eloquent-ruby"
|
89
|
+
#
|
90
|
+
# @example with query
|
91
|
+
# route = Route.new("GET", "/books/:id/reviews", -> {})
|
92
|
+
#
|
93
|
+
# route.expand(id: 54, status: "approved")
|
94
|
+
# # => "/books/54/reviews?status=approved"
|
95
|
+
def expand(params = {})
|
96
|
+
string = if @dynamic
|
97
|
+
new_path = path.gsub(SEGMENT_MATCH) do |match|
|
98
|
+
params.delete(match[1..-1].to_sym)
|
99
|
+
end
|
100
|
+
new_path.gsub(/\(|\)/, "")
|
101
|
+
else
|
102
|
+
path.dup
|
103
|
+
end
|
104
|
+
|
105
|
+
string = normalize_path(string)
|
106
|
+
return string if params.empty?
|
107
|
+
|
108
|
+
"#{string}?#{Rack::Utils.build_nested_query(params)}"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Segment for dynamic parts.
|
114
|
+
#
|
115
|
+
# @private
|
116
|
+
DYNAMIC_SEGMENT = "[^/.,;?]+"
|
117
|
+
|
118
|
+
# Segment for wildcards.
|
119
|
+
#
|
120
|
+
# @private
|
121
|
+
WILDCARD_SEGMENT = ".*"
|
122
|
+
|
123
|
+
# Match segment regexp.
|
124
|
+
#
|
125
|
+
# @private
|
126
|
+
SEGMENT_MATCH = /((:|\*)[\w]+)/
|
127
|
+
|
128
|
+
# @private
|
129
|
+
def path_to_pattern(path)
|
130
|
+
pattern = path.dup
|
131
|
+
|
132
|
+
if pattern.include?("(")
|
133
|
+
@dynamic = true
|
134
|
+
pattern = pattern.gsub("(", "(?:").gsub(")", ")?")
|
135
|
+
end
|
136
|
+
|
137
|
+
pattern = pattern.gsub(SEGMENT_MATCH) do |match|
|
138
|
+
@dynamic = true
|
139
|
+
segment = match[0] == ":" ? DYNAMIC_SEGMENT : WILDCARD_SEGMENT
|
140
|
+
"(?<#{match[1..-1]}>#{segment})"
|
141
|
+
end
|
142
|
+
|
143
|
+
/\A#{pattern}\z/
|
144
|
+
end
|
145
|
+
|
146
|
+
# @private
|
147
|
+
def normalize_path(path)
|
148
|
+
path = path.gsub(%r{[\/]{2,}}, "/")
|
149
|
+
.gsub(%r{\A[\/]+|[\/]+\z}, "")
|
150
|
+
.downcase
|
151
|
+
|
152
|
+
"/#{path}"
|
153
|
+
end
|
154
|
+
|
155
|
+
# @private
|
156
|
+
def build_endpoint_hash(endpoint)
|
157
|
+
if endpoint.is_a?(String)
|
158
|
+
ctrl, action = endpoint.gsub(/[:]{3,}/, "::")
|
159
|
+
.gsub(/\A[:]+|[:]+\z/, "")
|
160
|
+
.split("#")
|
161
|
+
|
162
|
+
{ controller: "#{ctrl}#{@controller_suffix}", action: action }
|
163
|
+
else
|
164
|
+
{ controller: endpoint, action: nil }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Rutter
|
6
|
+
# Routes helper.
|
7
|
+
class Routes
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# Delegate path and url method to the builder.
|
11
|
+
#
|
12
|
+
# @see Rutter::Builder#path
|
13
|
+
# @see Rutter::Builder#url
|
14
|
+
def_delegators :@builder, :path, :url
|
15
|
+
|
16
|
+
# Initializes the helper.
|
17
|
+
#
|
18
|
+
# @param [Rutter::Builder] builder
|
19
|
+
# Route builder object.
|
20
|
+
#
|
21
|
+
# @return [self]
|
22
|
+
def initialize(builder)
|
23
|
+
@builder = builder
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
# @private
|
29
|
+
def method_missing(method, *args)
|
30
|
+
named_route, type = method.to_s.split(/\_(path|url)\z/)
|
31
|
+
return super unless type
|
32
|
+
@builder.public_send(type, named_route.to_sym, *args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rutter/scope.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rutter
|
4
|
+
# Scoped set of routes.
|
5
|
+
class Scope
|
6
|
+
# Initializes the scope.
|
7
|
+
#
|
8
|
+
# @param [Rutter::Builder] router
|
9
|
+
# Root router object.
|
10
|
+
# @option opts [String] :path (nil)
|
11
|
+
# Path prefix
|
12
|
+
# @option opts [String] :namespace (nil)
|
13
|
+
# Namespace prefix
|
14
|
+
# @option opts [String, Symbol] :as (nil)
|
15
|
+
# Name prefix
|
16
|
+
# @yield
|
17
|
+
# Scope context.
|
18
|
+
#
|
19
|
+
# @return [self]
|
20
|
+
#
|
21
|
+
# @private
|
22
|
+
def initialize(router, **opts, &block)
|
23
|
+
@router = router
|
24
|
+
@path_prefix = opts[:path]
|
25
|
+
@namespace_prefix = opts[:namespace]
|
26
|
+
@name_prefix = opts[:as]
|
27
|
+
|
28
|
+
instance_eval(&block) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see Rutter::Builder#scope
|
32
|
+
def scope(**opts, &block)
|
33
|
+
opts[:as] = "#{@name_prefix}_#{opts[:as]}" if @name_prefix
|
34
|
+
opts[:path] = "#{@path_prefix}/#{opts[:path]}" if @path_prefix
|
35
|
+
|
36
|
+
if @namespace_prefix
|
37
|
+
opts[:namespace] = "#{@namespace_prefix}::#{opts[:namespace]}"
|
38
|
+
end
|
39
|
+
|
40
|
+
Scope.new(@router, **opts, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @see Rutter::Builder#mount
|
44
|
+
def mount(app, at:, **opts)
|
45
|
+
at = "#{@path_prefix}/#{at}" if @path_prefix
|
46
|
+
@router.mount app, at: at, **opts
|
47
|
+
end
|
48
|
+
|
49
|
+
# @see Rutter::Builder#root
|
50
|
+
def root(to:, as: :root)
|
51
|
+
get "/", to: to, as: as
|
52
|
+
end
|
53
|
+
|
54
|
+
# @see Rutter::Builder#add
|
55
|
+
Route::VERBS.each do |verb|
|
56
|
+
verb_method = verb.downcase
|
57
|
+
define_method verb_method do |path, **opts|
|
58
|
+
path = "#{@path_prefix}/#{path}" if @path_prefix
|
59
|
+
opts[:as] = "#{@name_prefix}_#{opts[:as]}" if opts[:as]
|
60
|
+
if @namespace_prefix && opts[:to].is_a?(String)
|
61
|
+
opts[:to] = "#{@namespace_prefix}::#{opts[:to]}"
|
62
|
+
end
|
63
|
+
|
64
|
+
@router.public_send verb_method, path, **opts
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/rutter.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rutter/version"
|
4
|
+
require_relative "rutter/builder"
|
5
|
+
require_relative "rutter/routes"
|
6
|
+
|
7
|
+
# HTTP router for Ramverk and Rack.
|
8
|
+
module Rutter
|
9
|
+
# Creates a new builder object.
|
10
|
+
#
|
11
|
+
# @return [Rutter::Builder]
|
12
|
+
#
|
13
|
+
# @see Rutter::Builder
|
14
|
+
def self.new(**opts, &block)
|
15
|
+
Builder.new(**opts, &block)
|
16
|
+
end
|
17
|
+
end
|
data/rutter.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("../lib/rutter/version", __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "rutter"
|
7
|
+
spec.version = Rutter::VERSION
|
8
|
+
spec.summary = "HTTP router for Rack."
|
9
|
+
|
10
|
+
spec.required_ruby_version = ">= 2.4.0"
|
11
|
+
spec.required_rubygems_version = ">= 2.5.0"
|
12
|
+
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.author = "Tobias Sandelius"
|
16
|
+
spec.email = "tobias@sandeli.us"
|
17
|
+
spec.homepage = "https://github.com/sandelius/rutter"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "rack", "~> 2.0"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "minitest"
|
28
|
+
spec.add_development_dependency "mocha"
|
29
|
+
spec.add_development_dependency "rack-test"
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe "Rutter::Builder#mount" do
|
6
|
+
let(:router) do
|
7
|
+
Rutter.new do
|
8
|
+
mount ->(_env) { [200, {}, ["Admin App"]] }, at: "/admin"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def app
|
13
|
+
router.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
it "forwards the request to the given application" do
|
17
|
+
get "/admin/with/sub/paths"
|
18
|
+
assert_equal "Admin App", last_response.body
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
class TestController
|
7
|
+
def self.call(env)
|
8
|
+
hash = env["rutter.params"]
|
9
|
+
hash["action"] = env["rutter.action"]
|
10
|
+
[100, {}, [JSON.generate(hash)]]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "ramverk.router_params" do
|
15
|
+
let(:router) { Rutter.new(controller_suffix: "Controller") }
|
16
|
+
|
17
|
+
def app
|
18
|
+
router.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
it "calls the endpoint (controller)" do
|
22
|
+
router.get "/books/:title", to: "Test#index"
|
23
|
+
get "/books/eloquent-ruby"
|
24
|
+
assert_equal 100, last_response.status
|
25
|
+
assert_equal '{"title":"eloquent-ruby","action":"index"}',
|
26
|
+
last_response.body
|
27
|
+
end
|
28
|
+
end
|