pakyow-routing 1.0.0.rc2 → 1.0.0.rc3
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
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
|