hanami-router 1.3.2 → 2.0.0.alpha1

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -3
  3. data/README.md +192 -154
  4. data/hanami-router.gemspec +23 -20
  5. data/lib/hanami/middleware/body_parser.rb +17 -13
  6. data/lib/hanami/middleware/body_parser/class_interface.rb +56 -56
  7. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  8. data/lib/hanami/middleware/body_parser/json_parser.rb +5 -3
  9. data/lib/hanami/middleware/error.rb +16 -0
  10. data/lib/hanami/router.rb +262 -149
  11. data/lib/hanami/router/version.rb +3 -1
  12. data/lib/hanami/routing.rb +193 -0
  13. data/lib/hanami/routing/endpoint.rb +122 -104
  14. data/lib/hanami/routing/endpoint_resolver.rb +20 -16
  15. data/lib/hanami/routing/prefix.rb +102 -0
  16. data/lib/hanami/routing/recognized_route.rb +40 -26
  17. data/lib/hanami/routing/resource.rb +9 -7
  18. data/lib/hanami/routing/resource/action.rb +58 -33
  19. data/lib/hanami/routing/resource/nested.rb +4 -1
  20. data/lib/hanami/routing/resource/options.rb +3 -1
  21. data/lib/hanami/routing/resources.rb +6 -4
  22. data/lib/hanami/routing/resources/action.rb +11 -6
  23. data/lib/hanami/routing/routes_inspector.rb +22 -20
  24. data/lib/hanami/routing/scope.rb +112 -0
  25. metadata +47 -25
  26. data/lib/hanami-router.rb +0 -1
  27. data/lib/hanami/routing/error.rb +0 -7
  28. data/lib/hanami/routing/force_ssl.rb +0 -212
  29. data/lib/hanami/routing/http_router.rb +0 -220
  30. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  31. data/lib/hanami/routing/namespace.rb +0 -98
  32. data/lib/hanami/routing/parsers.rb +0 -113
  33. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  34. data/lib/hanami/routing/parsing/parser.rb +0 -61
  35. data/lib/hanami/routing/route.rb +0 -71
@@ -1,30 +1,33 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'hanami/router/version'
5
+ require "hanami/router/version"
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = 'hanami-router'
8
+ spec.name = "hanami-router"
8
9
  spec.version = Hanami::Router::VERSION
9
- spec.authors = ['Luca Guidi']
10
- spec.email = ['me@lucaguidi.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'
10
+ spec.authors = ["Luca Guidi"]
11
+ spec.email = ["me@lucaguidi.com"]
12
+ spec.description = "Rack compatible HTTP router for Ruby"
13
+ spec.summary = "Rack compatible HTTP router for Ruby and Hanami"
14
+ spec.homepage = "http://hanamirb.org"
15
+ spec.license = "MIT"
15
16
 
16
- spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($/)
17
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($INPUT_RECORD_SEPARATOR)
17
18
  spec.executables = []
18
19
  spec.test_files = spec.files.grep(%r{^(test)/})
19
- spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.require_paths = ["lib"]
21
+ spec.required_ruby_version = ">= 2.5.0"
21
22
 
22
- spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'http_router', '0.11.2'
24
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency "rack", "~> 2.0"
24
+ spec.add_dependency "mustermann", "~> 1.0"
25
+ spec.add_dependency "mustermann-contrib", "~> 1.0"
26
+ spec.add_dependency "hanami-utils", "~> 2.0.alpha"
27
+ spec.add_dependency "dry-inflector", "~> 0.1"
25
28
 
26
- spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
27
- spec.add_development_dependency 'rake', '~> 13'
28
- spec.add_development_dependency 'rack-test', '~> 1.0'
29
- spec.add_development_dependency 'rspec', '~> 3.7'
29
+ spec.add_development_dependency "bundler", ">= 1.6", "< 3"
30
+ spec.add_development_dependency "rake", "~> 12"
31
+ spec.add_development_dependency "rack-test", "~> 1.0"
32
+ spec.add_development_dependency "rspec", "~> 3.7"
30
33
  end
@@ -1,5 +1,8 @@
1
- require 'hanami/utils/hash'
2
- require_relative 'body_parser/class_interface'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/hash"
4
+ require "hanami/middleware/error"
5
+ require_relative "body_parser/class_interface"
3
6
 
4
7
  module Hanami
5
8
  module Middleware
@@ -8,7 +11,7 @@ module Hanami
8
11
  class BodyParser
9
12
  # @since 1.3.0
10
13
  # @api private
11
- CONTENT_TYPE = 'CONTENT_TYPE'.freeze
14
+ CONTENT_TYPE = "CONTENT_TYPE"
12
15
 
13
16
  # @since 1.3.0
14
17
  # @api private
@@ -16,17 +19,17 @@ module Hanami
16
19
 
17
20
  # @since 1.3.0
18
21
  # @api private
19
- RACK_INPUT = 'rack.input'.freeze
22
+ RACK_INPUT = "rack.input"
20
23
 
21
24
  # @since 1.3.0
22
25
  # @api private
23
- ROUTER_PARAMS = 'router.params'.freeze
26
+ ROUTER_PARAMS = "router.params"
24
27
 
25
28
  # @api private
26
- ROUTER_PARSED_BODY = 'router.parsed_body'.freeze
29
+ ROUTER_PARSED_BODY = "router.parsed_body"
27
30
 
28
31
  # @api private
29
- FALLBACK_KEY = '_'.freeze
32
+ FALLBACK_KEY = "_"
30
33
 
31
34
  extend ClassInterface
32
35
 
@@ -55,18 +58,18 @@ module Hanami
55
58
  parser_names = Array(parser_names)
56
59
  return {} if parser_names.empty?
57
60
 
58
- parser_names.each_with_object({}) { |name, parsers|
61
+ parser_names.each_with_object({}) do |name, parsers|
59
62
  parser = self.class.for(name)
60
63
 
61
64
  parser.mime_types.each do |mime|
62
65
  parsers[mime] = parser
63
66
  end
64
- }
67
+ end
65
68
  end
66
69
 
67
70
  # @api private
68
71
  def _symbolize(body)
69
- if body.is_a?(Hash)
72
+ if body.is_a?(::Hash)
70
73
  Utils::Hash.deep_symbolize(body)
71
74
  else
72
75
  { FALLBACK_KEY => body }
@@ -82,9 +85,10 @@ module Hanami
82
85
 
83
86
  # @api private
84
87
  def media_type(env)
85
- if ct = content_type(env)
86
- ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
87
- end
88
+ ct = content_type(env)
89
+ return unless ct
90
+
91
+ ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
88
92
  end
89
93
 
90
94
  # @api private
@@ -1,56 +1,56 @@
1
- require 'hanami/utils/class'
2
- require 'hanami/utils/string'
3
- require_relative 'errors'
4
-
5
- module Hanami
6
- module Middleware
7
- class BodyParser
8
- # @api private
9
- # @since 1.3.0
10
- module ClassInterface
11
- # @api private
12
- # @since 1.3.0
13
- def for(parser)
14
- parser =
15
- case parser
16
- when String, Symbol
17
- require_parser(parser)
18
- when Class
19
- parser.new
20
- else
21
- parser
22
- end
23
-
24
- ensure_parser parser
25
-
26
- parser
27
- end
28
-
29
- private
30
-
31
- # @api private
32
- # @since 1.3.0
33
- PARSER_METHODS = %i[mime_types parse].freeze
34
-
35
- # @api private
36
- # @since 1.3.0
37
- def ensure_parser(parser)
38
- unless PARSER_METHODS.all? { |method| parser.respond_to?(method) }
39
- raise InvalidParserError.new(parser)
40
- end
41
- end
42
-
43
- # @api private
44
- # @since 1.3.0
45
- def require_parser(parser)
46
- require "hanami/middleware/body_parser/#{parser}_parser"
47
-
48
- parser = Utils::String.classify(parser)
49
- Utils::Class.load!("Hanami::Middleware::BodyParser::#{parser}Parser").new
50
- rescue LoadError, NameError
51
- raise UnknownParserError.new(parser)
52
- end
53
- end
54
- end
55
- end
56
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/class"
4
+ require "hanami/utils/string"
5
+ require_relative "errors"
6
+
7
+ module Hanami
8
+ module Middleware
9
+ class BodyParser
10
+ # @api private
11
+ # @since 1.3.0
12
+ module ClassInterface
13
+ # @api private
14
+ # @since 1.3.0
15
+ def for(parser) # rubocop:disable Metrics/MethodLength
16
+ parser =
17
+ case parser
18
+ when String, Symbol
19
+ require_parser(parser)
20
+ when Class
21
+ parser.new
22
+ else
23
+ parser
24
+ end
25
+
26
+ ensure_parser parser
27
+
28
+ parser
29
+ end
30
+
31
+ private
32
+
33
+ # @api private
34
+ # @since 1.3.0
35
+ PARSER_METHODS = %i[mime_types parse].freeze
36
+
37
+ # @api private
38
+ # @since 1.3.0
39
+ def ensure_parser(parser)
40
+ raise InvalidParserError.new(parser) unless PARSER_METHODS.all? { |method| parser.respond_to?(method) }
41
+ end
42
+
43
+ # @api private
44
+ # @since 1.3.0
45
+ def require_parser(parser)
46
+ require "hanami/middleware/body_parser/#{parser}_parser"
47
+
48
+ parser = Utils::String.classify(parser)
49
+ Utils::Class.load!("Hanami::Middleware::BodyParser::#{parser}Parser").new
50
+ rescue LoadError, NameError
51
+ raise UnknownParserError.new(parser)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,4 +1,4 @@
1
- require 'hanami/routing/error'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Hanami
4
4
  module Middleware
@@ -9,14 +9,17 @@ module Hanami
9
9
  # This is raised when parser fails to parse the body
10
10
  #
11
11
  # @since 1.3.0
12
- class BodyParsingError < Hanami::Routing::Parsing::BodyParsingError
12
+ class BodyParsingError < Hanami::Middleware::Error
13
13
  end
14
14
 
15
15
  # @since 1.3.0
16
- class UnknownParserError < Hanami::Routing::Parsing::UnknownParserError
16
+ class UnknownParserError < Hanami::Middleware::Error
17
+ def initialize(name)
18
+ super("Unknown body parser: `#{name.inspect}'")
19
+ end
17
20
  end
18
21
 
19
- class InvalidParserError < Hanami::Routing::Error
22
+ class InvalidParserError < Hanami::Middleware::Error
20
23
  end
21
24
  end
22
25
  end
@@ -1,5 +1,7 @@
1
- require 'hanami/utils/json'
2
- require_relative 'errors'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/json"
4
+ require_relative "errors"
3
5
 
4
6
  module Hanami
5
7
  module Middleware
@@ -10,7 +12,7 @@ module Hanami
10
12
  # @since 1.3.0
11
13
  # @api private
12
14
  def mime_types
13
- ['application/json', 'application/vnd.api+json']
15
+ ["application/json", "application/vnd.api+json"]
14
16
  end
15
17
 
16
18
  # Parse a json string
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # Hanami Rack middleware
5
+ #
6
+ # @since 1.3.0
7
+ module Middleware
8
+ unless defined?(Error)
9
+ # Base error for Rack middleware
10
+ #
11
+ # @since 2.0.0
12
+ class Error < ::StandardError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,9 +1,9 @@
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'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/request"
4
+ require "dry/inflector"
5
+ require "hanami/routing"
6
+ require "hanami/utils/hash"
7
7
 
8
8
  # Hanami
9
9
  #
@@ -75,7 +75,12 @@ module Hanami
75
75
  # end
76
76
  #
77
77
  # # All the requests starting with "/api" will be forwarded to Api::App
78
- class Router
78
+ #
79
+ class Router # rubocop:disable Metrics/ClassLength
80
+ # @since 2.0.0
81
+ # @api private
82
+ attr_reader :inflector
83
+
79
84
  # This error is raised when <tt>#call</tt> is invoked on a non-routable
80
85
  # recognized route.
81
86
  #
@@ -88,15 +93,15 @@ module Hanami
88
93
  class NotRoutableEndpointError < Hanami::Routing::Error
89
94
  # @since 0.5.0
90
95
  # @api private
91
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
96
+ REQUEST_METHOD = "REQUEST_METHOD"
92
97
 
93
98
  # @since 0.5.0
94
99
  # @api private
95
- PATH_INFO = 'PATH_INFO'.freeze
100
+ PATH_INFO = "PATH_INFO"
96
101
 
97
102
  # @since 0.5.0
98
103
  def initialize(env)
99
- super %(Cannot find routable endpoint for #{ env[REQUEST_METHOD] } "#{ env[PATH_INFO] }")
104
+ super %(Cannot find routable endpoint for #{env[REQUEST_METHOD]} "#{env[PATH_INFO]}")
100
105
  end
101
106
  end
102
107
 
@@ -106,7 +111,7 @@ module Hanami
106
111
  # @api private
107
112
  #
108
113
  # @see Hanami::Router#root
109
- ROOT_PATH = '/'.freeze
114
+ ROOT_PATH = "/"
110
115
 
111
116
  # Returns the given block as it is.
112
117
  #
@@ -154,8 +159,8 @@ module Hanami
154
159
  # (defaults to `Hanami::Routing::Route`)
155
160
  # @option options [String] :action_separator the separator between controller
156
161
  # and action name (eg. 'dashboard#show', where '#' is the :action_separator)
157
- # @option options [Array<Symbol,String,Object #mime_types, parse>] :parsers
158
- # the body parsers for mime types
162
+ # @option options [Object, #pluralize, #singularize] :inflector
163
+ # the inflector class (defaults to `Dry::Inflector.new`)
159
164
  #
160
165
  # @param blk [Proc] the optional block to define the routes
161
166
  #
@@ -178,51 +183,53 @@ module Hanami
178
183
  # end
179
184
  #
180
185
  # @example Body parsers
181
- # require 'json'
182
- # require 'hanami/router'
183
186
  #
184
- # # It parses JSON body and makes the attributes available to the params
187
+ # require 'hanami/router'
188
+ # require 'hanami/middleware/body_parser'
185
189
  #
186
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
190
+ # app = Hanami::Router.new do
191
+ # patch '/books/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] }
192
+ # end
187
193
  #
188
- # router = Hanami::Router.new(parsers: [:json]) do
189
- # patch '/books/:id', to: endpoint
190
- # end
194
+ # use Hanami::Middleware::BodyParser, :json
195
+ # run app
191
196
  #
192
- # # From the shell
197
+ # # From the shell
193
198
  #
194
- # curl http://localhost:2300/books/1 \
195
- # -H "Content-Type: application/json" \
196
- # -H "Accept: application/json" \
197
- # -d '{"published":"true"}' \
198
- # -X PATCH
199
+ # curl http://localhost:2300/books/1 \
200
+ # -H "Content-Type: application/json" \
201
+ # -H "Accept: application/json" \
202
+ # -d '{"published":"true"}' \
203
+ # -X PATCH
199
204
  #
200
205
  # # It returns
201
206
  #
202
207
  # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
203
208
  #
204
209
  # @example Custom body parser
205
- # require 'hanami/router'
206
210
  #
207
- # class XmlParser
208
- # def mime_types
209
- # ['application/xml', 'text/xml']
210
- # end
211
+ # require 'hanami/router'
212
+ # require 'hanami/middleware/body_parser'
211
213
  #
212
- # # Parse body and return a Hash
213
- # def parse(body)
214
- # # ...
215
- # end
216
- # end
217
214
  #
218
- # # It parses XML body and makes the attributes available to the params
215
+ # class XmlParser < Hanami::Middleware::BodyParser::Parser
216
+ # def mime_types
217
+ # ['application/xml', 'text/xml']
218
+ # end
219
219
  #
220
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
220
+ # # Parse body and return a Hash
221
+ # def parse(body)
222
+ # # parse xml
223
+ # end
224
+ # end
221
225
  #
222
- # router = Hanami::Router.new(parsers: [XmlParser.new]) do
223
- # patch '/authors/:id', to: endpoint
226
+ # app = Hanami::Router.new do
227
+ # patch '/authors/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] }
224
228
  # end
225
229
  #
230
+ # use Hanami::Middleware::BodyParser, XmlParser
231
+ # run app
232
+ #
226
233
  # # From the shell
227
234
  #
228
235
  # curl http://localhost:2300/authors/1 \
@@ -234,9 +241,29 @@ module Hanami
234
241
  # # It returns
235
242
  #
236
243
  # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
237
- def initialize(options = {}, &blk)
238
- @router = Routing::HttpRouter.new(options)
239
- define(&blk)
244
+ #
245
+ # rubocop:disable Metrics/MethodLength
246
+ def initialize(scheme: "http", host: "localhost", port: 80, prefix: "", namespace: nil, configuration: nil, inflector: Dry::Inflector.new, not_found: NOT_FOUND, not_allowed: NOT_ALLOWED, &blk)
247
+ @routes = []
248
+ @named = {}
249
+ @namespace = namespace
250
+ @configuration = configuration
251
+ @base = Routing::Uri.build(scheme: scheme, host: host, port: port)
252
+ @prefix = Utils::PathPrefix.new(prefix)
253
+ @inflector = inflector
254
+ @not_found = not_found
255
+ @not_allowed = not_allowed
256
+ instance_eval(&blk) unless blk.nil?
257
+ freeze
258
+ end
259
+ # rubocop:enable Metrics/MethodLength
260
+
261
+ # Freeze the router
262
+ #
263
+ # @since 2.0.0
264
+ def freeze
265
+ @routes.freeze
266
+ super
240
267
  end
241
268
 
242
269
  # Returns self
@@ -253,30 +280,6 @@ module Hanami
253
280
  self
254
281
  end
255
282
 
256
- # To support defining routes in the `define` wrapper.
257
- #
258
- # @param blk [Proc] the block to define the routes
259
- #
260
- # @return [Hanami::Routing::Route]
261
- #
262
- # @since 0.2.0
263
- #
264
- # @example In Hanami framework
265
- # class Application < Hanami::Application
266
- # configure do
267
- # routes 'config/routes'
268
- # end
269
- # end
270
- #
271
- # # In `config/routes`
272
- #
273
- # define do
274
- # get # ...
275
- # end
276
- def define(&blk)
277
- instance_eval(&blk) if block_given?
278
- end
279
-
280
283
  # Check if there are defined routes
281
284
  #
282
285
  # @return [TrueClass,FalseClass] the result of the check
@@ -292,7 +295,7 @@ module Hanami
292
295
  # router = Hanami::Router.new { get '/', to: ->(env) { } }
293
296
  # router.defined? # => true
294
297
  def defined?
295
- @router.routes.any?
298
+ @routes.any?
296
299
  end
297
300
 
298
301
  # Defines a route that accepts a GET request for the given path.
@@ -409,8 +412,8 @@ module Hanami
409
412
  #
410
413
  # # It will map to Flowers::Index.new, which is the
411
414
  # # Hanami::Controller convention.
412
- def get(path, options = {}, &blk)
413
- @router.get(path, options, &blk)
415
+ def get(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
416
+ add_route(GET, path, to, as, namespace, configuration, constraints, &blk)
414
417
  end
415
418
 
416
419
  # Defines a route that accepts a POST request for the given path.
@@ -428,8 +431,8 @@ module Hanami
428
431
  # @see Hanami::Router#get
429
432
  #
430
433
  # @since 0.1.0
431
- def post(path, options = {}, &blk)
432
- @router.post(path, options, &blk)
434
+ def post(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
435
+ add_route(POST, path, to, as, namespace, configuration, constraints, &blk)
433
436
  end
434
437
 
435
438
  # Defines a route that accepts a PUT request for the given path.
@@ -447,8 +450,8 @@ module Hanami
447
450
  # @see Hanami::Router#get
448
451
  #
449
452
  # @since 0.1.0
450
- def put(path, options = {}, &blk)
451
- @router.put(path, options, &blk)
453
+ def put(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
454
+ add_route(PUT, path, to, as, namespace, configuration, constraints, &blk)
452
455
  end
453
456
 
454
457
  # Defines a route that accepts a PATCH request for the given path.
@@ -466,8 +469,8 @@ module Hanami
466
469
  # @see Hanami::Router#get
467
470
  #
468
471
  # @since 0.1.0
469
- def patch(path, options = {}, &blk)
470
- @router.patch(path, options, &blk)
472
+ def patch(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
473
+ add_route(PATCH, path, to, as, namespace, configuration, constraints, &blk)
471
474
  end
472
475
 
473
476
  # Defines a route that accepts a DELETE request for the given path.
@@ -485,8 +488,8 @@ module Hanami
485
488
  # @see Hanami::Router#get
486
489
  #
487
490
  # @since 0.1.0
488
- def delete(path, options = {}, &blk)
489
- @router.delete(path, options, &blk)
491
+ def delete(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
492
+ add_route(DELETE, path, to, as, namespace, configuration, constraints, &blk)
490
493
  end
491
494
 
492
495
  # Defines a route that accepts a TRACE request for the given path.
@@ -504,8 +507,8 @@ module Hanami
504
507
  # @see Hanami::Router#get
505
508
  #
506
509
  # @since 0.1.0
507
- def trace(path, options = {}, &blk)
508
- @router.trace(path, options, &blk)
510
+ def trace(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
511
+ add_route(TRACE, path, to, as, namespace, configuration, constraints, &blk)
509
512
  end
510
513
 
511
514
  # Defines a route that accepts a LINK request for the given path.
@@ -523,8 +526,8 @@ module Hanami
523
526
  # @see Hanami::Router#get
524
527
  #
525
528
  # @since 0.8.0
526
- def link(path, options = {}, &blk)
527
- @router.link(path, options, &blk)
529
+ def link(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
530
+ add_route(LINK, path, to, as, namespace, configuration, constraints, &blk)
528
531
  end
529
532
 
530
533
  # Defines a route that accepts an UNLINK request for the given path.
@@ -542,8 +545,27 @@ module Hanami
542
545
  # @see Hanami::Router#get
543
546
  #
544
547
  # @since 0.8.0
545
- def unlink(path, options = {}, &blk)
546
- @router.unlink(path, options, &blk)
548
+ def unlink(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
549
+ add_route(UNLINK, path, to, as, namespace, configuration, constraints, &blk)
550
+ end
551
+
552
+ # Defines a route that accepts a OPTIONS request for the given path.
553
+ #
554
+ # @param path [String] the relative URL to be matched
555
+ #
556
+ # @param options [Hash] the options to customize the route
557
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
558
+ #
559
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
560
+ #
561
+ # @return [Hanami::Routing::Route] this may vary according to the :route
562
+ # option passed to the constructor
563
+ #
564
+ # @see Hanami::Router#get
565
+ #
566
+ # @since 0.1.0
567
+ def options(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
568
+ add_route(OPTIONS, path, to, as, namespace, configuration, constraints, &blk)
547
569
  end
548
570
 
549
571
  # Defines a root route (a GET route for '/')
@@ -572,27 +594,8 @@ module Hanami
572
594
  #
573
595
  # router.path(:root) # => "/"
574
596
  # router.url(:root) # => "https://hanamirb.org/"
575
- def root(options = {}, &blk)
576
- @router.get(ROOT_PATH, options.merge(as: :root), &blk)
577
- end
578
-
579
- # Defines a route that accepts a OPTIONS request for the given path.
580
- #
581
- # @param path [String] the relative URL to be matched
582
- #
583
- # @param options [Hash] the options to customize the route
584
- # @option options [String,Proc,Class,Object#call] :to the endpoint
585
- #
586
- # @param blk [Proc] the anonymous proc to be used as endpoint for the route
587
- #
588
- # @return [Hanami::Routing::Route] this may vary according to the :route
589
- # option passed to the constructor
590
- #
591
- # @see Hanami::Router#get
592
- #
593
- # @since 0.1.0
594
- def options(path, options = {}, &blk)
595
- @router.options(path, options, &blk)
597
+ def root(to: nil, as: :root, prefix: Utils::PathPrefix.new, namespace: nil, configuration: nil, &blk)
598
+ add_route(GET, prefix.join(ROOT_PATH), to, as, namespace, configuration, &blk)
596
599
  end
597
600
 
598
601
  # Defines an HTTP redirect
@@ -621,57 +624,75 @@ module Hanami
621
624
  #
622
625
  # router = Hanami::Router.new
623
626
  # router.redirect '/legacy', to: '/new_endpoint'
624
- def redirect(path, options = {}, &endpoint)
625
- destination_path = @router.find(options)
626
- get(path).redirect(destination_path, options[:code] || 301).tap do |route|
627
- route.dest = Hanami::Routing::RedirectEndpoint.new(destination_path, route.dest)
628
- end
627
+ def redirect(path, to:, code: 301)
628
+ to = Routing::Redirect.new(@prefix.join(to).to_s, code)
629
+ add_route(GET, path, to)
629
630
  end
630
631
 
631
- # Defines a Ruby block: all the routes defined within it will be namespaced
632
+ # Defines a Ruby block: all the routes defined within it will be prefixed
632
633
  # with the given relative path.
633
634
  #
634
- # Namespaces blocks can be nested multiple times.
635
+ # Prefix blocks can be nested multiple times.
635
636
  #
636
- # @param namespace [String] the relative path where the nested routes will
637
+ # @param path [String] the relative path where the nested routes will
637
638
  # be mounted
638
639
  # @param blk [Proc] the block that defines the resources
639
640
  #
640
- # @return [Hanami::Routing::Namespace] the generated namespace.
641
+ # @return [void]
641
642
  #
642
- # @since 0.1.0
643
+ # @since 2.0.0
643
644
  #
644
645
  # @see Hanami::Router
645
646
  #
646
647
  # @example Basic example
647
- # require 'hanami/router'
648
+ # require "hanami/router"
648
649
  #
649
650
  # Hanami::Router.new do
650
- # namespace 'trees' do
651
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
651
+ # prefix "trees" do
652
+ # get "/sequoia", to: endpoint # => "/trees/sequoia"
652
653
  # end
653
654
  # end
654
655
  #
655
- # @example Nested namespaces
656
- # require 'hanami/router'
656
+ # @example Nested prefix
657
+ # require "hanami/router"
657
658
  #
658
659
  # Hanami::Router.new do
659
- # namespace 'animals' do
660
- # namespace 'mammals' do
661
- # get '/cats', to: endpoint # => '/animals/mammals/cats'
660
+ # prefix "animals" do
661
+ # prefix "mammals" do
662
+ # get "/cats", to: endpoint # => "/animals/mammals/cats"
662
663
  # end
663
664
  # end
664
665
  # end
666
+ def prefix(path, namespace: nil, configuration: nil, &blk)
667
+ Routing::Prefix.new(self, path, namespace, configuration, &blk)
668
+ end
669
+
670
+ # Defines a scope for routes.
671
+ #
672
+ # A scope is a combination of a path prefix and a Ruby namespace.
673
+ #
674
+ # @param prefix [String] the path prefix
675
+ # @param namespace [Module] the Ruby namespace where to lookup endpoints
676
+ # @param configuration [Hanami::Controller::Configuration] the action
677
+ # configuration
678
+ # @param blk [Proc] the routes definition block
679
+ #
680
+ # @since 2.0.0
681
+ # @api private
665
682
  #
666
683
  # @example
667
- # require 'hanami/router'
684
+ # require "hanami/router"
685
+ # require "hanami/controller"
668
686
  #
669
- # router = Hanami::Router.new
670
- # router.namespace 'trees' do
671
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
687
+ # configuration = Hanami::Controller::Configuration.new
688
+ #
689
+ # Hanami::Router.new do
690
+ # scope "/admin", namespace: Admin::Controllers, configuration: configuration do
691
+ # root to: "home#index"
692
+ # end
672
693
  # end
673
- def namespace(namespace, &blk)
674
- Routing::Namespace.new(self, namespace, &blk)
694
+ def scope(prefix, namespace:, configuration:, &blk)
695
+ Routing::Scope.new(self, prefix, namespace, configuration, &blk)
675
696
  end
676
697
 
677
698
  # Defines a set of named routes for a single RESTful resource.
@@ -793,7 +814,7 @@ module Hanami
793
814
  # # | GET | /identity/keys | Identity::Keys | | :keys_identity |
794
815
  # # +------+----------------+----------------+------+----------------+
795
816
  def resource(name, options = {}, &blk)
796
- Routing::Resource.new(self, name, options.merge(separator: @router.action_separator), &blk)
817
+ Routing::Resource.new(self, name, options.merge(separator: Routing::Endpoint::ACTION_SEPARATOR), &blk)
797
818
  end
798
819
 
799
820
  # Defines a set of named routes for a plural RESTful resource.
@@ -916,7 +937,7 @@ module Hanami
916
937
  # # | GET | /articles/search | Articles::Search | | :search_articles |
917
938
  # # +------+------------------+------------------+------+------------------+
918
939
  def resources(name, options = {}, &blk)
919
- Routing::Resources.new(self, name, options.merge(separator: @router.action_separator), &blk)
940
+ Routing::Resources.new(self, name, options.merge(separator: Routing::Endpoint::ACTION_SEPARATOR), &blk)
920
941
  end
921
942
 
922
943
  # Mount a Rack application at the specified path.
@@ -1004,8 +1025,9 @@ module Hanami
1004
1025
  # # 3. RackThree is used as it is (object), because it respond to #call
1005
1026
  # # 4. That Proc is used as it is, because it respond to #call
1006
1027
  # # 5. That string is resolved as Dashboard::Index (Hanami::Controller)
1007
- def mount(app, options)
1008
- @router.mount(app, options)
1028
+ def mount(app, at:, host: nil)
1029
+ app = App.new(@prefix.join(at).to_s, Routing::Endpoint.find(app, @namespace), host: host)
1030
+ @routes.push(app)
1009
1031
  end
1010
1032
 
1011
1033
  # Resolve the given Rack env to a registered endpoint and invoke it.
@@ -1016,7 +1038,15 @@ module Hanami
1016
1038
  #
1017
1039
  # @since 0.1.0
1018
1040
  def call(env)
1019
- @router.call(env)
1041
+ (@routes.find { |r| r.match?(env) } || fallback(env)).call(env)
1042
+ end
1043
+
1044
+ def fallback(env)
1045
+ if @routes.find { |r| r.match_path?(env) }
1046
+ @not_allowed
1047
+ else
1048
+ @not_found
1049
+ end
1020
1050
  end
1021
1051
 
1022
1052
  # Recognize the given env, path, or name and return a route for testing
@@ -1128,14 +1158,11 @@ module Hanami
1128
1158
  # route.routable? # => false
1129
1159
  # route.params # => {:id=>"1"}
1130
1160
  def recognize(env, options = {}, params = nil)
1131
- require 'hanami/routing/recognized_route'
1132
-
1133
- env = env_for(env, options, params)
1134
- responses, _ = *@router.recognize(env)
1161
+ env = env_for(env, options, params)
1162
+ # FIXME: this finder is shared with #call and should be extracted
1163
+ route = @routes.find { |r| r.match?(env) }
1135
1164
 
1136
- Routing::RecognizedRoute.new(
1137
- responses.nil? ? responses : responses.first,
1138
- env, @router)
1165
+ Routing::RecognizedRoute.new(route, env, @namespace)
1139
1166
  end
1140
1167
 
1141
1168
  # Generate an relative URL for a specified named route.
@@ -1161,8 +1188,10 @@ module Hanami
1161
1188
  # router.path(:login) # => "/login"
1162
1189
  # router.path(:login, return_to: '/dashboard') # => "/login?return_to=%2Fdashboard"
1163
1190
  # router.path(:framework, name: 'router') # => "/router"
1164
- def path(route, *args)
1165
- @router.path(route, *args)
1191
+ def path(route, args = {})
1192
+ @named.fetch(route).path(args)
1193
+ rescue KeyError
1194
+ raise Hanami::Routing::InvalidRouteException.new("No route could be generated for #{route.inspect} - please check given arguments")
1166
1195
  end
1167
1196
 
1168
1197
  # Generate a URL for a specified named route.
@@ -1188,8 +1217,8 @@ module Hanami
1188
1217
  # router.url(:login) # => "https://hanamirb.org/login"
1189
1218
  # router.url(:login, return_to: '/dashboard') # => "https://hanamirb.org/login?return_to=%2Fdashboard"
1190
1219
  # router.url(:framework, name: 'router') # => "https://hanamirb.org/router"
1191
- def url(route, *args)
1192
- @router.url(route, *args)
1220
+ def url(route, args = {})
1221
+ @base + path(route, args)
1193
1222
  end
1194
1223
 
1195
1224
  # Returns an routes inspector
@@ -1214,8 +1243,8 @@ module Hanami
1214
1243
  # POST /login Sessions::Create
1215
1244
  # logout GET, HEAD /logout Sessions::Destroy
1216
1245
  def inspector
1217
- require 'hanami/routing/routes_inspector'
1218
- Routing::RoutesInspector.new(@router.routes, @router.prefix)
1246
+ require "hanami/routing/routes_inspector"
1247
+ Routing::RoutesInspector.new(@routes, @prefix)
1219
1248
  end
1220
1249
 
1221
1250
  protected
@@ -1233,8 +1262,8 @@ module Hanami
1233
1262
  #
1234
1263
  # @see Hanami::Router#recognize
1235
1264
  # @see http://www.rubydoc.info/github/rack/rack/Rack%2FMockRequest.env_for
1236
- def env_for(env, options = {}, params = nil)
1237
- env = case env
1265
+ def env_for(env, options = {}, params = nil) # rubocop:disable Metrics/MethodLength
1266
+ case env
1238
1267
  when String
1239
1268
  Rack::MockRequest.env_for(env, options)
1240
1269
  when Symbol
@@ -1248,5 +1277,89 @@ module Hanami
1248
1277
  env
1249
1278
  end
1250
1279
  end
1280
+
1281
+ private
1282
+
1283
+ PATH_INFO = "PATH_INFO"
1284
+ SCRIPT_NAME = "SCRIPT_NAME"
1285
+ SERVER_NAME = "SERVER_NAME"
1286
+ REQUEST_METHOD = "REQUEST_METHOD"
1287
+
1288
+ PARAMS = "router.params"
1289
+
1290
+ GET = "GET"
1291
+ HEAD = "HEAD"
1292
+ POST = "POST"
1293
+ PUT = "PUT"
1294
+ PATCH = "PATCH"
1295
+ DELETE = "DELETE"
1296
+ TRACE = "TRACE"
1297
+ OPTIONS = "OPTIONS"
1298
+ LINK = "LINK"
1299
+ UNLINK = "UNLINK"
1300
+
1301
+ NOT_FOUND = ->(_) { [404, { "Content-Length" => "9" }, ["Not Found"]] }.freeze
1302
+ NOT_ALLOWED = ->(_) { [405, { "Content-Length" => "18" }, ["Method Not Allowed"]] }.freeze
1303
+ ROOT = "/"
1304
+
1305
+ BODY = 2
1306
+
1307
+ attr_reader :configuration
1308
+
1309
+ # Application
1310
+ #
1311
+ # @since 2.0.0
1312
+ # @api private
1313
+ class App
1314
+ def initialize(path, endpoint, host: nil)
1315
+ @path = Mustermann.new(path, type: :rails, version: "5.0")
1316
+ @prefix = path.to_s
1317
+ @endpoint = endpoint
1318
+ @host = host
1319
+ freeze
1320
+ end
1321
+
1322
+ def match?(env)
1323
+ match_path?(env)
1324
+ end
1325
+
1326
+ def match_path?(env)
1327
+ result = env[PATH_INFO].start_with?(@prefix)
1328
+ result &&= @host == env[SERVER_NAME] unless @host.nil?
1329
+
1330
+ result
1331
+ end
1332
+
1333
+ def call(env)
1334
+ env[PARAMS] ||= {}
1335
+ env[PARAMS].merge!(Utils::Hash.deep_symbolize(@path.params(env[PATH_INFO]) || {}))
1336
+
1337
+ env[SCRIPT_NAME] = @prefix
1338
+ env[PATH_INFO] = env[PATH_INFO].sub(@prefix, "")
1339
+ env[PATH_INFO] = "/" if env[PATH_INFO] == ""
1340
+
1341
+ @endpoint.call(env)
1342
+ end
1343
+ end
1344
+
1345
+ def add_route(verb, path, to, as = nil, namespace = nil, config = nil, constraints = {}, &blk)
1346
+ to ||= blk
1347
+ config ||= configuration
1348
+
1349
+ path = path.to_s
1350
+ endpoint = Routing::Endpoint.find(to, namespace || @namespace, config)
1351
+ route = Routing::Route.new(verb_for(verb), @prefix.join(path).to_s, endpoint, constraints)
1352
+
1353
+ @routes.push(route)
1354
+ @named[as] = route unless as.nil?
1355
+ end
1356
+
1357
+ def verb_for(value)
1358
+ if value == GET
1359
+ [GET, HEAD]
1360
+ else
1361
+ [value]
1362
+ end
1363
+ end
1251
1364
  end
1252
1365
  end