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: 03a293a804dec482d43a8d9b780bc8cabb82c2f89de3f1e6f80cdba49b70873e
4
- data.tar.gz: 0d77d9f5aa487a5331fd1ba69a1411f5369386feff0e3a9d3b1fe0eeeb5079e8
3
+ metadata.gz: 52a52fa15197c4a3509beb255ae01273b7872478f60e19c5ec5833413ebb48c4
4
+ data.tar.gz: 9379faaa3d68a7cf1ded42adf87b6de01ed1616f591e565f4f374684fbc3fc04
5
5
  SHA512:
6
- metadata.gz: 16aa58f093586062b904ef2bf4fdb5b2e0576e4d3dd41bb4adf7981e49cef31976f1c7049cd2f117cb91fed99c92ce825fe61c715094e272111094b32d843194
7
- data.tar.gz: 21ecd4b69fe8039bda853c3a2e61e96d8f4fd27caf0be8d8669ce339cd68d8f58e99d355638b7d3f1a2f5d360e5965a40ad930d7c4f8871b9cd5d795d92a705b
6
+ metadata.gz: 519751841aba45aa608ef7cdc9767b798b79897d14cc4384fd9bda08472442d45177f68a2c616d75f07cd6b20bc3546dd8e814a6c446be0110bde259917522bd
7
+ data.tar.gz: 2f7ca81b72afd2b670a884e0b0383d77c1bb0111ae1a6f786797089e8a5fca15494f0f8e553ed1f7d7a7c969f6b06d647206e3092f386e0a003cdff79189397f
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pakyow
4
- module Routing
5
- module Actions
4
+ module Actions
5
+ module Routing
6
6
  class RespondMissing
7
7
  def call(connection)
8
8
  connection.app.controller_for_connection(connection).trigger(404)
@@ -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
- # Executes code for particular requests. For example:
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
- # default do
297
- # redirect "/foo"
26
+ # get "/" do
27
+ # # called for GET / requests
298
28
  # end
299
29
  # end
300
30
  #
301
- # @example Redirecting with a status code:
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
- # default do
304
- # redirect "/foo", as: 301
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
- # default do
311
- # redirect "http://foo.com/bar", trusted: true
52
+ # include AuthHelpers
53
+ #
54
+ # get :foo, "/foo" do
55
+ # current_user
312
56
  # end
313
57
  # end
314
58
  #
315
- def redirect(location, as: 302, trusted: false, **params)
316
- location = case location
317
- when Symbol
318
- app.endpoints.path(location, **params)
319
- else
320
- location
321
- end
322
-
323
- if trusted || URI(location).host.nil?
324
- @connection.status = Connection::Statuses.code(as)
325
- @connection.set_header("location", location)
326
- halt
327
- else
328
- raise Security::InsecureRedirect.new_with_message(
329
- location: location
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
- # @param location [String] what url the request should be rerouted to
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
- # @example
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
- # # render the form for @post
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
- # update do
349
- # if post_fails_to_create
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
- def reroute(location, method: connection.method, as: nil, **params)
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
- # The +Content-Type+ header will be set on the response based on the format that is being
379
- # responded to.
95
+ # Extensions can be defined and used to add shared routes to one or more controllers.
96
+ # See {Routing::Extension}.
380
97
  #
381
- # After yielding, request processing will be halted.
98
+ # = Other routing features
382
99
  #
383
- # @example
384
- # Pakyow::App.controller do
385
- # get "/foo.txt|html" do
386
- # respond_to :txt do
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
- # # do something for html format
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
- def respond_to(format)
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
- # Accepts data as a +String+ or +IO+ object. When passed a +File+ object, the mime type will be
406
- # determined automatically. The type can be set explicitly with the +type+ option.
115
+ # = Custom matchers
407
116
  #
408
- # Passing +name+ sets the +Content-Disposition+ header to "attachment". Otherwise, the
409
- # disposition will be set to "inline".
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
- # @example Sending data:
412
- # Pakyow::App.controller do
413
- # default do
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
- # @example Sending a file:
419
- # Pakyow::App.controller do
420
- # default do
421
- # filename = "foo.txt"
422
- # send File.open(filename), name: filename
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
- def send(file_or_data, type: nil, name: nil)
427
- if file_or_data.is_a?(IO) || file_or_data.is_a?(StringIO)
428
- data = file_or_data
149
+ class Controller
150
+ using Support::DeepDup
151
+ extend Support::Makeable
152
+ extend Support::ClassState
429
153
 
430
- if file_or_data.is_a?(File)
431
- @connection.set_header(Rack::CONTENT_LENGTH, file_or_data.size)
432
- type ||= Rack::Mime.mime_type(File.extname(file_or_data.path))
433
- end
154
+ include Support::Hookable
155
+ events :dispatch
434
156
 
435
- @connection.set_header(Rack::CONTENT_TYPE, type || DEFAULT_SEND_TYPE)
436
- elsif file_or_data.is_a?(String)
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
- @connection.set_header(CONTENT_DISPOSITION, name ? "attachment; filename=#{name}" : "inline")
445
- halt(data)
446
- end
160
+ include Support::Pipeline
447
161
 
448
- # Halts request processing, immediately returning the response.
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
- # Rejects the request, calling the next matching route.
459
- #
460
- def reject
461
- throw :reject
462
- end
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
- class_state :children, default: [], inheritable: false
465
- class_state :templates, default: {}, inheritable: true
466
- class_state :expansions, default: [], inheritable: false
467
- class_state :routes, default: DEFINABLE_HTTP_METHODS.each_with_object({}) { |supported_method, routes_hash|
468
- routes_hash[supported_method] = []
469
- }, inheritable: false
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
- class_state :limit_by_route, default: {}, inheritable: false
472
- class_state :skips_by_route, default: {}, inheritable: false
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
- # Global rules should be inherited by children, but route-specific rules
475
- # shouldn't be since they refer to a specific route in the current context.
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
- class << self
480
- def action(name, only: [], skip: [], &block)
481
- @__pipeline.actions.delete_if do |action|
482
- action.name == name
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
- if only.any?
486
- only.each do |route_name|
487
- (@limit_by_route[route_name] ||= []) << {
488
- insert: Support::Pipeline::Action.new(name, &block),
489
- after: @__pipeline.actions.last
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
- skip.each do |route_name|
497
- (@skips_by_route[route_name] ||= []) << name
498
- end
499
- end
236
+ @children.each do |child_controller|
237
+ child_controller.call(connection, request_path)
238
+ break if connection.halted?
239
+ end
500
240
 
501
- def skip(name, only: [])
502
- if only.empty?
503
- @global_skips << name
504
- else
505
- only.each do |route_name|
506
- (@skips_by_route[route_name] ||= []) << name
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
- def use_pipeline(*)
512
- super
513
-
514
- @limit_by_route = {}
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
- # Conveniently define defaults when subclassing +Pakyow::Controller+.
518
- #
519
- # @example
520
- # class MyController < Pakyow::Controller("/foo")
521
- # # more routes here
522
- # end
523
- #
524
- # rubocop:disable Naming/MethodName
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
- # Create a default route. Shorthand for +get "/"+.
531
- #
532
- # @see get
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
- # @!method get
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
- # Routes can be named, making them available for path building via {Controller#path}. For
548
- # example:
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
- # Pakyow::App.controller do
551
- # get :foo, "/foo" do
552
- # # do something
553
- # end
297
+ # @example Redirecting:
298
+ # Pakyow::App.controller do
299
+ # default do
300
+ # redirect "/foo"
554
301
  # end
302
+ # end
555
303
  #
556
- # @!method post
557
- # Create a route that matches +POST+ requests at +path+.
558
- #
559
- # @see get
560
- #
561
- # @!method put
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
- # @see get
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
- DEFINABLE_HTTP_METHODS.each do |http_method|
577
- define_method http_method.downcase.to_sym do |name_or_matcher = nil, matcher_or_name = nil, &block|
578
- build_route(http_method, name_or_matcher, matcher_or_name, &block)
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
- # Creates a nested group of routes, with an optional name.
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
- # @example Defining a group:
588
- # Pakyow::App.controller do
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
- # group :foo do
595
- # action :foo
596
- # action :bar
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
- # get :bar, "/bar" do
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
- # group do
609
- # action :foo
610
- #
611
- # get :baz, "/baz" do
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
- # @example Building a path to a route within a named group:
619
- # path :foo_bar
620
- # # => "/foo/bar"
621
- #
622
- # @example Building a path to a route within an unnamed group:
623
- # path :foo_baz
624
- # # => nil
625
- #
626
- # path :baz
627
- # # => "/baz"
628
- #
629
- def group(name = nil, **kwargs, &block)
630
- make_child(name, nil, **kwargs, &block)
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
- # Creates a group of routes and mounts them at a path, with an optional name. A namespace
634
- # behaves just like a group with regard to path lookup and action inheritance.
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
- # @example Defining a namespace:
384
+ # After yielding, request processing will be halted.
385
+ #
386
+ # @example
637
387
  # Pakyow::App.controller do
638
- # namespace :api, "/api" do
639
- # def auth
640
- # handle 401 unless authed?
388
+ # get "/foo.txt|html" do
389
+ # respond_to :txt do
390
+ # send "foo"
641
391
  # end
642
392
  #
643
- # namespace :project, "/projects" do
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 namespace(*args, **kwargs, &block)
653
- name, matcher = parse_name_and_matcher_from_args(*args)
654
- make_child(name, matcher, **kwargs, &block)
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
- # Creates a route template with a name and block. The block is evaluated within a
658
- # {Routing::Expansion} instance when / if it is later expanded at some endpoint (creating a
659
- # namespace).
404
+ DEFAULT_SEND_TYPE = "application/octet-stream".freeze
405
+
406
+ # Sends a file or other data in the response.
660
407
  #
661
- # Route templates are used to define a scaffold of default routes that will later be expanded
662
- # at some path. During expansion, the scaffolded routes are also mapped to routing logic.
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
- # Because routes can be referenced by name during expansion, route templates provide a way to
665
- # create a domain-specific-language, or DSL, around a routing concern. This is used within
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 Defining a template:
414
+ # @example Sending data:
669
415
  # Pakyow::App.controller do
670
- # template :talkback do
671
- # get :hello, "/hello"
672
- # get :goodbye, "/goodbye"
416
+ # default do
417
+ # send "foo", type: "text/plain"
673
418
  # end
674
419
  # end
675
420
  #
676
- # @example Expanding a template:
677
- #
421
+ # @example Sending a file:
678
422
  # Pakyow::App.controller do
679
- # talkback :en, "/en" do
680
- # hello do
681
- # send "hello"
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 template(name, &template_block)
706
- templates[name] = template_block
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
- # Expands a defined route template, or raises +NameError+.
451
+ # Halts request processing, immediately returning the response.
710
452
  #
711
- # @see template
453
+ # The response body will be set to +body+ prior to halting (if it's a non-nil value).
712
454
  #
713
- def expand(name, *args, **options, &block)
714
- make_child(*args).expand_within(name, **options, &block)
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
- # Attempts to find and expand a template, avoiding the need to call {expand} explicitly. For
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
- # expand :resource, :posts, "/posts" do
725
- # end
726
- # end
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
- def method_missing(name, *args, &block)
729
- if templates.include?(name)
730
- expand(name, *args, &block)
731
- else
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
- def respond_to_missing?(method_name, include_private = false)
737
- templates.include?(method_name) || super
738
- end
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
- # @api private
741
- attr_reader :path, :matcher
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
- # @api private
744
- attr_accessor :parent
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
- def path_to_self
747
- return path unless parent
748
- File.join(parent.path_to_self.to_s, path.to_s)
749
- end
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
- def name_of_self
752
- return __object_name.name unless parent
753
- [parent.name_of_self.to_s, __object_name.name.to_s].join("_").to_sym
754
- end
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
- def endpoints
757
- self_name = __object_name&.name
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
- # Ignore member and collection namespaces for endpoint building.
712
+ # Expands a defined route template, or raises +NameError+.
760
713
  #
761
- self_name = nil if self_name == :member || self_name == :collection
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 << Endpoint.new(
778
- name: [self_name, route.name.to_s].compact.join("_"),
779
- method: route.method,
780
- builder: Routing::Route::EndpointBuilder.new(
781
- route: route, path: path_to_self
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
- def make(*args, **kwargs, &block)
797
- name, matcher = parse_name_and_matcher_from_args(*args)
799
+ # @api private
800
+ def make(*args, **kwargs, &block)
801
+ name, matcher = parse_name_and_matcher_from_args(*args)
798
802
 
799
- path = path_from_matcher(matcher)
800
- matcher = finalize_matcher(matcher || "/")
803
+ path = path_from_matcher(matcher)
804
+ matcher = finalize_matcher(matcher || "/")
801
805
 
802
- super(name, path: path, matcher: matcher, **kwargs, &block)
803
- end
806
+ super(name, path: path, matcher: matcher, **kwargs, &block)
807
+ end
804
808
 
805
- # @api private
806
- def make_child(*args, **kwargs, &block)
807
- name, matcher = parse_name_and_matcher_from_args(*args)
809
+ # @api private
810
+ def make_child(*args, **kwargs, &block)
811
+ name, matcher = parse_name_and_matcher_from_args(*args)
808
812
 
809
- if name && name.is_a?(Symbol) && child = children.find { |possible_child| possible_child.__object_name.name == name }
810
- if block_given?
811
- child.instance_exec(&block)
812
- end
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
- child
815
- else
816
- if name && name.is_a?(Symbol) && __object_name
817
- name = __object_name.isolated(name)
818
- end
818
+ child
819
+ else
820
+ if name && name.is_a?(Symbol) && __object_name
821
+ name = __object_name.isolated(name)
822
+ end
819
823
 
820
- make(name, matcher, parent: self, **kwargs, &block).tap do |controller|
821
- children << controller
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
- # @api private
827
- def expand_within(name, **options, &block)
828
- raise NameError, "unknown template `#{name}'" unless template = templates[name]
829
- Routing::Expansion.new(name, self, options, &template)
830
- class_eval(&block)
831
- self
832
- end
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
- protected
838
+ private
835
839
 
836
- def parse_name_and_matcher_from_args(name_or_matcher = nil, matcher_or_name = nil)
837
- Support::Aargv.normalize([name_or_matcher, matcher_or_name].compact, name: [Symbol, Support::ObjectName], matcher: Object).values_at(:name, :matcher)
838
- end
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
- def finalize_matcher(matcher)
841
- if matcher.is_a?(String)
842
- converted_matcher = String.normalize_path(matcher.split("/").map { |segment|
843
- if segment.include?(":")
844
- "(?<#{segment[1..-1]}>(\\w|[-.~:@!$\\'\\(\\)\\*\\+,;])+)"
845
- else
846
- segment
847
- end
848
- }.join("/"))
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
- Regexp.new("^#{String.normalize_path(converted_matcher)}")
851
- else
852
- matcher
854
+ Regexp.new("^#{String.normalize_path(converted_matcher)}")
855
+ else
856
+ matcher
857
+ end
853
858
  end
854
- end
855
859
 
856
- def path_from_matcher(matcher)
857
- if matcher.is_a?(String)
858
- matcher
859
- else
860
- nil
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
- def build_route(method, *args, &block)
865
- name, matcher = parse_name_and_matcher_from_args(*args)
866
- Routing::Route.new(matcher, name: name, method: method, &block).tap do |route|
867
- routes[method] << route
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