hanami-router 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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