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: 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