hanami-router 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,20 +4,26 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'hanami/router/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "hanami-router"
7
+ spec.name = 'hanami-router'
8
8
  spec.version = Hanami::Router::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
9
+ spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
10
+ spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
11
+ spec.description = %q{Rack compatible HTTP router for Ruby}
12
+ spec.summary = %q{Rack compatible HTTP router for Ruby and Hanami}
13
+ spec.homepage = 'http://hanamirb.org'
14
+ spec.license = 'MIT'
11
15
 
12
- spec.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
16
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($/)
17
+ spec.executables = []
18
+ spec.test_files = spec.files.grep(%r{^(test)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 2.0.0'
15
21
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
22
+ spec.add_dependency 'http_router', '~> 0.11'
23
+ spec.add_dependency 'hanami-utils', '~> 0.7'
20
24
 
21
- spec.add_development_dependency "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency 'bundler', '~> 1.5'
26
+ spec.add_development_dependency 'minitest', '~> 5'
27
+ spec.add_development_dependency 'rake', '~> 10'
28
+ spec.add_development_dependency 'rack-test', '~> 0.6'
23
29
  end
@@ -0,0 +1 @@
1
+ require 'hanami/router'
data/lib/hanami/router.rb CHANGED
@@ -1,7 +1,1164 @@
1
- require "hanami/router/version"
1
+ require 'rack/request'
2
+ require 'hanami/routing/http_router'
3
+ require 'hanami/routing/namespace'
4
+ require 'hanami/routing/resource'
5
+ require 'hanami/routing/resources'
6
+ require 'hanami/routing/error'
2
7
 
3
8
  module Hanami
4
- module Router
5
- # Your code goes here...
9
+ # Rack compatible, lightweight and fast HTTP Router.
10
+ #
11
+ # @since 0.1.0
12
+ #
13
+ # @example It offers an intuitive DSL, that supports most of the HTTP verbs:
14
+ # require 'hanami/router'
15
+ #
16
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
17
+ # router = Hanami::Router.new do
18
+ # get '/', to: endpoint # => get and head requests
19
+ # post '/', to: endpoint
20
+ # put '/', to: endpoint
21
+ # patch '/', to: endpoint
22
+ # delete '/', to: endpoint
23
+ # options '/', to: endpoint
24
+ # trace '/', to: endpoint
25
+ # end
26
+ #
27
+ #
28
+ #
29
+ # @example Specify an endpoint with `:to` (Rack compatible object)
30
+ # require 'hanami/router'
31
+ #
32
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
33
+ # router = Hanami::Router.new do
34
+ # get '/', to: endpoint
35
+ # end
36
+ #
37
+ # # :to is mandatory for the default resolver (`Hanami::Routing::EndpointResolver.new`),
38
+ # # This behavior can be changed by passing a custom resolver to `Hanami::Router#initialize`
39
+ #
40
+ #
41
+ #
42
+ # @example Specify an endpoint with `:to` (controller and action string)
43
+ # require 'hanami/router'
44
+ #
45
+ # router = Hanami::Router.new do
46
+ # get '/', to: 'articles#show' # => Articles::Show
47
+ # end
48
+ #
49
+ # # This is a builtin feature for a Hanami::Controller convention.
50
+ #
51
+ #
52
+ #
53
+ # @example Specify a named route with `:as`
54
+ # require 'hanami/router'
55
+ #
56
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
57
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org') do
58
+ # get '/', to: endpoint, as: :root
59
+ # end
60
+ #
61
+ # router.path(:root) # => '/'
62
+ # router.url(:root) # => 'https://hanamirb.org/'
63
+ #
64
+ # # This isn't mandatory for the default route class (`Hanami::Routing::Route`),
65
+ # # This behavior can be changed by passing a custom route to `Hanami::Router#initialize`
66
+ #
67
+ # @example Mount an application
68
+ # require 'hanami/router'
69
+ #
70
+ # router = Hanami::Router.new do
71
+ # mount Api::App, at: '/api'
72
+ # end
73
+ #
74
+ # # All the requests starting with "/api" will be forwarded to Api::App
75
+ class Router
76
+ # This error is raised when <tt>#call</tt> is invoked on a non-routable
77
+ # recognized route.
78
+ #
79
+ # @since 0.5.0
80
+ #
81
+ # @see Hanami::Router#recognize
82
+ # @see Hanami::Routing::RecognizedRoute
83
+ # @see Hanami::Routing::RecognizedRoute#call
84
+ # @see Hanami::Routing::RecognizedRoute#routable?
85
+ class NotRoutableEndpointError < Hanami::Routing::Error
86
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
87
+ PATH_INFO = 'PATH_INFO'.freeze
88
+
89
+ def initialize(env)
90
+ super %(Cannot find routable endpoint for #{ env[REQUEST_METHOD] } "#{ env[PATH_INFO] }")
91
+ end
92
+ end
93
+
94
+ # Returns the given block as it is.
95
+ #
96
+ # When Hanami::Router is used as a standalone gem and the routes are defined
97
+ # into a configuration file, some systems could raise an exception.
98
+ #
99
+ # Imagine the following file into a Ruby on Rails application:
100
+ #
101
+ # get '/', to: 'api#index'
102
+ #
103
+ # Because Ruby on Rails in production mode use to eager load code and the
104
+ # routes file uses top level method calls, it crashes the application.
105
+ #
106
+ # If we wrap these routes with <tt>Hanami::Router.define</tt>, the block
107
+ # doesn't get yielded but just returned to the caller as it is.
108
+ #
109
+ # Usually the receiver of this block is <tt>Hanami::Router#initialize</tt>,
110
+ # which finally evaluates the block.
111
+ #
112
+ # @param blk [Proc] a set of route definitions
113
+ #
114
+ # @return [Proc] the given block
115
+ #
116
+ # @since 0.5.0
117
+ #
118
+ # @example
119
+ # # apps/web/config/routes.rb
120
+ # Hanami::Router.define do
121
+ # get '/', to: 'home#index'
122
+ # end
123
+ def self.define(&blk)
124
+ blk
125
+ end
126
+
127
+ # Initialize the router.
128
+ #
129
+ # @param options [Hash] the options to initialize the router
130
+ #
131
+ # @option options [String] :scheme The HTTP scheme (defaults to `"http"`)
132
+ # @option options [String] :host The URL host (defaults to `"localhost"`)
133
+ # @option options [String] :port The URL port (defaults to `"80"`)
134
+ # @option options [Object, #resolve, #find, #action_separator] :resolver
135
+ # the route resolver (defaults to `Hanami::Routing::EndpointResolver.new`)
136
+ # @option options [Object, #generate] :route the route class
137
+ # (defaults to `Hanami::Routing::Route`)
138
+ # @option options [String] :action_separator the separator between controller
139
+ # and action name (eg. 'dashboard#show', where '#' is the :action_separator)
140
+ # @option options [Array<Symbol,String,Object #mime_types, parse>] :parsers
141
+ # the body parsers for mime types
142
+ #
143
+ # @param blk [Proc] the optional block to define the routes
144
+ #
145
+ # @return [Hanami::Router] self
146
+ #
147
+ # @since 0.1.0
148
+ #
149
+ # @example Basic example
150
+ # require 'hanami/router'
151
+ #
152
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
153
+ #
154
+ # router = Hanami::Router.new
155
+ # router.get '/', to: endpoint
156
+ #
157
+ # # or
158
+ #
159
+ # router = Hanami::Router.new do
160
+ # get '/', to: endpoint
161
+ # end
162
+ #
163
+ # @example Body parsers
164
+ # require 'json'
165
+ # require 'hanami/router'
166
+ #
167
+ # # It parses JSON body and makes the attributes available to the params
168
+ #
169
+ # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
170
+ #
171
+ # router = Hanami::Router.new(parsers: [:json]) do
172
+ # patch '/books/:id', to: endpoint
173
+ # end
174
+ #
175
+ # # From the shell
176
+ #
177
+ # curl http://localhost:2300/books/1 \
178
+ # -H "Content-Type: application/json" \
179
+ # -H "Accept: application/json" \
180
+ # -d '{"published":"true"}' \
181
+ # -X PATCH
182
+ #
183
+ # # It returns
184
+ #
185
+ # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
186
+ #
187
+ # @example Custom body parser
188
+ # require 'hanami/router'
189
+ #
190
+ # class XmlParser
191
+ # def mime_types
192
+ # ['application/xml', 'text/xml']
193
+ # end
194
+ #
195
+ # # Parse body and return a Hash
196
+ # def parse(body)
197
+ # # ...
198
+ # end
199
+ # end
200
+ #
201
+ # # It parses XML body and makes the attributes available to the params
202
+ #
203
+ # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
204
+ #
205
+ # router = Hanami::Router.new(parsers: [XmlParser.new]) do
206
+ # patch '/authors/:id', to: endpoint
207
+ # end
208
+ #
209
+ # # From the shell
210
+ #
211
+ # curl http://localhost:2300/authors/1 \
212
+ # -H "Content-Type: application/xml" \
213
+ # -H "Accept: application/xml" \
214
+ # -d '<name>LG</name>' \
215
+ # -X PATCH
216
+ #
217
+ # # It returns
218
+ #
219
+ # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
220
+ def initialize(options = {}, &blk)
221
+ @router = Routing::HttpRouter.new(options)
222
+ define(&blk)
223
+ end
224
+
225
+ # Returns self
226
+ #
227
+ # This is a duck-typing trick for compatibility with `Hanami::Application`.
228
+ # It's used by `Hanami::Routing::RoutesInspector` to inspect both apps and
229
+ # routers.
230
+ #
231
+ # @return [self]
232
+ #
233
+ # @since 0.2.0
234
+ # @api private
235
+ def routes
236
+ self
237
+ end
238
+
239
+ # To support defining routes in the `define` wrapper.
240
+ #
241
+ # @param blk [Proc] the block to define the routes
242
+ #
243
+ # @return [Hanami::Routing::Route]
244
+ #
245
+ # @since 0.2.0
246
+ #
247
+ # @example In Hanami framework
248
+ # class Application < Hanami::Application
249
+ # configure do
250
+ # routes 'config/routes'
251
+ # end
252
+ # end
253
+ #
254
+ # # In `config/routes`
255
+ #
256
+ # define do
257
+ # get # ...
258
+ # end
259
+ def define(&blk)
260
+ instance_eval(&blk) if block_given?
261
+ end
262
+
263
+ # Check if there are defined routes
264
+ #
265
+ # @return [TrueClass,FalseClass] the result of the check
266
+ #
267
+ # @since 0.2.0
268
+ # @api private
269
+ #
270
+ # @example
271
+ #
272
+ # router = Hanami::Router.new
273
+ # router.defined? # => false
274
+ #
275
+ # router = Hanami::Router.new { get '/', to: ->(env) { } }
276
+ # router.defined? # => true
277
+ def defined?
278
+ @router.routes.any?
279
+ end
280
+
281
+ # Defines a route that accepts a GET request for the given path.
282
+ #
283
+ # @param path [String] the relative URL to be matched
284
+ #
285
+ # @param options [Hash] the options to customize the route
286
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
287
+ #
288
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
289
+ #
290
+ # @return [Hanami::Routing::Route] this may vary according to the :route
291
+ # option passed to the constructor
292
+ #
293
+ # @since 0.1.0
294
+ #
295
+ # @example Fixed matching string
296
+ # require 'hanami/router'
297
+ #
298
+ # router = Hanami::Router.new
299
+ # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
300
+ #
301
+ # @example String matching with variables
302
+ # require 'hanami/router'
303
+ #
304
+ # router = Hanami::Router.new
305
+ # router.get '/flowers/:id',
306
+ # to: ->(env) {
307
+ # [
308
+ # 200,
309
+ # {},
310
+ # ["Hello from Flower no. #{ env['router.params'][:id] }!"]
311
+ # ]
312
+ # }
313
+ #
314
+ # @example Variables Constraints
315
+ # require 'hanami/router'
316
+ #
317
+ # router = Hanami::Router.new
318
+ # router.get '/flowers/:id',
319
+ # id: /\d+/,
320
+ # to: ->(env) { [200, {}, [":id must be a number!"]] }
321
+ #
322
+ # @example String matching with globbling
323
+ # require 'hanami/router'
324
+ #
325
+ # router = Hanami::Router.new
326
+ # router.get '/*',
327
+ # to: ->(env) {
328
+ # [
329
+ # 200,
330
+ # {},
331
+ # ["This is catch all: #{ env['router.params'].inspect }!"]
332
+ # ]
333
+ # }
334
+ #
335
+ # @example String matching with optional tokens
336
+ # require 'hanami/router'
337
+ #
338
+ # router = Hanami::Router.new
339
+ # router.get '/hanami(.:format)',
340
+ # to: ->(env) {
341
+ # [200, {}, ["You've requested #{ env['router.params'][:format] }!"]]
342
+ # }
343
+ #
344
+ # @example Named routes
345
+ # require 'hanami/router'
346
+ #
347
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
348
+ # router.get '/hanami',
349
+ # to: ->(env) { [200, {}, ['Hello from Hanami!']] },
350
+ # as: :hanami
351
+ #
352
+ # router.path(:hanami) # => "/hanami"
353
+ # router.url(:hanami) # => "https://hanamirb.org/hanami"
354
+ #
355
+ # @example Duck typed endpoints (Rack compatible objects)
356
+ # require 'hanami/router'
357
+ #
358
+ # router = Hanami::Router.new
359
+ #
360
+ # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
361
+ # router.get '/middleware', to: Middleware
362
+ # router.get '/rack-app', to: RackApp.new
363
+ # router.get '/method', to: ActionControllerSubclass.action(:new)
364
+ #
365
+ # # Everything that responds to #call is invoked as it is
366
+ #
367
+ # @example Duck typed endpoints (strings)
368
+ # require 'hanami/router'
369
+ #
370
+ # class RackApp
371
+ # def call(env)
372
+ # # ...
373
+ # end
374
+ # end
375
+ #
376
+ # router = Hanami::Router.new
377
+ # router.get '/hanami', to: 'rack_app' # it will map to RackApp.new
378
+ #
379
+ # @example Duck typed endpoints (string: controller + action)
380
+ # require 'hanami/router'
381
+ #
382
+ # module Flowers
383
+ # class Index
384
+ # def call(env)
385
+ # # ...
386
+ # end
387
+ # end
388
+ # end
389
+ #
390
+ # router = Hanami::Router.new
391
+ # router.get '/flowers', to: 'flowers#index'
392
+ #
393
+ # # It will map to Flowers::Index.new, which is the
394
+ # # Hanami::Controller convention.
395
+ def get(path, options = {}, &blk)
396
+ @router.get(path, options, &blk)
397
+ end
398
+
399
+ # Defines a route that accepts a POST request for the given path.
400
+ #
401
+ # @param path [String] the relative URL to be matched
402
+ #
403
+ # @param options [Hash] the options to customize the route
404
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
405
+ #
406
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
407
+ #
408
+ # @return [Hanami::Routing::Route] this may vary according to the :route
409
+ # option passed to the constructor
410
+ #
411
+ # @see Hanami::Router#get
412
+ #
413
+ # @since 0.1.0
414
+ def post(path, options = {}, &blk)
415
+ @router.post(path, options, &blk)
416
+ end
417
+
418
+ # Defines a route that accepts a PUT request for the given path.
419
+ #
420
+ # @param path [String] the relative URL to be matched
421
+ #
422
+ # @param options [Hash] the options to customize the route
423
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
424
+ #
425
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
426
+ #
427
+ # @return [Hanami::Routing::Route] this may vary according to the :route
428
+ # option passed to the constructor
429
+ #
430
+ # @see Hanami::Router#get
431
+ #
432
+ # @since 0.1.0
433
+ def put(path, options = {}, &blk)
434
+ @router.put(path, options, &blk)
435
+ end
436
+
437
+ # Defines a route that accepts a PATCH request for the given path.
438
+ #
439
+ # @param path [String] the relative URL to be matched
440
+ #
441
+ # @param options [Hash] the options to customize the route
442
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
443
+ #
444
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
445
+ #
446
+ # @return [Hanami::Routing::Route] this may vary according to the :route
447
+ # option passed to the constructor
448
+ #
449
+ # @see Hanami::Router#get
450
+ #
451
+ # @since 0.1.0
452
+ def patch(path, options = {}, &blk)
453
+ @router.patch(path, options, &blk)
454
+ end
455
+
456
+ # Defines a route that accepts a DELETE request for the given path.
457
+ #
458
+ # @param path [String] the relative URL to be matched
459
+ #
460
+ # @param options [Hash] the options to customize the route
461
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
462
+ #
463
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
464
+ #
465
+ # @return [Hanami::Routing::Route] this may vary according to the :route
466
+ # option passed to the constructor
467
+ #
468
+ # @see Hanami::Router#get
469
+ #
470
+ # @since 0.1.0
471
+ def delete(path, options = {}, &blk)
472
+ @router.delete(path, options, &blk)
473
+ end
474
+
475
+ # Defines a route that accepts a TRACE request for the given path.
476
+ #
477
+ # @param path [String] the relative URL to be matched
478
+ #
479
+ # @param options [Hash] the options to customize the route
480
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
481
+ #
482
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
483
+ #
484
+ # @return [Hanami::Routing::Route] this may vary according to the :route
485
+ # option passed to the constructor
486
+ #
487
+ # @see Hanami::Router#get
488
+ #
489
+ # @since 0.1.0
490
+ def trace(path, options = {}, &blk)
491
+ @router.trace(path, options, &blk)
492
+ end
493
+
494
+ # Defines a route that accepts a OPTIONS request for the given path.
495
+ #
496
+ # @param path [String] the relative URL to be matched
497
+ #
498
+ # @param options [Hash] the options to customize the route
499
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
500
+ #
501
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
502
+ #
503
+ # @return [Hanami::Routing::Route] this may vary according to the :route
504
+ # option passed to the constructor
505
+ #
506
+ # @see Hanami::Router#get
507
+ #
508
+ # @since 0.1.0
509
+ def options(path, options = {}, &blk)
510
+ @router.options(path, options, &blk)
511
+ end
512
+
513
+ # Defines an HTTP redirect
514
+ #
515
+ # @param path [String] the path that needs to be redirected
516
+ # @param options [Hash] the options to customize the redirect behavior
517
+ # @option options [Fixnum] the HTTP status to return (defaults to `301`)
518
+ #
519
+ # @return [Hanami::Routing::Route] the generated route.
520
+ # This may vary according to the `:route` option passed to the initializer
521
+ #
522
+ # @since 0.1.0
523
+ #
524
+ # @see Hanami::Router
525
+ #
526
+ # @example
527
+ # require 'hanami/router'
528
+ #
529
+ # Hanami::Router.new do
530
+ # redirect '/legacy', to: '/new_endpoint'
531
+ # redirect '/legacy2', to: '/new_endpoint2', code: 302
532
+ # end
533
+ #
534
+ # @example
535
+ # require 'hanami/router'
536
+ #
537
+ # router = Hanami::Router.new
538
+ # router.redirect '/legacy', to: '/new_endpoint'
539
+ def redirect(path, options = {}, &endpoint)
540
+ get(path).redirect @router.find(options), options[:code] || 301
541
+ end
542
+
543
+ # Defines a Ruby block: all the routes defined within it will be namespaced
544
+ # with the given relative path.
545
+ #
546
+ # Namespaces blocks can be nested multiple times.
547
+ #
548
+ # @param namespace [String] the relative path where the nested routes will
549
+ # be mounted
550
+ # @param blk [Proc] the block that defines the resources
551
+ #
552
+ # @return [Hanami::Routing::Namespace] the generated namespace.
553
+ #
554
+ # @since 0.1.0
555
+ #
556
+ # @see Hanami::Router
557
+ #
558
+ # @example Basic example
559
+ # require 'hanami/router'
560
+ #
561
+ # Hanami::Router.new do
562
+ # namespace 'trees' do
563
+ # get '/sequoia', to: endpoint # => '/trees/sequoia'
564
+ # end
565
+ # end
566
+ #
567
+ # @example Nested namespaces
568
+ # require 'hanami/router'
569
+ #
570
+ # Hanami::Router.new do
571
+ # namespace 'animals' do
572
+ # namespace 'mammals' do
573
+ # get '/cats', to: endpoint # => '/animals/mammals/cats'
574
+ # end
575
+ # end
576
+ # end
577
+ #
578
+ # @example
579
+ # require 'hanami/router'
580
+ #
581
+ # router = Hanami::Router.new
582
+ # router.namespace 'trees' do
583
+ # get '/sequoia', to: endpoint # => '/trees/sequoia'
584
+ # end
585
+ def namespace(namespace, &blk)
586
+ Routing::Namespace.new(self, namespace, &blk)
587
+ end
588
+
589
+ # Defines a set of named routes for a single RESTful resource.
590
+ # It has a built-in integration for Hanami::Controller.
591
+ #
592
+ # @param name [String] the name of the resource
593
+ # @param options [Hash] a set of options to customize the routes
594
+ # @option options [Array<Symbol>] :only a subset of the default routes
595
+ # that we want to generate
596
+ # @option options [Array<Symbol>] :except prevent the given routes to be
597
+ # generated
598
+ # @param blk [Proc] a block of code to generate additional routes
599
+ #
600
+ # @return [Hanami::Routing::Resource]
601
+ #
602
+ # @since 0.1.0
603
+ #
604
+ # @see Hanami::Routing::Resource
605
+ # @see Hanami::Routing::Resource::Action
606
+ # @see Hanami::Routing::Resource::Options
607
+ #
608
+ # @example Default usage
609
+ # require 'hanami/router'
610
+ #
611
+ # Hanami::Router.new do
612
+ # resource 'identity'
613
+ # end
614
+ #
615
+ # # It generates:
616
+ # #
617
+ # # +--------+----------------+-------------------+----------+----------------+
618
+ # # | Verb | Path | Action | Name | Named Route |
619
+ # # +--------+----------------+-------------------+----------+----------------+
620
+ # # | GET | /identity | Identity::Show | :show | :identity |
621
+ # # | GET | /identity/new | Identity::New | :new | :new_identity |
622
+ # # | POST | /identity | Identity::Create | :create | :identity |
623
+ # # | GET | /identity/edit | Identity::Edit | :edit | :edit_identity |
624
+ # # | PATCH | /identity | Identity::Update | :update | :identity |
625
+ # # | DELETE | /identity | Identity::Destroy | :destroy | :identity |
626
+ # # +--------+----------------+-------------------+----------+----------------+
627
+ #
628
+ #
629
+ #
630
+ # @example Limit the generated routes with :only
631
+ # require 'hanami/router'
632
+ #
633
+ # Hanami::Router.new do
634
+ # resource 'identity', only: [:show, :new, :create]
635
+ # end
636
+ #
637
+ # # It generates:
638
+ # #
639
+ # # +--------+----------------+------------------+----------+----------------+
640
+ # # | Verb | Path | Action | Name | Named Route |
641
+ # # +--------+----------------+------------------+----------+----------------+
642
+ # # | GET | /identity | Identity::Show | :show | :identity |
643
+ # # | GET | /identity/new | Identity::New | :new | :new_identity |
644
+ # # | POST | /identity | Identity::Create | :create | :identity |
645
+ # # +--------+----------------+------------------+----------+----------------+
646
+ #
647
+ #
648
+ #
649
+ # @example Limit the generated routes with :except
650
+ # require 'hanami/router'
651
+ #
652
+ # Hanami::Router.new do
653
+ # resource 'identity', except: [:edit, :update, :destroy]
654
+ # end
655
+ #
656
+ # # It generates:
657
+ # #
658
+ # # +--------+----------------+------------------+----------+----------------+
659
+ # # | Verb | Path | Action | Name | Named Route |
660
+ # # +--------+----------------+------------------+----------+----------------+
661
+ # # | GET | /identity | Identity::Show | :show | :identity |
662
+ # # | GET | /identity/new | Identity::New | :new | :new_identity |
663
+ # # | POST | /identity | Identity::Create | :create | :identity |
664
+ # # +--------+----------------+------------------+----------+----------------+
665
+ #
666
+ #
667
+ #
668
+ # @example Additional single routes
669
+ # require 'hanami/router'
670
+ #
671
+ # Hanami::Router.new do
672
+ # resource 'identity', only: [] do
673
+ # member do
674
+ # patch 'activate'
675
+ # end
676
+ # end
677
+ # end
678
+ #
679
+ # # It generates:
680
+ # #
681
+ # # +--------+--------------------+--------------------+------+--------------------+
682
+ # # | Verb | Path | Action | Name | Named Route |
683
+ # # +--------+--------------------+--------------------+------+--------------------+
684
+ # # | PATCH | /identity/activate | Identity::Activate | | :activate_identity |
685
+ # # +--------+--------------------+--------------------+------+--------------------+
686
+ #
687
+ #
688
+ #
689
+ # @example Additional collection routes
690
+ # require 'hanami/router'
691
+ #
692
+ # Hanami::Router.new do
693
+ # resource 'identity', only: [] do
694
+ # collection do
695
+ # get 'keys'
696
+ # end
697
+ # end
698
+ # end
699
+ #
700
+ # # It generates:
701
+ # #
702
+ # # +------+----------------+----------------+------+----------------+
703
+ # # | Verb | Path | Action | Name | Named Route |
704
+ # # +------+----------------+----------------+------+----------------+
705
+ # # | GET | /identity/keys | Identity::Keys | | :keys_identity |
706
+ # # +------+----------------+----------------+------+----------------+
707
+ def resource(name, options = {}, &blk)
708
+ Routing::Resource.new(self, name, options.merge(separator: @router.action_separator), &blk)
709
+ end
710
+
711
+ # Defines a set of named routes for a plural RESTful resource.
712
+ # It has a built-in integration for Hanami::Controller.
713
+ #
714
+ # @param name [String] the name of the resource
715
+ # @param options [Hash] a set of options to customize the routes
716
+ # @option options [Array<Symbol>] :only a subset of the default routes
717
+ # that we want to generate
718
+ # @option options [Array<Symbol>] :except prevent the given routes to be
719
+ # generated
720
+ # @param blk [Proc] a block of code to generate additional routes
721
+ #
722
+ # @return [Hanami::Routing::Resources]
723
+ #
724
+ # @since 0.1.0
725
+ #
726
+ # @see Hanami::Routing::Resources
727
+ # @see Hanami::Routing::Resources::Action
728
+ # @see Hanami::Routing::Resource::Options
729
+ #
730
+ # @example Default usage
731
+ # require 'hanami/router'
732
+ #
733
+ # Hanami::Router.new do
734
+ # resources 'articles'
735
+ # end
736
+ #
737
+ # # It generates:
738
+ # #
739
+ # # +--------+--------------------+-------------------+----------+----------------+
740
+ # # | Verb | Path | Action | Name | Named Route |
741
+ # # +--------+--------------------+-------------------+----------+----------------+
742
+ # # | GET | /articles | Articles::Index | :index | :articles |
743
+ # # | GET | /articles/:id | Articles::Show | :show | :articles |
744
+ # # | GET | /articles/new | Articles::New | :new | :new_articles |
745
+ # # | POST | /articles | Articles::Create | :create | :articles |
746
+ # # | GET | /articles/:id/edit | Articles::Edit | :edit | :edit_articles |
747
+ # # | PATCH | /articles/:id | Articles::Update | :update | :articles |
748
+ # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
749
+ # # +--------+--------------------+-------------------+----------+----------------+
750
+ #
751
+ #
752
+ #
753
+ # @example Limit the generated routes with :only
754
+ # require 'hanami/router'
755
+ #
756
+ # Hanami::Router.new do
757
+ # resources 'articles', only: [:index]
758
+ # end
759
+ #
760
+ # # It generates:
761
+ # #
762
+ # # +------+-----------+-----------------+--------+-------------+
763
+ # # | Verb | Path | Action | Name | Named Route |
764
+ # # +------+-----------+-----------------+--------+-------------+
765
+ # # | GET | /articles | Articles::Index | :index | :articles |
766
+ # # +------+-----------+-----------------+--------+-------------+
767
+ #
768
+ #
769
+ #
770
+ # @example Limit the generated routes with :except
771
+ # require 'hanami/router'
772
+ #
773
+ # Hanami::Router.new do
774
+ # resources 'articles', except: [:edit, :update]
775
+ # end
776
+ #
777
+ # # It generates:
778
+ # #
779
+ # # +--------+--------------------+-------------------+----------+----------------+
780
+ # # | Verb | Path | Action | Name | Named Route |
781
+ # # +--------+--------------------+-------------------+----------+----------------+
782
+ # # | GET | /articles | Articles::Index | :index | :articles |
783
+ # # | GET | /articles/:id | Articles::Show | :show | :articles |
784
+ # # | GET | /articles/new | Articles::New | :new | :new_articles |
785
+ # # | POST | /articles | Articles::Create | :create | :articles |
786
+ # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
787
+ # # +--------+--------------------+-------------------+----------+----------------+
788
+ #
789
+ #
790
+ #
791
+ # @example Additional single routes
792
+ # require 'hanami/router'
793
+ #
794
+ # Hanami::Router.new do
795
+ # resources 'articles', only: [] do
796
+ # member do
797
+ # patch 'publish'
798
+ # end
799
+ # end
800
+ # end
801
+ #
802
+ # # It generates:
803
+ # #
804
+ # # +--------+-----------------------+-------------------+------+-------------------+
805
+ # # | Verb | Path | Action | Name | Named Route |
806
+ # # +--------+-----------------------+-------------------+------+-------------------+
807
+ # # | PATCH | /articles/:id/publish | Articles::Publish | | :publish_articles |
808
+ # # +--------+-----------------------+-------------------+------+-------------------+
809
+ #
810
+ #
811
+ #
812
+ # @example Additional collection routes
813
+ # require 'hanami/router'
814
+ #
815
+ # Hanami::Router.new do
816
+ # resources 'articles', only: [] do
817
+ # collection do
818
+ # get 'search'
819
+ # end
820
+ # end
821
+ # end
822
+ #
823
+ # # It generates:
824
+ # #
825
+ # # +------+------------------+------------------+------+------------------+
826
+ # # | Verb | Path | Action | Name | Named Route |
827
+ # # +------+------------------+------------------+------+------------------+
828
+ # # | GET | /articles/search | Articles::Search | | :search_articles |
829
+ # # +------+------------------+------------------+------+------------------+
830
+ def resources(name, options = {}, &blk)
831
+ Routing::Resources.new(self, name, options.merge(separator: @router.action_separator), &blk)
832
+ end
833
+
834
+ # Mount a Rack application at the specified path.
835
+ # All the requests starting with the specified path, will be forwarded to
836
+ # the given application.
837
+ #
838
+ # All the other methods (eg #get) support callable objects, but they
839
+ # restrict the range of the acceptable HTTP verb. Mounting an application
840
+ # with #mount doesn't apply this kind of restriction at the router level,
841
+ # but let the application to decide.
842
+ #
843
+ # @param app [#call] a class or an object that responds to #call
844
+ # @param options [Hash] the options to customize the mount
845
+ # @option options [:at] the relative path where to mount the app
846
+ #
847
+ # @since 0.1.1
848
+ #
849
+ # @example Basic usage
850
+ # require 'hanami/router'
851
+ #
852
+ # Hanami::Router.new do
853
+ # mount Api::App.new, at: '/api'
854
+ # end
855
+ #
856
+ # # Requests:
857
+ # #
858
+ # # GET /api # => 200
859
+ # # GET /api/articles # => 200
860
+ # # POST /api/articles # => 200
861
+ # # GET /api/unknown # => 404
862
+ #
863
+ # @example Difference between #get and #mount
864
+ # require 'hanami/router'
865
+ #
866
+ # Hanami::Router.new do
867
+ # get '/rack1', to: RackOne.new
868
+ # mount RackTwo.new, at: '/rack2'
869
+ # end
870
+ #
871
+ # # Requests:
872
+ # #
873
+ # # # /rack1 will only accept GET
874
+ # # GET /rack1 # => 200 (RackOne.new)
875
+ # # POST /rack1 # => 405
876
+ # #
877
+ # # # /rack2 accepts all the verbs and delegate the decision to RackTwo
878
+ # # GET /rack2 # => 200 (RackTwo.new)
879
+ # # POST /rack2 # => 200 (RackTwo.new)
880
+ #
881
+ # @example Types of mountable applications
882
+ # require 'hanami/router'
883
+ #
884
+ # class RackOne
885
+ # def self.call(env)
886
+ # end
887
+ # end
888
+ #
889
+ # class RackTwo
890
+ # def call(env)
891
+ # end
892
+ # end
893
+ #
894
+ # class RackThree
895
+ # def call(env)
896
+ # end
897
+ # end
898
+ #
899
+ # module Dashboard
900
+ # class Index
901
+ # def call(env)
902
+ # end
903
+ # end
904
+ # end
905
+ #
906
+ # Hanami::Router.new do
907
+ # mount RackOne, at: '/rack1'
908
+ # mount RackTwo, at: '/rack2'
909
+ # mount RackThree.new, at: '/rack3'
910
+ # mount ->(env) {[200, {}, ['Rack Four']]}, at: '/rack4'
911
+ # mount 'dashboard#index', at: '/dashboard'
912
+ # end
913
+ #
914
+ # # 1. RackOne is used as it is (class), because it respond to .call
915
+ # # 2. RackTwo is initialized, because it respond to #call
916
+ # # 3. RackThree is used as it is (object), because it respond to #call
917
+ # # 4. That Proc is used as it is, because it respond to #call
918
+ # # 5. That string is resolved as Dashboard::Index (Hanami::Controller)
919
+ def mount(app, options)
920
+ @router.mount(app, options)
921
+ end
922
+
923
+ # Resolve the given Rack env to a registered endpoint and invoke it.
924
+ #
925
+ # @param env [Hash] a Rack env instance
926
+ #
927
+ # @return [Rack::Response, Array]
928
+ #
929
+ # @since 0.1.0
930
+ def call(env)
931
+ @router.call(env)
932
+ end
933
+
934
+ # Recognize the given env, path, or name and return a route for testing
935
+ # inspection.
936
+ #
937
+ # If the route cannot be recognized, it still returns an object for testing
938
+ # inspection.
939
+ #
940
+ # @param env [Hash, String, Symbol] Rack env, path or route name
941
+ # @param options [Hash] a set of options for Rack env or route params
942
+ # @param params [Hash] a set of params
943
+ #
944
+ # @return [Hanami::Routing::RecognizedRoute] the recognized route
945
+ #
946
+ # @since 0.5.0
947
+ #
948
+ # @see Hanami::Router#env_for
949
+ # @see Hanami::Routing::RecognizedRoute
950
+ #
951
+ # @example Successful Path Recognition
952
+ # require 'hanami/router'
953
+ #
954
+ # router = Hanami::Router.new do
955
+ # get '/books/:id', to: 'books#show', as: :book
956
+ # end
957
+ #
958
+ # route = router.recognize('/books/23')
959
+ # route.verb # => "GET" (default)
960
+ # route.routable? # => true
961
+ # route.params # => {:id=>"23"}
962
+ #
963
+ # @example Successful Rack Env Recognition
964
+ # require 'hanami/router'
965
+ #
966
+ # router = Hanami::Router.new do
967
+ # get '/books/:id', to: 'books#show', as: :book
968
+ # end
969
+ #
970
+ # route = router.recognize(Rack::MockRequest.env_for('/books/23'))
971
+ # route.verb # => "GET" (default)
972
+ # route.routable? # => true
973
+ # route.params # => {:id=>"23"}
974
+ #
975
+ # @example Successful Named Route Recognition
976
+ # require 'hanami/router'
977
+ #
978
+ # router = Hanami::Router.new do
979
+ # get '/books/:id', to: 'books#show', as: :book
980
+ # end
981
+ #
982
+ # route = router.recognize(:book, id: 23)
983
+ # route.verb # => "GET" (default)
984
+ # route.routable? # => true
985
+ # route.params # => {:id=>"23"}
986
+ #
987
+ # @example Failing Recognition For Unknown Path
988
+ # require 'hanami/router'
989
+ #
990
+ # router = Hanami::Router.new do
991
+ # get '/books/:id', to: 'books#show', as: :book
992
+ # end
993
+ #
994
+ # route = router.recognize('/books')
995
+ # route.verb # => "GET" (default)
996
+ # route.routable? # => false
997
+ #
998
+ # @example Failing Recognition For Path With Wrong HTTP Verb
999
+ # require 'hanami/router'
1000
+ #
1001
+ # router = Hanami::Router.new do
1002
+ # get '/books/:id', to: 'books#show', as: :book
1003
+ # end
1004
+ #
1005
+ # route = router.recognize('/books/23', method: :post)
1006
+ # route.verb # => "POST"
1007
+ # route.routable? # => false
1008
+ #
1009
+ # @example Failing Recognition For Rack Env With Wrong HTTP Verb
1010
+ # require 'hanami/router'
1011
+ #
1012
+ # router = Hanami::Router.new do
1013
+ # get '/books/:id', to: 'books#show', as: :book
1014
+ # end
1015
+ #
1016
+ # route = router.recognize(Rack::MockRequest.env_for('/books/23', method: :post))
1017
+ # route.verb # => "POST"
1018
+ # route.routable? # => false
1019
+ #
1020
+ # @example Failing Recognition Named Route With Wrong Params
1021
+ # require 'hanami/router'
1022
+ #
1023
+ # router = Hanami::Router.new do
1024
+ # get '/books/:id', to: 'books#show', as: :book
1025
+ # end
1026
+ #
1027
+ # route = router.recognize(:book)
1028
+ # route.verb # => "GET" (default)
1029
+ # route.routable? # => false
1030
+ #
1031
+ # @example Failing Recognition Named Route With Wrong HTTP Verb
1032
+ # require 'hanami/router'
1033
+ #
1034
+ # router = Hanami::Router.new do
1035
+ # get '/books/:id', to: 'books#show', as: :book
1036
+ # end
1037
+ #
1038
+ # route = router.recognize(:book, {method: :post}, {id: 1})
1039
+ # route.verb # => "POST"
1040
+ # route.routable? # => false
1041
+ # route.params # => {:id=>"1"}
1042
+ def recognize(env, options = {}, params = nil)
1043
+ require 'hanami/routing/recognized_route'
1044
+
1045
+ env = env_for(env, options, params)
1046
+ responses, _ = *@router.recognize(env)
1047
+
1048
+ Routing::RecognizedRoute.new(
1049
+ responses.nil? ? responses : responses.first,
1050
+ env, @router)
1051
+ end
1052
+
1053
+ # Generate an relative URL for a specified named route.
1054
+ # The additional arguments will be used to compose the relative URL - in
1055
+ # case it has tokens to match - and for compose the query string.
1056
+ #
1057
+ # @param route [Symbol] the route name
1058
+ #
1059
+ # @return [String]
1060
+ #
1061
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
1062
+ # recognize a route, because of the given arguments.
1063
+ #
1064
+ # @since 0.1.0
1065
+ #
1066
+ # @example
1067
+ # require 'hanami/router'
1068
+ #
1069
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
1070
+ # router.get '/login', to: 'sessions#new', as: :login
1071
+ # router.get '/:name', to: 'frameworks#show', as: :framework
1072
+ #
1073
+ # router.path(:login) # => "/login"
1074
+ # router.path(:login, return_to: '/dashboard') # => "/login?return_to=%2Fdashboard"
1075
+ # router.path(:framework, name: 'router') # => "/router"
1076
+ def path(route, *args)
1077
+ @router.path(route, *args)
1078
+ end
1079
+
1080
+ # Generate a URL for a specified named route.
1081
+ # The additional arguments will be used to compose the relative URL - in
1082
+ # case it has tokens to match - and for compose the query string.
1083
+ #
1084
+ # @param route [Symbol] the route name
1085
+ #
1086
+ # @return [String]
1087
+ #
1088
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
1089
+ # recognize a route, because of the given arguments.
1090
+ #
1091
+ # @since 0.1.0
1092
+ #
1093
+ # @example
1094
+ # require 'hanami/router'
1095
+ #
1096
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
1097
+ # router.get '/login', to: 'sessions#new', as: :login
1098
+ # router.get '/:name', to: 'frameworks#show', as: :framework
1099
+ #
1100
+ # router.url(:login) # => "https://hanamirb.org/login"
1101
+ # router.url(:login, return_to: '/dashboard') # => "https://hanamirb.org/login?return_to=%2Fdashboard"
1102
+ # router.url(:framework, name: 'router') # => "https://hanamirb.org/router"
1103
+ def url(route, *args)
1104
+ @router.url(route, *args)
1105
+ end
1106
+
1107
+ # Returns an routes inspector
1108
+ #
1109
+ # @since 0.2.0
1110
+ #
1111
+ # @see Hanami::Routing::RoutesInspector
1112
+ #
1113
+ # @example
1114
+ # require 'hanami/router'
1115
+ #
1116
+ # router = Hanami::Router.new do
1117
+ # get '/', to: 'home#index'
1118
+ # get '/login', to: 'sessions#new', as: :login
1119
+ # post '/login', to: 'sessions#create'
1120
+ # delete '/logout', to: 'sessions#destroy', as: :logout
1121
+ # end
1122
+ #
1123
+ # puts router.inspector
1124
+ # # => GET, HEAD / Home::Index
1125
+ # login GET, HEAD /login Sessions::New
1126
+ # POST /login Sessions::Create
1127
+ # logout GET, HEAD /logout Sessions::Destroy
1128
+ def inspector
1129
+ require 'hanami/routing/routes_inspector'
1130
+ Routing::RoutesInspector.new(@router.routes)
1131
+ end
1132
+
1133
+ protected
1134
+
1135
+ # Fabricate Rack env for the given Rack env, path or named route
1136
+ #
1137
+ # @param env [Hash, String, Symbol] Rack env, path or route name
1138
+ # @param options [Hash] a set of options for Rack env or route params
1139
+ # @param params [Hash] a set of params
1140
+ #
1141
+ # @return [Hash] Rack env
1142
+ #
1143
+ # @since 0.5.0
1144
+ # @api private
1145
+ #
1146
+ # @see Hanami::Router#recognize
1147
+ # @see http://www.rubydoc.info/github/rack/rack/Rack%2FMockRequest.env_for
1148
+ def env_for(env, options = {}, params = nil)
1149
+ env = case env
1150
+ when String
1151
+ Rack::MockRequest.env_for(env, options)
1152
+ when Symbol
1153
+ begin
1154
+ url = path(env, params || options)
1155
+ return env_for(url, options)
1156
+ rescue Hanami::Routing::InvalidRouteException
1157
+ {}
1158
+ end
1159
+ else
1160
+ env
1161
+ end
1162
+ end
6
1163
  end
7
1164
  end