pakyow-routing 1.0.0.rc2 → 1.0.0.rc3
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
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52a52fa15197c4a3509beb255ae01273b7872478f60e19c5ec5833413ebb48c4
|
4
|
+
data.tar.gz: 9379faaa3d68a7cf1ded42adf87b6de01ed1616f591e565f4f374684fbc3fc04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 519751841aba45aa608ef7cdc9767b798b79897d14cc4384fd9bda08472442d45177f68a2c616d75f07cd6b20bc3546dd8e814a6c446be0110bde259917522bd
|
7
|
+
data.tar.gz: 2f7ca81b72afd2b670a884e0b0383d77c1bb0111ae1a6f786797089e8a5fca15494f0f8e553ed1f7d7a7c969f6b06d647206e3092f386e0a003cdff79189397f
|
@@ -19,754 +19,766 @@ require "pakyow/routing/controller/behavior/error_handling"
|
|
19
19
|
require "pakyow/routing/controller/behavior/param_verification"
|
20
20
|
|
21
21
|
module Pakyow
|
22
|
-
|
23
|
-
|
24
|
-
# Pakyow::App.controller do
|
25
|
-
# get "/" do
|
26
|
-
# # called for GET / requests
|
27
|
-
# end
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# A +Class+ is created dynamically for each defined controller. When matched, a route is called in
|
31
|
-
# context of its controller. This means that any method defined in a controller is available to be
|
32
|
-
# called from within a route. For example:
|
33
|
-
#
|
34
|
-
# Pakyow::App.controller do
|
35
|
-
# def foo
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# get :foo, "/foo" do
|
39
|
-
# foo
|
40
|
-
# end
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
# Including modules works as expected:
|
44
|
-
#
|
45
|
-
# module AuthHelpers
|
46
|
-
# def current_user
|
47
|
-
# end
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# Pakyow::App.controller do
|
51
|
-
# include AuthHelpers
|
52
|
-
#
|
53
|
-
# get :foo, "/foo" do
|
54
|
-
# current_user
|
55
|
-
# end
|
56
|
-
# end
|
57
|
-
#
|
58
|
-
# See {App.controller} for more details on defining controllers.
|
59
|
-
#
|
60
|
-
# = Supported HTTP methods
|
61
|
-
#
|
62
|
-
# - +GET+
|
63
|
-
# - +POST+
|
64
|
-
# - +PUT+
|
65
|
-
# - +PATCH+
|
66
|
-
# - +DELETE+
|
67
|
-
#
|
68
|
-
# See {get}, {post}, {put}, {patch}, and {delete}.
|
69
|
-
#
|
70
|
-
# +HEAD+ requests are handled automatically via {Rack::Head}.
|
71
|
-
#
|
72
|
-
# = Building paths for named routes
|
73
|
-
#
|
74
|
-
# Path building is supported via {Controller#path} and {Controller#path_to}.
|
75
|
-
#
|
76
|
-
# = Reusing logic with actions
|
77
|
-
#
|
78
|
-
# Methods can be defined as additional actions for a route. For example:
|
79
|
-
#
|
80
|
-
# Pakyow::App.controller do
|
81
|
-
# action :called_before
|
82
|
-
#
|
83
|
-
# def called_before
|
84
|
-
# ...
|
85
|
-
# end
|
86
|
-
#
|
87
|
-
# get :foo, "/foo" do
|
88
|
-
# ...
|
89
|
-
# end
|
90
|
-
# end
|
91
|
-
#
|
92
|
-
# = Extending controllers
|
93
|
-
#
|
94
|
-
# Extensions can be defined and used to add shared routes to one or more controllers.
|
95
|
-
# See {Routing::Extension}.
|
96
|
-
#
|
97
|
-
# = Other routing features
|
98
|
-
#
|
99
|
-
# More advanced route features are available, including groups, namespaces, and templates. See
|
100
|
-
# {group}, {namespace}, and {template}.
|
101
|
-
#
|
102
|
-
# = Controller subclasses
|
103
|
-
#
|
104
|
-
# It's possible to work with controllers outside of Pakyow's DSL. For example:
|
105
|
-
#
|
106
|
-
# class FooController < Pakyow::Controller("/foo")
|
107
|
-
# default do
|
108
|
-
# # available at GET /foo
|
109
|
-
# end
|
110
|
-
# end
|
111
|
-
#
|
112
|
-
# Pakyow::App.controller << FooController
|
113
|
-
#
|
114
|
-
# = Custom matchers
|
115
|
-
#
|
116
|
-
# Controllers and routes can be defined with a matcher rather than a path. The matcher could be a
|
117
|
-
# +Regexp+ or any custom object that implements +match?+. For example:
|
118
|
-
#
|
119
|
-
# class CustomMatcher
|
120
|
-
# def match?(path)
|
121
|
-
# path == "/custom"
|
122
|
-
# end
|
123
|
-
# end
|
124
|
-
#
|
125
|
-
# Pakyow::App.controller CustomMatcher.new do
|
126
|
-
# end
|
127
|
-
#
|
128
|
-
# Custom matchers can also make data available in +params+ by implementing +match+ and returning
|
129
|
-
# an object that implements +named_captures+. For example:
|
130
|
-
#
|
131
|
-
# class CustomMatcher
|
132
|
-
# def match?(path)
|
133
|
-
# path == "/custom"
|
134
|
-
# end
|
135
|
-
#
|
136
|
-
# def match(path)
|
137
|
-
# return self if match?(path)
|
138
|
-
# end
|
139
|
-
#
|
140
|
-
# def named_captures
|
141
|
-
# { foo: "bar" }
|
142
|
-
# end
|
143
|
-
# end
|
144
|
-
#
|
145
|
-
# Pakyow::App.controller CustomMatcher.new do
|
146
|
-
# end
|
147
|
-
#
|
148
|
-
class Controller
|
149
|
-
using Support::DeepDup
|
150
|
-
extend Support::Makeable
|
151
|
-
extend Support::ClassState
|
152
|
-
|
153
|
-
include Support::Hookable
|
154
|
-
events :dispatch
|
155
|
-
|
156
|
-
include Routing::Behavior::ErrorHandling
|
157
|
-
include Routing::Behavior::ParamVerification
|
158
|
-
|
159
|
-
include Support::Pipeline
|
160
|
-
|
161
|
-
using Support::Refinements::String::Normalization
|
162
|
-
|
163
|
-
controller = self
|
164
|
-
Pakyow.singleton_class.class_eval do
|
165
|
-
define_method :Controller do |path|
|
166
|
-
controller.Controller(path)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
METHOD_GET = :get
|
171
|
-
METHOD_HEAD = :head
|
172
|
-
METHOD_POST = :post
|
173
|
-
METHOD_PUT = :put
|
174
|
-
METHOD_PATCH = :patch
|
175
|
-
METHOD_DELETE = :delete
|
176
|
-
|
177
|
-
DEFINABLE_HTTP_METHODS = [
|
178
|
-
METHOD_GET,
|
179
|
-
METHOD_POST,
|
180
|
-
METHOD_PUT,
|
181
|
-
METHOD_PATCH,
|
182
|
-
METHOD_DELETE
|
183
|
-
].freeze
|
184
|
-
|
185
|
-
CONTENT_DISPOSITION = "Content-Disposition".freeze
|
186
|
-
|
187
|
-
require "pakyow/routing/expansion"
|
188
|
-
|
189
|
-
# Controllers must be initialized with an argument, even though the argument
|
190
|
-
# isn't actually used. This is a side-effect of allowing templates to define
|
191
|
-
# a route named "new". In the expansion, we differentiate between expanding
|
192
|
-
# and initializing by whether an argument is present, which works because
|
193
|
-
# arguments aren't passed for expansions.
|
194
|
-
#
|
195
|
-
def initialize(app)
|
196
|
-
@children = self.class.children.map { |child|
|
197
|
-
child.new(app)
|
198
|
-
}
|
199
|
-
|
200
|
-
self.class.routes.values.flatten.each do |route|
|
201
|
-
route.pipeline = self.class.__pipeline.dup
|
202
|
-
|
203
|
-
self.class.limit_by_route[route.name].to_a.reverse.each do |limit|
|
204
|
-
if index = route.pipeline.actions.index(limit[:after])
|
205
|
-
route.pipeline.actions.insert(index + 1, limit[:insert])
|
206
|
-
else
|
207
|
-
route.pipeline.actions << limit[:insert]
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
route.pipeline.actions.delete_if do |action|
|
212
|
-
self.class.global_skips.to_a.include?(action.name) ||
|
213
|
-
self.class.skips_by_route[route.name].to_a.include?(action.name)
|
214
|
-
end
|
215
|
-
|
216
|
-
route.pipeline.actions << Support::Pipeline::Action.new(:dispatch)
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
def call(connection, request_path = connection.path)
|
221
|
-
request_method = connection.method
|
222
|
-
if request_method == METHOD_HEAD
|
223
|
-
request_method = METHOD_GET
|
224
|
-
end
|
225
|
-
|
226
|
-
matcher = self.class.matcher
|
227
|
-
if match = matcher.match(request_path)
|
228
|
-
match_data = match.named_captures
|
229
|
-
connection.params.merge!(match_data)
|
230
|
-
|
231
|
-
if matcher.is_a?(Regexp)
|
232
|
-
request_path = String.normalize_path(request_path.sub(matcher, ""))
|
233
|
-
end
|
234
|
-
|
235
|
-
@children.each do |child_controller|
|
236
|
-
child_controller.call(connection, request_path)
|
237
|
-
break if connection.halted?
|
238
|
-
end
|
239
|
-
|
240
|
-
unless connection.halted?
|
241
|
-
self.class.routes[request_method].to_a.each do |route|
|
242
|
-
catch :reject do
|
243
|
-
if route_match = route.match(request_path)
|
244
|
-
connection.params.merge!(route_match.named_captures)
|
245
|
-
|
246
|
-
connection.set(
|
247
|
-
:__endpoint_path,
|
248
|
-
String.normalize_path(
|
249
|
-
File.join(
|
250
|
-
self.class.path_to_self.to_s, route.path.to_s
|
251
|
-
)
|
252
|
-
)
|
253
|
-
)
|
254
|
-
|
255
|
-
connection.set(:__endpoint_name, route.name)
|
256
|
-
|
257
|
-
dup.call_route(connection, route)
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
break if connection.halted?
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def call_route(connection, route)
|
268
|
-
@connection, @route = connection, route
|
269
|
-
@route.pipeline.callable(self).call(connection); halt
|
270
|
-
rescue StandardError => error
|
271
|
-
@connection.logger.houston(error)
|
272
|
-
handle_error(error)
|
273
|
-
end
|
274
|
-
|
275
|
-
def dispatch
|
276
|
-
halted = false
|
277
|
-
performing :dispatch do
|
278
|
-
halted = catch :halt do
|
279
|
-
@route.call(self)
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
# Catching the halt then re-halting lets us call after dispatch hooks in non-error cases.
|
284
|
-
#
|
285
|
-
halt if halted
|
286
|
-
end
|
287
|
-
|
288
|
-
# Redirects to +location+ and immediately halts request processing.
|
22
|
+
module Routing
|
23
|
+
# Executes code for particular requests. For example:
|
289
24
|
#
|
290
|
-
# @param location [String] what url the request should be redirected to
|
291
|
-
# @param as [Integer, Symbol] the status to redirect with
|
292
|
-
# @param trusted [Boolean] whether or not the location is trusted
|
293
|
-
#
|
294
|
-
# @example Redirecting:
|
295
25
|
# Pakyow::App.controller do
|
296
|
-
#
|
297
|
-
#
|
26
|
+
# get "/" do
|
27
|
+
# # called for GET / requests
|
298
28
|
# end
|
299
29
|
# end
|
300
30
|
#
|
301
|
-
#
|
31
|
+
# A +Class+ is created dynamically for each defined controller. When matched, a route is called in
|
32
|
+
# context of its controller. This means that any method defined in a controller is available to be
|
33
|
+
# called from within a route. For example:
|
34
|
+
#
|
302
35
|
# Pakyow::App.controller do
|
303
|
-
#
|
304
|
-
#
|
36
|
+
# def foo
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# get :foo, "/foo" do
|
40
|
+
# foo
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Including modules works as expected:
|
45
|
+
#
|
46
|
+
# module AuthHelpers
|
47
|
+
# def current_user
|
305
48
|
# end
|
306
49
|
# end
|
307
50
|
#
|
308
|
-
# @example Redirecting to a remote location:
|
309
51
|
# Pakyow::App.controller do
|
310
|
-
#
|
311
|
-
#
|
52
|
+
# include AuthHelpers
|
53
|
+
#
|
54
|
+
# get :foo, "/foo" do
|
55
|
+
# current_user
|
312
56
|
# end
|
313
57
|
# end
|
314
58
|
#
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
)
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
# Reroutes the request to a different location. Instead of an http redirect, the request will
|
335
|
-
# continued to be handled in the current request lifecycle.
|
59
|
+
# See {App.controller} for more details on defining controllers.
|
60
|
+
#
|
61
|
+
# = Supported HTTP methods
|
62
|
+
#
|
63
|
+
# - +GET+
|
64
|
+
# - +POST+
|
65
|
+
# - +PUT+
|
66
|
+
# - +PATCH+
|
67
|
+
# - +DELETE+
|
68
|
+
#
|
69
|
+
# See {get}, {post}, {put}, {patch}, and {delete}.
|
70
|
+
#
|
71
|
+
# +HEAD+ requests are handled automatically via {Rack::Head}.
|
72
|
+
#
|
73
|
+
# = Building paths for named routes
|
336
74
|
#
|
337
|
-
#
|
338
|
-
# @param method [Symbol] the http method to reroute as
|
75
|
+
# Path building is supported via {Controller#path} and {Controller#path_to}.
|
339
76
|
#
|
340
|
-
#
|
341
|
-
# Pakyow::App.resource :posts, "/posts" do
|
342
|
-
# edit do
|
343
|
-
# @post ||= find_post_by_id(params[:post_id])
|
77
|
+
# = Reusing logic with actions
|
344
78
|
#
|
345
|
-
#
|
79
|
+
# Methods can be defined as additional actions for a route. For example:
|
80
|
+
#
|
81
|
+
# Pakyow::App.controller do
|
82
|
+
# action :called_before
|
83
|
+
#
|
84
|
+
# def called_before
|
85
|
+
# ...
|
346
86
|
# end
|
347
87
|
#
|
348
|
-
#
|
349
|
-
#
|
350
|
-
# @post = failed_post_object
|
351
|
-
# reroute path(:posts_edit, post_id: @post.id), method: :get
|
352
|
-
# end
|
88
|
+
# get :foo, "/foo" do
|
89
|
+
# ...
|
353
90
|
# end
|
354
91
|
# end
|
355
92
|
#
|
356
|
-
|
357
|
-
connection = @connection.__getobj__
|
358
|
-
|
359
|
-
# Make sure the endpoint is set.
|
360
|
-
#
|
361
|
-
connection.endpoint
|
362
|
-
|
363
|
-
connection.instance_variable_set(:@method, method)
|
364
|
-
connection.instance_variable_set(:@path, location.is_a?(Symbol) ? app.endpoints.path(location, **params) : location)
|
365
|
-
|
366
|
-
# Change the response status, if set.
|
367
|
-
#
|
368
|
-
connection.status = Connection::Statuses.code(as) if as
|
369
|
-
|
370
|
-
@connection.set(:__endpoint_path, nil)
|
371
|
-
@connection.set(:__endpoint_name, nil)
|
372
|
-
|
373
|
-
app.perform(@connection); halt
|
374
|
-
end
|
375
|
-
|
376
|
-
# Responds to a specific request format.
|
93
|
+
# = Extending controllers
|
377
94
|
#
|
378
|
-
#
|
379
|
-
#
|
95
|
+
# Extensions can be defined and used to add shared routes to one or more controllers.
|
96
|
+
# See {Routing::Extension}.
|
380
97
|
#
|
381
|
-
#
|
98
|
+
# = Other routing features
|
382
99
|
#
|
383
|
-
#
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
# send "foo"
|
388
|
-
# end
|
100
|
+
# More advanced route features are available, including groups, namespaces, and templates. See
|
101
|
+
# {group}, {namespace}, and {template}.
|
102
|
+
#
|
103
|
+
# = Controller subclasses
|
389
104
|
#
|
390
|
-
#
|
105
|
+
# It's possible to work with controllers outside of Pakyow's DSL. For example:
|
106
|
+
#
|
107
|
+
# class FooController < Pakyow::Routing::Controller("/foo")
|
108
|
+
# default do
|
109
|
+
# # available at GET /foo
|
391
110
|
# end
|
392
111
|
# end
|
393
112
|
#
|
394
|
-
|
395
|
-
return unless @connection.format == format.to_sym
|
396
|
-
@connection.format = format
|
397
|
-
yield
|
398
|
-
halt
|
399
|
-
end
|
400
|
-
|
401
|
-
DEFAULT_SEND_TYPE = "application/octet-stream".freeze
|
402
|
-
|
403
|
-
# Sends a file or other data in the response.
|
113
|
+
# Pakyow::App.controller << FooController
|
404
114
|
#
|
405
|
-
#
|
406
|
-
# determined automatically. The type can be set explicitly with the +type+ option.
|
115
|
+
# = Custom matchers
|
407
116
|
#
|
408
|
-
#
|
409
|
-
#
|
117
|
+
# Controllers and routes can be defined with a matcher rather than a path. The matcher could be a
|
118
|
+
# +Regexp+ or any custom object that implements +match?+. For example:
|
410
119
|
#
|
411
|
-
#
|
412
|
-
#
|
413
|
-
#
|
414
|
-
# send "foo", type: "text/plain"
|
120
|
+
# class CustomMatcher
|
121
|
+
# def match?(path)
|
122
|
+
# path == "/custom"
|
415
123
|
# end
|
416
124
|
# end
|
417
125
|
#
|
418
|
-
#
|
419
|
-
#
|
420
|
-
#
|
421
|
-
#
|
422
|
-
#
|
126
|
+
# Pakyow::App.controller CustomMatcher.new do
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# Custom matchers can also make data available in +params+ by implementing +match+ and returning
|
130
|
+
# an object that implements +named_captures+. For example:
|
131
|
+
#
|
132
|
+
# class CustomMatcher
|
133
|
+
# def match?(path)
|
134
|
+
# path == "/custom"
|
423
135
|
# end
|
136
|
+
#
|
137
|
+
# def match(path)
|
138
|
+
# return self if match?(path)
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# def named_captures
|
142
|
+
# { foo: "bar" }
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# Pakyow::App.controller CustomMatcher.new do
|
424
147
|
# end
|
425
148
|
#
|
426
|
-
|
427
|
-
|
428
|
-
|
149
|
+
class Controller
|
150
|
+
using Support::DeepDup
|
151
|
+
extend Support::Makeable
|
152
|
+
extend Support::ClassState
|
429
153
|
|
430
|
-
|
431
|
-
|
432
|
-
type ||= Rack::Mime.mime_type(File.extname(file_or_data.path))
|
433
|
-
end
|
154
|
+
include Support::Hookable
|
155
|
+
events :dispatch
|
434
156
|
|
435
|
-
|
436
|
-
|
437
|
-
@connection.set_header(Rack::CONTENT_LENGTH, file_or_data.bytesize)
|
438
|
-
@connection.set_header(Rack::CONTENT_TYPE, type) if type
|
439
|
-
data = StringIO.new(file_or_data)
|
440
|
-
else
|
441
|
-
raise ArgumentError, "expected an IO or String object"
|
442
|
-
end
|
157
|
+
include Routing::Behavior::ErrorHandling
|
158
|
+
include Routing::Behavior::ParamVerification
|
443
159
|
|
444
|
-
|
445
|
-
halt(data)
|
446
|
-
end
|
160
|
+
include Support::Pipeline
|
447
161
|
|
448
|
-
|
449
|
-
#
|
450
|
-
# The response body will be set to +body+ prior to halting (if it's a non-nil value).
|
451
|
-
#
|
452
|
-
def halt(body = nil, status: nil)
|
453
|
-
@connection.body = body if body
|
454
|
-
@connection.status = Connection::Statuses.code(status) if status
|
455
|
-
@connection.halt
|
456
|
-
end
|
162
|
+
using Support::Refinements::String::Normalization
|
457
163
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
164
|
+
controller = self
|
165
|
+
Pakyow::Routing.singleton_class.class_eval do
|
166
|
+
define_method :Controller do |path|
|
167
|
+
controller.Controller(path)
|
168
|
+
end
|
169
|
+
end
|
463
170
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
171
|
+
METHOD_GET = :get
|
172
|
+
METHOD_HEAD = :head
|
173
|
+
METHOD_POST = :post
|
174
|
+
METHOD_PUT = :put
|
175
|
+
METHOD_PATCH = :patch
|
176
|
+
METHOD_DELETE = :delete
|
177
|
+
|
178
|
+
DEFINABLE_HTTP_METHODS = [
|
179
|
+
METHOD_GET,
|
180
|
+
METHOD_POST,
|
181
|
+
METHOD_PUT,
|
182
|
+
METHOD_PATCH,
|
183
|
+
METHOD_DELETE
|
184
|
+
].freeze
|
185
|
+
|
186
|
+
CONTENT_DISPOSITION = "Content-Disposition".freeze
|
187
|
+
|
188
|
+
require "pakyow/routing/expansion"
|
189
|
+
|
190
|
+
# Controllers must be initialized with an argument, even though the argument
|
191
|
+
# isn't actually used. This is a side-effect of allowing templates to define
|
192
|
+
# a route named "new". In the expansion, we differentiate between expanding
|
193
|
+
# and initializing by whether an argument is present, which works because
|
194
|
+
# arguments aren't passed for expansions.
|
195
|
+
#
|
196
|
+
def initialize(app)
|
197
|
+
@children = self.class.children.map { |child|
|
198
|
+
child.new(app)
|
199
|
+
}
|
200
|
+
|
201
|
+
self.class.routes.values.flatten.each do |route|
|
202
|
+
route.pipeline = self.class.__pipeline.dup
|
203
|
+
|
204
|
+
self.class.limit_by_route[route.name].to_a.reverse.each do |limit|
|
205
|
+
if index = route.pipeline.actions.index(limit[:after])
|
206
|
+
route.pipeline.actions.insert(index + 1, limit[:insert])
|
207
|
+
else
|
208
|
+
route.pipeline.actions << limit[:insert]
|
209
|
+
end
|
210
|
+
end
|
470
211
|
|
471
|
-
|
472
|
-
|
212
|
+
route.pipeline.actions.delete_if do |action|
|
213
|
+
self.class.global_skips.to_a.include?(action.name) ||
|
214
|
+
self.class.skips_by_route[route.name].to_a.include?(action.name)
|
215
|
+
end
|
473
216
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
class_state :global_skips, default: [], inheritable: true
|
217
|
+
route.pipeline.actions << Support::Pipeline::Action.new(:dispatch)
|
218
|
+
end
|
219
|
+
end
|
478
220
|
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
221
|
+
def call(connection, request_path = connection.path)
|
222
|
+
request_method = connection.method
|
223
|
+
if request_method == METHOD_HEAD
|
224
|
+
request_method = METHOD_GET
|
483
225
|
end
|
484
226
|
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
227
|
+
matcher = self.class.matcher
|
228
|
+
if match = matcher.match(request_path)
|
229
|
+
match_data = match.named_captures
|
230
|
+
connection.params.merge!(match_data)
|
231
|
+
|
232
|
+
if matcher.is_a?(Regexp)
|
233
|
+
request_path = String.normalize_path(request_path.sub(matcher, ""))
|
491
234
|
end
|
492
|
-
else
|
493
|
-
super(name, &block)
|
494
|
-
end
|
495
235
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
236
|
+
@children.each do |child_controller|
|
237
|
+
child_controller.call(connection, request_path)
|
238
|
+
break if connection.halted?
|
239
|
+
end
|
500
240
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
241
|
+
unless connection.halted?
|
242
|
+
self.class.routes[request_method].to_a.each do |route|
|
243
|
+
catch :reject do
|
244
|
+
if route_match = route.match(request_path)
|
245
|
+
connection.params.merge!(route_match.named_captures)
|
246
|
+
|
247
|
+
connection.set(
|
248
|
+
:__endpoint_path,
|
249
|
+
String.normalize_path(
|
250
|
+
File.join(
|
251
|
+
self.class.path_to_self.to_s, route.path.to_s
|
252
|
+
)
|
253
|
+
)
|
254
|
+
)
|
255
|
+
|
256
|
+
connection.set(:__endpoint_name, route.name)
|
257
|
+
|
258
|
+
dup.call_route(connection, route)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
break if connection.halted?
|
263
|
+
end
|
507
264
|
end
|
508
265
|
end
|
509
266
|
end
|
510
267
|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
@
|
268
|
+
# @api private
|
269
|
+
def call_route(connection, route)
|
270
|
+
@connection, @route = connection, route
|
271
|
+
@route.pipeline.callable(self).call(connection); halt
|
272
|
+
rescue StandardError => error
|
273
|
+
@connection.logger.houston(error)
|
274
|
+
handle_error(error)
|
515
275
|
end
|
516
276
|
|
517
|
-
#
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
def Controller(matcher)
|
526
|
-
make(matcher)
|
527
|
-
end
|
528
|
-
# rubocop:enabled Naming/MethodName
|
277
|
+
# @api private
|
278
|
+
def dispatch
|
279
|
+
halted = false
|
280
|
+
performing :dispatch do
|
281
|
+
halted = catch :halt do
|
282
|
+
@route.call(self)
|
283
|
+
end
|
284
|
+
end
|
529
285
|
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
#
|
534
|
-
def default(&block)
|
535
|
-
get :default, "/", &block
|
286
|
+
# Catching the halt then re-halting lets us call after dispatch hooks in non-error cases.
|
287
|
+
#
|
288
|
+
halt if halted
|
536
289
|
end
|
537
290
|
|
538
|
-
#
|
539
|
-
# Create a route that matches +GET+ requests at +path+. For example:
|
540
|
-
#
|
541
|
-
# Pakyow::App.controller do
|
542
|
-
# get "/foo" do
|
543
|
-
# # do something
|
544
|
-
# end
|
545
|
-
# end
|
291
|
+
# Redirects to +location+ and immediately halts request processing.
|
546
292
|
#
|
547
|
-
#
|
548
|
-
#
|
293
|
+
# @param location [String] what url the request should be redirected to
|
294
|
+
# @param as [Integer, Symbol] the status to redirect with
|
295
|
+
# @param trusted [Boolean] whether or not the location is trusted
|
549
296
|
#
|
550
|
-
#
|
551
|
-
#
|
552
|
-
#
|
553
|
-
#
|
297
|
+
# @example Redirecting:
|
298
|
+
# Pakyow::App.controller do
|
299
|
+
# default do
|
300
|
+
# redirect "/foo"
|
554
301
|
# end
|
302
|
+
# end
|
555
303
|
#
|
556
|
-
#
|
557
|
-
#
|
558
|
-
#
|
559
|
-
#
|
560
|
-
#
|
561
|
-
#
|
562
|
-
# Create a route that matches +PUT+ requests at +path+.
|
563
|
-
#
|
564
|
-
# @see get
|
565
|
-
#
|
566
|
-
# @!method patch
|
567
|
-
# Create a route that matches +PATCH+ requests at +path+.
|
568
|
-
#
|
569
|
-
# @see get
|
570
|
-
#
|
571
|
-
# @!method delete
|
572
|
-
# Create a route that matches +DELETE+ requests at +path+.
|
304
|
+
# @example Redirecting with a status code:
|
305
|
+
# Pakyow::App.controller do
|
306
|
+
# default do
|
307
|
+
# redirect "/foo", as: 301
|
308
|
+
# end
|
309
|
+
# end
|
573
310
|
#
|
574
|
-
#
|
311
|
+
# @example Redirecting to a remote location:
|
312
|
+
# Pakyow::App.controller do
|
313
|
+
# default do
|
314
|
+
# redirect "http://foo.com/bar", trusted: true
|
315
|
+
# end
|
316
|
+
# end
|
575
317
|
#
|
576
|
-
|
577
|
-
|
578
|
-
|
318
|
+
def redirect(location, as: 302, trusted: false, **params)
|
319
|
+
location = case location
|
320
|
+
when Symbol
|
321
|
+
app.endpoints.path(location, **params)
|
322
|
+
else
|
323
|
+
location
|
324
|
+
end
|
325
|
+
|
326
|
+
if trusted || URI(location).host.nil?
|
327
|
+
@connection.status = Connection::Statuses.code(as)
|
328
|
+
@connection.set_header("location", location)
|
329
|
+
halt
|
330
|
+
else
|
331
|
+
raise Security::InsecureRedirect.new_with_message(
|
332
|
+
location: location
|
333
|
+
)
|
579
334
|
end
|
580
335
|
end
|
581
336
|
|
582
|
-
#
|
583
|
-
#
|
584
|
-
# Named groups make the routes available for path building. Paths to routes defined in unnamed
|
585
|
-
# groups are referenced by the most direct parent group that is named.
|
337
|
+
# Reroutes the request to a different location. Instead of an http redirect, the request will
|
338
|
+
# continued to be handled in the current request lifecycle.
|
586
339
|
#
|
587
|
-
# @
|
588
|
-
#
|
589
|
-
#
|
590
|
-
# def foo
|
591
|
-
# logger.info "foo"
|
592
|
-
# end
|
340
|
+
# @param location [String] what url the request should be rerouted to
|
341
|
+
# @param method [Symbol] the http method to reroute as
|
593
342
|
#
|
594
|
-
#
|
595
|
-
#
|
596
|
-
#
|
597
|
-
#
|
598
|
-
# def bar
|
599
|
-
# logger.info "bar"
|
600
|
-
# end
|
343
|
+
# @example
|
344
|
+
# Pakyow::App.resource :posts, "/posts" do
|
345
|
+
# edit do
|
346
|
+
# @post ||= find_post_by_id(params[:post_id])
|
601
347
|
#
|
602
|
-
#
|
603
|
-
# # "foo" and "bar" have both been logged
|
604
|
-
# send "foo.bar"
|
605
|
-
# end
|
348
|
+
# # render the form for @post
|
606
349
|
# end
|
607
350
|
#
|
608
|
-
#
|
609
|
-
#
|
610
|
-
#
|
611
|
-
#
|
612
|
-
# # "foo" has been logged
|
613
|
-
# send "baz"
|
351
|
+
# update do
|
352
|
+
# if post_fails_to_create
|
353
|
+
# @post = failed_post_object
|
354
|
+
# reroute path(:posts_edit, post_id: @post.id), method: :get
|
614
355
|
# end
|
615
356
|
# end
|
616
357
|
# end
|
617
358
|
#
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
359
|
+
def reroute(location, method: connection.method, as: nil, **params)
|
360
|
+
connection = @connection.__getobj__
|
361
|
+
|
362
|
+
# Make sure the endpoint is set.
|
363
|
+
#
|
364
|
+
connection.endpoint
|
365
|
+
|
366
|
+
connection.instance_variable_set(:@method, method)
|
367
|
+
connection.instance_variable_set(:@path, location.is_a?(Symbol) ? app.endpoints.path(location, **params) : location)
|
368
|
+
|
369
|
+
# Change the response status, if set.
|
370
|
+
#
|
371
|
+
connection.status = Connection::Statuses.code(as) if as
|
372
|
+
|
373
|
+
@connection.set(:__endpoint_path, nil)
|
374
|
+
@connection.set(:__endpoint_name, nil)
|
375
|
+
|
376
|
+
app.perform(@connection); halt
|
631
377
|
end
|
632
378
|
|
633
|
-
#
|
634
|
-
#
|
379
|
+
# Responds to a specific request format.
|
380
|
+
#
|
381
|
+
# The +Content-Type+ header will be set on the response based on the format that is being
|
382
|
+
# responded to.
|
635
383
|
#
|
636
|
-
#
|
384
|
+
# After yielding, request processing will be halted.
|
385
|
+
#
|
386
|
+
# @example
|
637
387
|
# Pakyow::App.controller do
|
638
|
-
#
|
639
|
-
#
|
640
|
-
#
|
388
|
+
# get "/foo.txt|html" do
|
389
|
+
# respond_to :txt do
|
390
|
+
# send "foo"
|
641
391
|
# end
|
642
392
|
#
|
643
|
-
#
|
644
|
-
# get :list, "/" do
|
645
|
-
# # route is accessible via 'GET /api/projects'
|
646
|
-
# send projects.to_json
|
647
|
-
# end
|
648
|
-
# end
|
393
|
+
# # do something for html format
|
649
394
|
# end
|
650
395
|
# end
|
651
396
|
#
|
652
|
-
def
|
653
|
-
|
654
|
-
|
397
|
+
def respond_to(format)
|
398
|
+
return unless @connection.format == format.to_sym
|
399
|
+
@connection.format = format
|
400
|
+
yield
|
401
|
+
halt
|
655
402
|
end
|
656
403
|
|
657
|
-
|
658
|
-
|
659
|
-
#
|
404
|
+
DEFAULT_SEND_TYPE = "application/octet-stream".freeze
|
405
|
+
|
406
|
+
# Sends a file or other data in the response.
|
660
407
|
#
|
661
|
-
#
|
662
|
-
#
|
408
|
+
# Accepts data as a +String+ or +IO+ object. When passed a +File+ object, the mime type will be
|
409
|
+
# determined automatically. The type can be set explicitly with the +type+ option.
|
663
410
|
#
|
664
|
-
#
|
665
|
-
#
|
666
|
-
# Pakyow itself to define the resource template ({Routing::Extension::Resource}).
|
411
|
+
# Passing +name+ sets the +Content-Disposition+ header to "attachment". Otherwise, the
|
412
|
+
# disposition will be set to "inline".
|
667
413
|
#
|
668
|
-
# @example
|
414
|
+
# @example Sending data:
|
669
415
|
# Pakyow::App.controller do
|
670
|
-
#
|
671
|
-
#
|
672
|
-
# get :goodbye, "/goodbye"
|
416
|
+
# default do
|
417
|
+
# send "foo", type: "text/plain"
|
673
418
|
# end
|
674
419
|
# end
|
675
420
|
#
|
676
|
-
# @example
|
677
|
-
#
|
421
|
+
# @example Sending a file:
|
678
422
|
# Pakyow::App.controller do
|
679
|
-
#
|
680
|
-
#
|
681
|
-
#
|
682
|
-
# end
|
683
|
-
#
|
684
|
-
# goodbye do
|
685
|
-
# send "goodbye"
|
686
|
-
# end
|
687
|
-
#
|
688
|
-
# # we can also extend the expansion
|
689
|
-
# # for our particular use-case
|
690
|
-
# get "/thanks" do
|
691
|
-
# send "thanks"
|
692
|
-
# end
|
693
|
-
# end
|
694
|
-
#
|
695
|
-
# talkback :fr, "/fr" do
|
696
|
-
# hello do
|
697
|
-
# send "bonjour"
|
698
|
-
# end
|
699
|
-
#
|
700
|
-
# # `goodbye` will not be an endpoint
|
701
|
-
# # since we did not expand it here
|
423
|
+
# default do
|
424
|
+
# filename = "foo.txt"
|
425
|
+
# send File.open(filename), name: filename
|
702
426
|
# end
|
703
427
|
# end
|
704
428
|
#
|
705
|
-
def
|
706
|
-
|
429
|
+
def send(file_or_data, type: nil, name: nil)
|
430
|
+
if file_or_data.is_a?(IO) || file_or_data.is_a?(StringIO)
|
431
|
+
data = file_or_data
|
432
|
+
|
433
|
+
if file_or_data.is_a?(File)
|
434
|
+
@connection.set_header("content-length", file_or_data.size)
|
435
|
+
type ||= MiniMime.lookup_by_filename(file_or_data.path)&.content_type.to_s
|
436
|
+
end
|
437
|
+
|
438
|
+
@connection.set_header("content-type", type || DEFAULT_SEND_TYPE)
|
439
|
+
elsif file_or_data.is_a?(String)
|
440
|
+
@connection.set_header("content-length", file_or_data.bytesize)
|
441
|
+
@connection.set_header("content-type", type) if type
|
442
|
+
data = StringIO.new(file_or_data)
|
443
|
+
else
|
444
|
+
raise ArgumentError, "expected an IO or String object"
|
445
|
+
end
|
446
|
+
|
447
|
+
@connection.set_header(CONTENT_DISPOSITION, name ? "attachment; filename=#{name}" : "inline")
|
448
|
+
halt(data)
|
707
449
|
end
|
708
450
|
|
709
|
-
#
|
451
|
+
# Halts request processing, immediately returning the response.
|
710
452
|
#
|
711
|
-
#
|
453
|
+
# The response body will be set to +body+ prior to halting (if it's a non-nil value).
|
712
454
|
#
|
713
|
-
def
|
714
|
-
|
455
|
+
def halt(body = nil, status: nil)
|
456
|
+
@connection.body = body if body
|
457
|
+
@connection.status = Connection::Statuses.code(status) if status
|
458
|
+
@connection.halt
|
715
459
|
end
|
716
460
|
|
717
|
-
#
|
718
|
-
# example, these calls are identical:
|
719
|
-
#
|
720
|
-
# Pakyow::App.controller do
|
721
|
-
# resource :posts, "/posts" do
|
722
|
-
# end
|
461
|
+
# Rejects the request, calling the next matching route.
|
723
462
|
#
|
724
|
-
|
725
|
-
|
726
|
-
|
463
|
+
def reject
|
464
|
+
throw :reject
|
465
|
+
end
|
466
|
+
|
467
|
+
class_state :children, default: [], inheritable: false
|
468
|
+
class_state :templates, default: {}, inheritable: true
|
469
|
+
class_state :expansions, default: [], inheritable: false
|
470
|
+
class_state :routes, default: DEFINABLE_HTTP_METHODS.each_with_object({}) { |supported_method, routes_hash|
|
471
|
+
routes_hash[supported_method] = []
|
472
|
+
}, inheritable: false
|
473
|
+
|
474
|
+
class_state :limit_by_route, default: {}, inheritable: false
|
475
|
+
class_state :skips_by_route, default: {}, inheritable: false
|
476
|
+
|
477
|
+
# Global rules should be inherited by children, but route-specific rules
|
478
|
+
# shouldn't be since they refer to a specific route in the current context.
|
727
479
|
#
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
480
|
+
class_state :global_skips, default: [], inheritable: true
|
481
|
+
|
482
|
+
class << self
|
483
|
+
def action(name, only: [], skip: [], &block)
|
484
|
+
@__pipeline.actions.delete_if do |action|
|
485
|
+
action.name == name
|
486
|
+
end
|
487
|
+
|
488
|
+
if only.any?
|
489
|
+
only.each do |route_name|
|
490
|
+
(@limit_by_route[route_name] ||= []) << {
|
491
|
+
insert: Support::Pipeline::Action.new(name, &block),
|
492
|
+
after: @__pipeline.actions.last
|
493
|
+
}
|
494
|
+
end
|
495
|
+
else
|
496
|
+
super(name, &block)
|
497
|
+
end
|
498
|
+
|
499
|
+
skip.each do |route_name|
|
500
|
+
(@skips_by_route[route_name] ||= []) << name
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def skip(name, only: [])
|
505
|
+
if only.empty?
|
506
|
+
@global_skips << name
|
507
|
+
else
|
508
|
+
only.each do |route_name|
|
509
|
+
(@skips_by_route[route_name] ||= []) << name
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def use_pipeline(*)
|
732
515
|
super
|
516
|
+
|
517
|
+
@limit_by_route = {}
|
733
518
|
end
|
734
|
-
end
|
735
519
|
|
736
|
-
|
737
|
-
|
738
|
-
|
520
|
+
# Conveniently define defaults when subclassing +Pakyow::Routing::Controller+.
|
521
|
+
#
|
522
|
+
# @example
|
523
|
+
# class MyController < Pakyow::Routing::Controller("/foo")
|
524
|
+
# # more routes here
|
525
|
+
# end
|
526
|
+
#
|
527
|
+
# rubocop:disable Naming/MethodName
|
528
|
+
def Controller(matcher)
|
529
|
+
make(matcher)
|
530
|
+
end
|
531
|
+
# rubocop:enabled Naming/MethodName
|
739
532
|
|
740
|
-
|
741
|
-
|
533
|
+
# Create a default route. Shorthand for +get "/"+.
|
534
|
+
#
|
535
|
+
# @see get
|
536
|
+
#
|
537
|
+
def default(&block)
|
538
|
+
get :default, "/", &block
|
539
|
+
end
|
742
540
|
|
743
|
-
|
744
|
-
|
541
|
+
# @!method get
|
542
|
+
# Create a route that matches +GET+ requests at +path+. For example:
|
543
|
+
#
|
544
|
+
# Pakyow::App.controller do
|
545
|
+
# get "/foo" do
|
546
|
+
# # do something
|
547
|
+
# end
|
548
|
+
# end
|
549
|
+
#
|
550
|
+
# Routes can be named, making them available for path building via {Controller#path}. For
|
551
|
+
# example:
|
552
|
+
#
|
553
|
+
# Pakyow::App.controller do
|
554
|
+
# get :foo, "/foo" do
|
555
|
+
# # do something
|
556
|
+
# end
|
557
|
+
# end
|
558
|
+
#
|
559
|
+
# @!method post
|
560
|
+
# Create a route that matches +POST+ requests at +path+.
|
561
|
+
#
|
562
|
+
# @see get
|
563
|
+
#
|
564
|
+
# @!method put
|
565
|
+
# Create a route that matches +PUT+ requests at +path+.
|
566
|
+
#
|
567
|
+
# @see get
|
568
|
+
#
|
569
|
+
# @!method patch
|
570
|
+
# Create a route that matches +PATCH+ requests at +path+.
|
571
|
+
#
|
572
|
+
# @see get
|
573
|
+
#
|
574
|
+
# @!method delete
|
575
|
+
# Create a route that matches +DELETE+ requests at +path+.
|
576
|
+
#
|
577
|
+
# @see get
|
578
|
+
#
|
579
|
+
DEFINABLE_HTTP_METHODS.each do |http_method|
|
580
|
+
define_method http_method.downcase.to_sym do |name_or_matcher = nil, matcher_or_name = nil, &block|
|
581
|
+
build_route(http_method, name_or_matcher, matcher_or_name, &block)
|
582
|
+
end
|
583
|
+
end
|
745
584
|
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
585
|
+
# Creates a nested group of routes, with an optional name.
|
586
|
+
#
|
587
|
+
# Named groups make the routes available for path building. Paths to routes defined in unnamed
|
588
|
+
# groups are referenced by the most direct parent group that is named.
|
589
|
+
#
|
590
|
+
# @example Defining a group:
|
591
|
+
# Pakyow::App.controller do
|
592
|
+
#
|
593
|
+
# def foo
|
594
|
+
# logger.info "foo"
|
595
|
+
# end
|
596
|
+
#
|
597
|
+
# group :foo do
|
598
|
+
# action :foo
|
599
|
+
# action :bar
|
600
|
+
#
|
601
|
+
# def bar
|
602
|
+
# logger.info "bar"
|
603
|
+
# end
|
604
|
+
#
|
605
|
+
# get :bar, "/bar" do
|
606
|
+
# # "foo" and "bar" have both been logged
|
607
|
+
# send "foo.bar"
|
608
|
+
# end
|
609
|
+
# end
|
610
|
+
#
|
611
|
+
# group do
|
612
|
+
# action :foo
|
613
|
+
#
|
614
|
+
# get :baz, "/baz" do
|
615
|
+
# # "foo" has been logged
|
616
|
+
# send "baz"
|
617
|
+
# end
|
618
|
+
# end
|
619
|
+
# end
|
620
|
+
#
|
621
|
+
# @example Building a path to a route within a named group:
|
622
|
+
# path :foo_bar
|
623
|
+
# # => "/foo/bar"
|
624
|
+
#
|
625
|
+
# @example Building a path to a route within an unnamed group:
|
626
|
+
# path :foo_baz
|
627
|
+
# # => nil
|
628
|
+
#
|
629
|
+
# path :baz
|
630
|
+
# # => "/baz"
|
631
|
+
#
|
632
|
+
def group(name = nil, **kwargs, &block)
|
633
|
+
make_child(name, nil, **kwargs, &block)
|
634
|
+
end
|
750
635
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
636
|
+
# Creates a group of routes and mounts them at a path, with an optional name. A namespace
|
637
|
+
# behaves just like a group with regard to path lookup and action inheritance.
|
638
|
+
#
|
639
|
+
# @example Defining a namespace:
|
640
|
+
# Pakyow::App.controller do
|
641
|
+
# namespace :api, "/api" do
|
642
|
+
# def auth
|
643
|
+
# handle 401 unless authed?
|
644
|
+
# end
|
645
|
+
#
|
646
|
+
# namespace :project, "/projects" do
|
647
|
+
# get :list, "/" do
|
648
|
+
# # route is accessible via 'GET /api/projects'
|
649
|
+
# send projects.to_json
|
650
|
+
# end
|
651
|
+
# end
|
652
|
+
# end
|
653
|
+
# end
|
654
|
+
#
|
655
|
+
def namespace(*args, **kwargs, &block)
|
656
|
+
name, matcher = parse_name_and_matcher_from_args(*args)
|
657
|
+
make_child(name, matcher, **kwargs, &block)
|
658
|
+
end
|
755
659
|
|
756
|
-
|
757
|
-
|
660
|
+
# Creates a route template with a name and block. The block is evaluated within a
|
661
|
+
# {Routing::Expansion} instance when / if it is later expanded at some endpoint (creating a
|
662
|
+
# namespace).
|
663
|
+
#
|
664
|
+
# Route templates are used to define a scaffold of default routes that will later be expanded
|
665
|
+
# at some path. During expansion, the scaffolded routes are also mapped to routing logic.
|
666
|
+
#
|
667
|
+
# Because routes can be referenced by name during expansion, route templates provide a way to
|
668
|
+
# create a domain-specific-language, or DSL, around a routing concern. This is used within
|
669
|
+
# Pakyow itself to define the resource template ({Routing::Extension::Resource}).
|
670
|
+
#
|
671
|
+
# @example Defining a template:
|
672
|
+
# Pakyow::App.controller do
|
673
|
+
# template :talkback do
|
674
|
+
# get :hello, "/hello"
|
675
|
+
# get :goodbye, "/goodbye"
|
676
|
+
# end
|
677
|
+
# end
|
678
|
+
#
|
679
|
+
# @example Expanding a template:
|
680
|
+
#
|
681
|
+
# Pakyow::App.controller do
|
682
|
+
# talkback :en, "/en" do
|
683
|
+
# hello do
|
684
|
+
# send "hello"
|
685
|
+
# end
|
686
|
+
#
|
687
|
+
# goodbye do
|
688
|
+
# send "goodbye"
|
689
|
+
# end
|
690
|
+
#
|
691
|
+
# # we can also extend the expansion
|
692
|
+
# # for our particular use-case
|
693
|
+
# get "/thanks" do
|
694
|
+
# send "thanks"
|
695
|
+
# end
|
696
|
+
# end
|
697
|
+
#
|
698
|
+
# talkback :fr, "/fr" do
|
699
|
+
# hello do
|
700
|
+
# send "bonjour"
|
701
|
+
# end
|
702
|
+
#
|
703
|
+
# # `goodbye` will not be an endpoint
|
704
|
+
# # since we did not expand it here
|
705
|
+
# end
|
706
|
+
# end
|
707
|
+
#
|
708
|
+
def template(name, &template_block)
|
709
|
+
templates[name] = template_block
|
710
|
+
end
|
758
711
|
|
759
|
-
#
|
712
|
+
# Expands a defined route template, or raises +NameError+.
|
760
713
|
#
|
761
|
-
|
714
|
+
# @see template
|
715
|
+
#
|
716
|
+
def expand(name, *args, **options, &block)
|
717
|
+
make_child(*args).expand_within(name, **options, &block)
|
718
|
+
end
|
719
|
+
|
720
|
+
# Attempts to find and expand a template, avoiding the need to call {expand} explicitly. For
|
721
|
+
# example, these calls are identical:
|
722
|
+
#
|
723
|
+
# Pakyow::App.controller do
|
724
|
+
# resource :posts, "/posts" do
|
725
|
+
# end
|
726
|
+
#
|
727
|
+
# expand :resource, :posts, "/posts" do
|
728
|
+
# end
|
729
|
+
# end
|
730
|
+
#
|
731
|
+
def method_missing(name, *args, &block)
|
732
|
+
if templates.include?(name)
|
733
|
+
expand(name, *args, &block)
|
734
|
+
else
|
735
|
+
super
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
def respond_to_missing?(method_name, include_private = false)
|
740
|
+
templates.include?(method_name) || super
|
741
|
+
end
|
742
|
+
|
743
|
+
# @api private
|
744
|
+
attr_reader :path, :matcher
|
745
|
+
|
746
|
+
# @api private
|
747
|
+
attr_accessor :parent
|
748
|
+
|
749
|
+
def path_to_self
|
750
|
+
return path unless parent
|
751
|
+
File.join(parent.path_to_self.to_s, path.to_s)
|
752
|
+
end
|
753
|
+
|
754
|
+
def name_of_self
|
755
|
+
return __object_name.name unless parent
|
756
|
+
[parent.name_of_self.to_s, __object_name.name.to_s].join("_").to_sym
|
757
|
+
end
|
758
|
+
|
759
|
+
def endpoints
|
760
|
+
self_name = __object_name&.name
|
761
|
+
|
762
|
+
# Ignore member and collection namespaces for endpoint building.
|
763
|
+
#
|
764
|
+
self_name = nil if self_name == :member || self_name == :collection
|
765
|
+
|
766
|
+
[].tap do |endpoints|
|
767
|
+
@routes.values.flatten.each do |route|
|
768
|
+
if route.name == :default && self_name
|
769
|
+
# Register the endpoint without the default name for easier lookup.
|
770
|
+
#
|
771
|
+
endpoints << Endpoint.new(
|
772
|
+
name: self_name,
|
773
|
+
method: route.method,
|
774
|
+
builder: Routing::Route::EndpointBuilder.new(
|
775
|
+
route: route, path: path_to_self
|
776
|
+
)
|
777
|
+
)
|
778
|
+
end
|
762
779
|
|
763
|
-
[].tap do |endpoints|
|
764
|
-
@routes.values.flatten.each do |route|
|
765
|
-
if route.name == :default && self_name
|
766
|
-
# Register the endpoint without the default name for easier lookup.
|
767
|
-
#
|
768
780
|
endpoints << Endpoint.new(
|
769
|
-
name: self_name,
|
781
|
+
name: [self_name, route.name.to_s].compact.join("_"),
|
770
782
|
method: route.method,
|
771
783
|
builder: Routing::Route::EndpointBuilder.new(
|
772
784
|
route: route, path: path_to_self
|
@@ -774,97 +786,90 @@ module Pakyow
|
|
774
786
|
)
|
775
787
|
end
|
776
788
|
|
777
|
-
endpoints
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
789
|
+
children.flat_map(&:endpoints).each do |child_endpoint|
|
790
|
+
endpoints << Endpoint.new(
|
791
|
+
name: [self_name, child_endpoint.name].compact.join("_"),
|
792
|
+
method: child_endpoint.method,
|
793
|
+
builder: child_endpoint.builder
|
782
794
|
)
|
783
|
-
|
784
|
-
end
|
785
|
-
|
786
|
-
children.flat_map(&:endpoints).each do |child_endpoint|
|
787
|
-
endpoints << Endpoint.new(
|
788
|
-
name: [self_name, child_endpoint.name].compact.join("_"),
|
789
|
-
method: child_endpoint.method,
|
790
|
-
builder: child_endpoint.builder
|
791
|
-
)
|
795
|
+
end
|
792
796
|
end
|
793
797
|
end
|
794
|
-
end
|
795
798
|
|
796
|
-
|
797
|
-
|
799
|
+
# @api private
|
800
|
+
def make(*args, **kwargs, &block)
|
801
|
+
name, matcher = parse_name_and_matcher_from_args(*args)
|
798
802
|
|
799
|
-
|
800
|
-
|
803
|
+
path = path_from_matcher(matcher)
|
804
|
+
matcher = finalize_matcher(matcher || "/")
|
801
805
|
|
802
|
-
|
803
|
-
|
806
|
+
super(name, path: path, matcher: matcher, **kwargs, &block)
|
807
|
+
end
|
804
808
|
|
805
|
-
|
806
|
-
|
807
|
-
|
809
|
+
# @api private
|
810
|
+
def make_child(*args, **kwargs, &block)
|
811
|
+
name, matcher = parse_name_and_matcher_from_args(*args)
|
808
812
|
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
+
if name && name.is_a?(Symbol) && child = children.find { |possible_child| possible_child.__object_name.name == name }
|
814
|
+
if block_given?
|
815
|
+
child.instance_exec(&block)
|
816
|
+
end
|
813
817
|
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
818
|
+
child
|
819
|
+
else
|
820
|
+
if name && name.is_a?(Symbol) && __object_name
|
821
|
+
name = __object_name.isolated(name)
|
822
|
+
end
|
819
823
|
|
820
|
-
|
821
|
-
|
824
|
+
make(name, matcher, parent: self, **kwargs, &block).tap do |controller|
|
825
|
+
children << controller
|
826
|
+
end
|
822
827
|
end
|
823
828
|
end
|
824
|
-
end
|
825
829
|
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
830
|
+
# @api private
|
831
|
+
def expand_within(name, **options, &block)
|
832
|
+
raise NameError, "unknown template `#{name}'" unless template = templates[name]
|
833
|
+
Routing::Expansion.new(name, self, options, &template)
|
834
|
+
class_eval(&block)
|
835
|
+
self
|
836
|
+
end
|
833
837
|
|
834
|
-
|
838
|
+
private
|
835
839
|
|
836
|
-
|
837
|
-
|
838
|
-
|
840
|
+
def parse_name_and_matcher_from_args(name_or_matcher = nil, matcher_or_name = nil)
|
841
|
+
Support::Aargv.normalize([name_or_matcher, matcher_or_name].compact, name: [Symbol, Support::ObjectName], matcher: Object).values_at(:name, :matcher)
|
842
|
+
end
|
839
843
|
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
844
|
+
def finalize_matcher(matcher)
|
845
|
+
if matcher.is_a?(String)
|
846
|
+
converted_matcher = String.normalize_path(matcher.split("/").map { |segment|
|
847
|
+
if segment.include?(":")
|
848
|
+
"(?<#{segment[1..-1]}>(\\w|[-.~:@!$\\'\\(\\)\\*\\+,;])+)"
|
849
|
+
else
|
850
|
+
segment
|
851
|
+
end
|
852
|
+
}.join("/"))
|
849
853
|
|
850
|
-
|
851
|
-
|
852
|
-
|
854
|
+
Regexp.new("^#{String.normalize_path(converted_matcher)}")
|
855
|
+
else
|
856
|
+
matcher
|
857
|
+
end
|
853
858
|
end
|
854
|
-
end
|
855
859
|
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
860
|
+
def path_from_matcher(matcher)
|
861
|
+
if matcher.is_a?(String)
|
862
|
+
matcher
|
863
|
+
else
|
864
|
+
nil
|
865
|
+
end
|
861
866
|
end
|
862
|
-
end
|
863
867
|
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
+
def build_route(method, *args, &block)
|
869
|
+
name, matcher = parse_name_and_matcher_from_args(*args)
|
870
|
+
Routing::Route.new(matcher, name: name, method: method, &block).tap do |route|
|
871
|
+
routes[method] << route
|
872
|
+
end
|
868
873
|
end
|
869
874
|
end
|
870
875
|
end
|