rutter 0.1.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 +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
|